/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.framework.uitools; // JDK import java.awt.Dimension; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.Collection; import java.util.Iterator; import java.util.Vector; import javax.swing.JComponent; import javax.swing.event.SwingPropertyChangeSupport; /** * This class is responsible to set a preferred width on the registered objects * (either <code>JComponent</code> or <code>ComponentAligner</code>) based * on the widest component. Although <code>Component</code> should be accepted, * <code>JComponent</code> s are required because * {@link JComponent#setPreferredSize(java.awt.Dimension)}and * {@link JComponent#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)} * is not defined for <code>Component</code>. * <p> * Note: Apparently * {@link JComponent#addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)} * will migrate to <code>Component</code> in JDK 1.5 but * {@link JComponent#setPreferredSize(java.awt.Dimension)} remains on * <code>JComponent</code>. * * @version 10.1.3 * @author Pascal Filion */ public final class ComponentAligner { /** * <code>true</code> if the length of every component needs to be updated * when component are added or removed; <code>false</code> to add or remove * the component and then at the end invoke {@link #revalidatePreferredSize()}. */ private boolean autoValidate; /** * The utility class used to support bound properties. */ private PropertyChangeSupport changeSupport; /** * Prevents infinite recursion when recalculating the preferred width. * This happens in an hierarchy of <code>ComponentAligner</code>s. The lock * has to be placed here and not in the {@link ComponentAlignerWrapper}. */ private boolean locked; /** * The length of the widest component. If the length was not calulcated, then * this value is -1. */ private int maximumWidth; /** * The listener added to each of the components that listens only to a text * change. */ private PropertyChangeListener propertyChangeListener; /** * The collection of {@link Wrapper}s encapsulating either * <code>JComponent</code>s or {@link ComponentAligner}s. */ private Collection/*<Wrapper>*/ wrappers; /** * The property name used to listen to the change of text in a component. */ private static final String TEXT_PROPERTY = "text"; /** * Creates a new <code>ComponentAligner</code> with the autovalidating active. */ public ComponentAligner() { this(true); } /** * Creates a new <code>ComponentAligner</code>. * * @param autoValidate <code>true</code> if the length of every component * needs to be updated when component are added or removed; <code>false</code> * to add or remove the component and then at the end invoke * {@link #revalidatePreferredSize()} */ public ComponentAligner(boolean autoValidate) { super(); initialize(autoValidate); } /** * Creates a new <code>ComponentAligner</code>. * * @param items The collection of <code>JComponent</code>s and/or * <code>ComponentAligner</code>s */ public ComponentAligner(Collection components) { this(); addAll(components); } /** * Creates a new <code>ComponentAligner</code>. * * @param items The collection of <code>JComponent</code>s and/or * <code>ComponentAligner</code>s * @param autoValidate <code>true</code> if the length of every component * needs to be updated when component are added or removed; <code>false</code> * to add or remove the component and then at the end invoke * {@link #revalidatePreferredSize()} */ public ComponentAligner(Collection items, boolean autoValidate) { this(autoValidate); addAll(items); } /** * Adds the given component. Its preferred width will be used along with the * width of all the other components in order to get the widest component and * use its width as the width for all the components. * * @param componentAligner The <code>ComponentAligner</code> to be added * @exception IllegalArgumentException Can't add the ComponentAligner to itself */ public void add(ComponentAligner componentAligner) { if (componentAligner == this) throw new IllegalArgumentException("Can't add the ComponentAligner to itself"); Wrapper wrapper = buildWrapper(componentAligner); wrapper.addPropertyChangeListener(propertyChangeListener); wrappers.add(wrapper); if (!componentAligner.wrappers.isEmpty()) revalidate(); } /** * Adds the given component. Its preferred width will be used along with the * width of all the other componentw in order to get the widest component and * use its width as the width for all the components. * * @param component The component to be added */ public void add(JComponent component) { Wrapper wrapper = buildWrapper(component); wrapper.addPropertyChangeListener(propertyChangeListener); wrappers.add(wrapper); revalidate(); } /** * Adds the items contained in the given collection into this * <code>ComponentAligner</code>. The preferred width of each item will be * used along with the width of all the other items in order to get the * widest component and use its width as the width for all the components. * * @param items The collection of <code>JComponent</code>s and/or * <code>ComponentAligner</code>s */ public void addAll(Collection items) { // Deactivate the auto validation while adding all the JComponents and/or // ComponentAligners in order to improve performance boolean oldAutoValidate = autoValidate; autoValidate = false; for (Iterator iter = items.iterator(); iter.hasNext();) { Object item = iter.next(); if (item instanceof ComponentAligner) add((ComponentAligner) item); else add((JComponent) item); } autoValidate = oldAutoValidate; revalidate(); } /** * Adds a <code>PropertyChangeListener</code> for a specific property. The * listener will be invoked only when a call on <code>firePropertyChange</code> * names that specific property. * * @param listener The <code>PropertyChangeListener</code> to be added */ private void addPropertyChangeListener(PropertyChangeListener listener) { if (changeSupport == null) changeSupport = new SwingPropertyChangeSupport(this); changeSupport.addPropertyChangeListener(TEXT_PROPERTY, listener); } /** * Creates a new <code>Wrapper</code> that encapsulates the given source. * * @param componentAligner The <code>ComponentAligner</code> to be wrapped * @return A new {@link ComponentAlignerWrapper} */ private Wrapper buildWrapper(ComponentAligner componentAligner) { return new ComponentAlignerWrapper(componentAligner); } /** * Creates a new <code>Wrapper</code> that encapsulates the given source. * * @param component The component to be wrapped * @return A new {@link ComponentWrapper} */ private Wrapper buildWrapper(JComponent component) { return new ComponentWrapper(component); } /** * Reports a bound property change. * * @param oldValue the old value of the property (as an int) * @param newValue the new value of the property (as an int) */ private void firePropertyChange(int oldValue, int newValue) { if ((changeSupport != null) && (oldValue != newValue)) { changeSupport.firePropertyChange(TEXT_PROPERTY, new Integer(oldValue), new Integer(newValue)); } } /** * Returns the length of the widest component. If the length was not * calulcated, then this value is -1. * * @return The width of the widest component or -1 if the length has not * been calulated yet */ public int getMaximumWidth() { return maximumWidth; } /** * Returns the preferred size by determining which component has the greatest * width. * * @return The preferred size of this <code>ComponentAligner</code>, which is * {@link #getMaximumWidth()} for the width */ private Dimension getPreferredSize() { if (maximumWidth == -1) recalculateWidth(); return new Dimension(maximumWidth, 0); } /** * Initializes this <code>ComponentAligner</code>. * * @param autoValidate <code>true</code> if the length of every component * needs to be updated when component are added or removed; <code>false</code> * to add or remove the component and then at the end invoke * {@link #revalidatePreferredSize()} */ private void initialize(boolean autoValidate) { this.autoValidate = autoValidate; this.maximumWidth = -1; this.propertyChangeListener = new PropertyChangeHandler(); this.wrappers = new Vector/*<Wrapper>*/(); } /** * Invalidates the preferred size of the given object. * * @param source The source object to be invalidated */ private void invalidate(Object source) { Wrapper wrapper = retrieveWrapper(source); if (wrapper.isLocked()) return; Dimension size = wrapper.getCachedSize(); size.width = 0; size.height = 0; wrapper.setPreferredSize(null); } /** * Determines whether the length of each component should be set each time a * component is added or removed. If the component's text is changed and * {@link #autoValidate()}returns <code>true</code> then the length of each * component is automatically updated. When <code>false</code> is returned, * {@link #revalidatePreferredSize()}has to be called manually. * * @return <code>true</code> to recalculate the length of every component * when a component is either added or removed; <code>false</code> to allow * all the components to be either added or removed before invoking * {@link #revalidatePreferredSize()} */ public boolean isAutoValidate() { return autoValidate; } /** * Updates the maximum length based on the widest component. This methods * does not update the width of the components. */ private void recalculateWidth() { int width = -1; for (Iterator iter = wrappers(); iter.hasNext() ;) { Wrapper wrapper = (Wrapper) iter.next(); Dimension size = wrapper.getCachedSize(); // The preferred size has not been calculated yet if (size.height == 0) { Dimension newSize = wrapper.getPreferredSize(); size.width = newSize.width; size.height = newSize.height; } // Only keep the greatest width width = Math.max(size.width, width); } locked = true; setMaximumWidth(width); locked = false; } /** * Removes the given <code>ComponentAligner</code>. Its preferred width * will not be used when calculating the widest component. * * @param componentAligner The <code>componentAligner</code> to be removed */ public void remove(ComponentAligner componentAligner) { Wrapper wrapper = retrieveWrapper(componentAligner); wrapper.removePropertyChangeListener(propertyChangeListener); wrappers.remove(wrapper); revalidate(); } /** * Removes the given component. Its preferred width will not be used when * calculating the widest component. * * @param component The component to be removed */ public void remove(JComponent component) { Wrapper wrapper = retrieveWrapper(component); wrapper.removePropertyChangeListener(propertyChangeListener); wrappers.remove(wrapper); revalidate(); } /** * Removes the given <code>PropertyChangeListener</code>. * * @param listener The <code>PropertyChangeListener</code> to be removed */ private void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(TEXT_PROPERTY, listener); if (!changeSupport.hasListeners(TEXT_PROPERTY)) changeSupport = null; } /** * Retrieves the <code>Wrapper</code> that is encapsulating the given object. * * @param source Either a <code>JComponent</code> or a <code>ComponentAligner</code> * @return Its <code>Wrapper</code> */ private Wrapper retrieveWrapper(Object source) { for (Iterator iter = wrappers(); iter.hasNext(); ) { Wrapper wrapper = (Wrapper) iter.next(); if (wrapper.getSource() == source) return wrapper; } throw new IllegalArgumentException("Can't retrieve the Wrapper for " + source); } /** * If the count of component is greater than one and {@link #autoValidate()} * returns <code>true<code>, then the preferred size of all the registered * <code>JComponent</code>s will be udpated. */ private void revalidate() { if (isAutoValidate()) revalidatePreferredSize(); } /** * Updates the preferred size of every component based on the widest * component. */ public void revalidatePreferredSize() { recalculateWidth(); revalidatePreferredSizeImp(); } /** * Updates the preferred size of every component based on the widest * component. */ private void revalidatePreferredSizeImp() { // Set the preferred width for every component for (Iterator iter = wrappers(); iter.hasNext() ;) { Wrapper wrapper = (Wrapper) iter.next(); Dimension size = wrapper.getCachedSize(); size = new Dimension(maximumWidth, size.height); wrapper.setPreferredSize(size); } } /** * Determines whether the length of each component should be set each time a * component is added or removed. If the component's text is changed and * {@link #autoValidate()}returns <code>true</code> then the length of each * component is automatically updated. When <code>false</code> is returned, * {@link #revalidatePreferredSize()}has to be called manually. * * @param autoValidate <code>true</code> if the length of every component * needs to be updated when components are added or removed; <code>false</code> * to add or remove the component and then at the end invoke * {@link #revalidatePreferredSize()} */ public void setAutoValidate(boolean autoValidate) { boolean oldAutoValidate = isAutoValidate(); this.autoValidate = autoValidate; if (!oldAutoValidate) revalidate(); } /** * Sets the length of the widest component. If the length was not calulcated, * then this value is -1. * * @param maximumWidth The width of the widest component */ private void setMaximumWidth(int maximumWidth) { int oldMaximumWidth = getMaximumWidth(); this.maximumWidth = maximumWidth; firePropertyChange(oldMaximumWidth, maximumWidth); } /** * Returns an iterator over the set of <code>Component</code>s. * * @return The iterator of <code>Component</code>s */ private Iterator/*<Wrapper>*/ wrappers() { return wrappers.iterator(); } /** * This <code>Wrapper</code> encapsulates a {@link ComponentAligner}. */ private class ComponentAlignerWrapper implements Wrapper { /** * The cached size, which is {@link ComponentAligner#maximumWidth}. */ private Dimension cachedSize; /** * The <code>ComponentAligner</code> encapsulated by this * <code>Wrapper</code>. */ private final ComponentAligner componentAligner; /** * Creates a new <code>ComponentAlignerWrapper</code> that encapsulates * the given <code>ComponentAligner</code>. * * @param componentAligner The <code>ComponentAligner</code> to be * encapsulated by this <code>Wrapper</code> */ private ComponentAlignerWrapper(ComponentAligner componentAligner) { super(); this.componentAligner = componentAligner; cachedSize = new Dimension(componentAligner.maximumWidth, 0); } /** * Adds a <code>PropertyChangeListener</code> for a specific property. * The listener will be invoked only when a call on * <code>firePropertyChange</code> names that specific property. * * @param listener The <code>PropertyChangeListener</code> to be added */ public void addPropertyChangeListener(PropertyChangeListener listener) { componentAligner.addPropertyChangeListener(listener); } /** * Returns the cached size of the encapsulated source. * * @return A non-<code>null</code> size */ public Dimension getCachedSize() { return cachedSize; } /** * Returns the preferred size of the encapsulated source. * * @return The preferred size */ public Dimension getPreferredSize() { return componentAligner.getPreferredSize(); } /** * Returns the encapsulated object. * * @return A <code>ComponentAligner</code> */ public Object getSource() { return componentAligner; } /** * Prevents infinite recursion when recalculating the preferred width. * This happens in an hierarchy of <code>ComponentAligner</code>s. * * @return <code>true<code> to prevent this <code>Wrapper</code> from * being invalidated; otherwise <code>false<code> */ public boolean isLocked() { return componentAligner.locked; } /** * Removes a <code>PropertyChangeListener</code> for a specific property. * If listener is <code>null</code>, no exception is thrown and no * action is performed. * * @param listener The <code>PropertyChangeListener</code> to be removed */ public void removePropertyChangeListener(PropertyChangeListener listener) { componentAligner.removePropertyChangeListener(listener); } /** * Sets the preferred size on the encapsulated source. * * @param preferredSize The new preferred size */ public void setPreferredSize(Dimension preferredSize) { if (preferredSize == null) { componentAligner.maximumWidth = -1; } else if (componentAligner.maximumWidth != preferredSize.width) { componentAligner.maximumWidth = preferredSize.width; componentAligner.revalidatePreferredSizeImp(); } } } /** * This <code>Wrapper</code> encapsulates a {@link JComponent}. */ private class ComponentWrapper implements Wrapper { /** * The cached size, which is component's preferred size. */ private Dimension cachedSize; /** * The component to be encapsulated by this <code>Wrapper</code>. */ private final JComponent component; /** * Creates a new <code>ComponentWrapper</code> that encapsulates the given * component. * * @param component The component to be encapsulated by this <code>Wrapper</code> */ private ComponentWrapper(JComponent component) { super(); this.component = component; cachedSize = new Dimension(); } /** * Adds a <code>PropertyChangeListener</code> for a specific property. * The listener will be invoked only when a call on * <code>firePropertyChange</code> names that specific property. * * @param listener The <code>PropertyChangeListener</code> to be added */ public void addPropertyChangeListener(PropertyChangeListener listener) { component.addPropertyChangeListener(TEXT_PROPERTY, listener); } /** * Returns the cached size of the encapsulated source. * * @return A non-<code>null</code> size */ public Dimension getCachedSize() { return cachedSize; } /** * Returns the preferred size of the encapsulated source. * * @return The preferred size */ public Dimension getPreferredSize() { return component.getPreferredSize(); } /** * Returns the encapsulated object. * * @return A <code>JComponent</code> */ public Object getSource() { return component; } /** * Prevents infinite recursion when recalculating the preferred width. * This happens in an hierarchy of <code>ComponentAligner</code>s. * * @return <code>false<code> is always returned for a <code>JComponent</code> */ public boolean isLocked() { return false; } /** * Removes a <code>PropertyChangeListener</code> for a specific property. * If listener is <code>null</code>, no exception is thrown and no * action is performed. * * @param propertyName The name of the property that was listened on * @param listener The <code>PropertyChangeListener</code> to be removed */ public void removePropertyChangeListener(PropertyChangeListener listener) { component.removePropertyChangeListener(TEXT_PROPERTY, listener); } /** * Sets the preferred size on the encapsulated source. * * @param preferredSize The new preferred size */ public void setPreferredSize(Dimension preferredSize) { component.setPreferredSize(preferredSize); } } /** * The listener added to each of the component that listens only to a text * change. */ private class PropertyChangeHandler implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent e) { invalidate(e.getSource()); revalidate(); } } /** * This <code>Wrapper</code> helps to encapsulate heterogeneous objects and * apply the same behavior on them. */ private interface Wrapper { /** * Adds a <code>PropertyChangeListener</code> for a specific property. * The listener will be invoked only when a call on * <code>firePropertyChange</code> names that specific property. * * @param listener The <code>PropertyChangeListener</code> to be added */ public void addPropertyChangeListener(PropertyChangeListener listener); /** * Returns the cached size of the encapsulated source. * * @return A non-<code>null</code> size */ public Dimension getCachedSize(); /** * Returns the preferred size of the encapsulated source. * * @return The preferred size */ public Dimension getPreferredSize(); /** * Returns the encapsulated object. * * @return The object that is been wrapped */ public Object getSource(); /** * Prevents infinite recursion when recalculating the preferred width. * This happens in an hierarchy of <code>ComponentAligner</code>s. * * @return <code>true<code> to prevent this <code>Wrapper</code> from * being invalidated; otherwise <code>false<code> */ public boolean isLocked(); /** * Removes a <code>PropertyChangeListener</code> for a specific property. * If listener is <code>null</code>, no exception is thrown and no * action is performed. * * @param listener The <code>PropertyChangeListener</code> to be removed */ public void removePropertyChangeListener(PropertyChangeListener listener); /** * Sets the preferred size on the encapsulated source. * * @param preferredSize The new preferred size */ public void setPreferredSize(Dimension preferredSize); } }