0001: /*
0002: * Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
0003: *
0004: * Redistribution and use in source and binary forms, with or without
0005: * modification, are permitted provided that the following conditions are met:
0006: *
0007: * o Redistributions of source code must retain the above copyright notice,
0008: * this list of conditions and the following disclaimer.
0009: *
0010: * o Redistributions in binary form must reproduce the above copyright notice,
0011: * this list of conditions and the following disclaimer in the documentation
0012: * and/or other materials provided with the distribution.
0013: *
0014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
0015: * its contributors may be used to endorse or promote products derived
0016: * from this software without specific prior written permission.
0017: *
0018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
0019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
0020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
0021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
0022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
0023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
0024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
0025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
0026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
0027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
0028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0029: */
0030:
0031: package com.jgoodies.binding.adapter;
0032:
0033: import java.awt.Color;
0034: import java.awt.Component;
0035: import java.awt.KeyboardFocusManager;
0036: import java.awt.event.FocusAdapter;
0037: import java.awt.event.FocusEvent;
0038: import java.beans.PropertyChangeEvent;
0039: import java.beans.PropertyChangeListener;
0040: import java.beans.PropertyChangeListenerProxy;
0041: import java.beans.PropertyChangeSupport;
0042: import java.lang.ref.ReferenceQueue;
0043: import java.lang.ref.WeakReference;
0044:
0045: import javax.swing.*;
0046: import javax.swing.text.JTextComponent;
0047:
0048: import com.jgoodies.binding.beans.PropertyConnector;
0049: import com.jgoodies.binding.list.SelectionInList;
0050: import com.jgoodies.binding.value.BufferedValueModel;
0051: import com.jgoodies.binding.value.ComponentValueModel;
0052: import com.jgoodies.binding.value.ValueModel;
0053:
0054: /**
0055: * Consists only of static methods that bind Swing components to ValueModels.
0056: * This is one of two helper classes that assist you in establishing a binding:
0057: * 1) this Bindings class binds components that have been created before;
0058: * it wraps ValueModels with the adapters from package
0059: * <code>com.jgoodies.binding.adapter</code>.
0060: * 2) the BasicComponentFactory creates Swing components that are then
0061: * bound using this Bindings class.<p>
0062: *
0063: * If you have an existing factory that vends Swing components,
0064: * you can use this Bindings class to bind them to ValueModels.
0065: * If you don't have such a factory, you can use the BasicComponentFactory
0066: * or a custom subclass to create and bind Swing components.<p>
0067: *
0068: * The binding process for JCheckBox, JRadioButton, and other AbstractButtons
0069: * retains the former enablement state. Before the new (adapting) model
0070: * is set, the enablement is requested from the model, not the button.
0071: * This enablement is set after the new model has been set.<p>
0072: *
0073: * TODO: Consider adding binding methods for JProgressBar,
0074: * JSlider, JSpinner, and JTabbedPane.<p>
0075: *
0076: * TODO: Consider adding connection methods for pairs of bean properties.
0077: * In addition to the PropertyConnector's {@code connect} method,
0078: * this could add boolean operators such as: not, and, or, nor.
0079: *
0080: * @author Karsten Lentzsch
0081: * @version $Revision: 1.32 $
0082: *
0083: * @see com.jgoodies.binding.value.ValueModel
0084: * @see BasicComponentFactory
0085: */
0086: public final class Bindings {
0087:
0088: /**
0089: * A JTextComponent client property key used to store and retrieve
0090: * the BufferedValueModel associated with text components that
0091: * commit on focus lost.
0092: *
0093: * @see #bind(JTextArea, ValueModel, boolean)
0094: * @see #bind(JTextField, ValueModel, boolean)
0095: * @see #flushImmediately()
0096: * @see BufferedValueModel
0097: */
0098: private static final String COMMIT_ON_FOCUS_LOST_MODEL_KEY = "commitOnFocusListModel";
0099:
0100: /**
0101: * A JComponent client property key used to store
0102: * and retrieve an associated ComponentValueModel.
0103: *
0104: * @see #addComponentPropertyHandler(JComponent, ValueModel)
0105: * @see #removeComponentPropertyHandler(JComponent)
0106: * @see ComponentValueModel
0107: */
0108: private static final String COMPONENT_VALUE_MODEL_KEY = "componentValueModel";
0109:
0110: /**
0111: * A JComponent client property key used to store
0112: * and retrieve an associated ComponentPropertyHandler.
0113: *
0114: * @see #addComponentPropertyHandler(JComponent, ValueModel)
0115: * @see #removeComponentPropertyHandler(JComponent)
0116: */
0117: private static final String COMPONENT_PROPERTY_HANDLER_KEY = "componentPropertyHandler";
0118:
0119: /**
0120: * Triggers a commit in the shared focus lost trigger
0121: * if focus is lost permanently. Shared among all components
0122: * that are configured to commit on focus lost.
0123: *
0124: * @see #createCommitOnFocusLostModel(ValueModel, Component)
0125: */
0126: static final FocusLostHandler FOCUS_LOST_HANDLER = new FocusLostHandler();
0127:
0128: /**
0129: * Holds a weak trigger that is shared by BufferedValueModels
0130: * that commit on permanent focus change.
0131: *
0132: * @see #createCommitOnFocusLostModel(ValueModel, Component)
0133: */
0134: static final WeakTrigger FOCUS_LOST_TRIGGER = new WeakTrigger();
0135:
0136: private Bindings() {
0137: // Suppresses default constructor; prevents instantiation.
0138: }
0139:
0140: // Binding Methods ********************************************************
0141:
0142: /**
0143: * Binds a JCheckBox to the given ValueModel and retains the enablement
0144: * state. The bound check box is selected if and only if the model's value
0145: * equals <code>Boolean.TRUE</code>.<p>
0146: *
0147: * The value model is converted to the required interface
0148: * ToggleButtonModel using a ToggleButtonAdapter.
0149: *
0150: * @param checkBox the check box to be bound
0151: * @param valueModel the model that provides a Boolean value
0152: *
0153: * @throws NullPointerException if the checkBox or valueModel
0154: * is <code>null</code>
0155: */
0156: public static void bind(JCheckBox checkBox, ValueModel valueModel) {
0157: boolean enabled = checkBox.getModel().isEnabled();
0158: checkBox.setModel(new ToggleButtonAdapter(valueModel));
0159: checkBox.setEnabled(enabled);
0160:
0161: addComponentPropertyHandler(checkBox, valueModel);
0162: }
0163:
0164: /**
0165: * Binds a JCheckBoxMenuItem to the given ValueModel and retains
0166: * the enablement state. The bound menu item is selected if and only if
0167: * the model's value equals <code>Boolean.TRUE</code>.<p>
0168: *
0169: * <strong>Note:</strong> For users of the JGoodies UIF (user interface
0170: * framework) the recommended way to create and bind check box menu items
0171: * is the class <code>com.jgoodies.uif.ToggleAction</code>.<p>
0172: *
0173: * The value model is converted to the required interface
0174: * <code>ToggleButtonModel</code> using a <code>ToggleButtonAdapter</code>.
0175: *
0176: * @param checkBoxMenuItem the check box menu item to be bound
0177: * @param valueModel the model that provides a Boolean value
0178: *
0179: * @throws NullPointerException if the menu item or valueModel
0180: * is <code>null</code>
0181: */
0182: public static void bind(JCheckBoxMenuItem checkBoxMenuItem,
0183: ValueModel valueModel) {
0184: boolean enabled = checkBoxMenuItem.getModel().isEnabled();
0185: checkBoxMenuItem.setModel(new ToggleButtonAdapter(valueModel));
0186: checkBoxMenuItem.setEnabled(enabled);
0187:
0188: addComponentPropertyHandler(checkBoxMenuItem, valueModel);
0189: }
0190:
0191: /**
0192: * Binds a JColorChooser to the given Color-typed ValueModel.
0193: * The ValueModel must be of type Color and must
0194: * allow read-access to its value.<p>
0195: *
0196: * Also, it is strongly recommended (though not required) that
0197: * the underlying ValueModel provides only non-null values.
0198: * This is so because the ColorSelectionModel behavior is undefined
0199: * for <code>null</code> values and it may have unpredictable results.
0200: * To avoid these problems, you may bind the ColorChooser with
0201: * a default color using {@link #bind(JColorChooser, ValueModel, Color)}.<p>
0202: *
0203: * Since the color chooser is considered a container, not a single
0204: * component, it is not synchronized with the valueModel's
0205: * {@link ComponentValueModel} properties - if any.<p>
0206: *
0207: * <strong>Note:</strong> There's a bug in Java 1.4.2, Java 5 and Java 6
0208: * that affects this binding. The BasicColorChooserUI doesn't listen
0209: * to changes in the selection model, and so the preview panel won't
0210: * update if the selected color changes. As a workaround you can use
0211: * {@link BasicComponentFactory#createColorChooser(ValueModel)},
0212: * or you could use a Look&Feel that fixes the bug mentioned above.
0213: *
0214: * @param colorChooser the color chooser to be bound
0215: * @param valueModel the model that provides non-<code>null</code>
0216: * Color values.
0217: *
0218: * @throws NullPointerException if the color chooser or value model
0219: * is <code>null</code>
0220: *
0221: * @see #bind(JColorChooser, ValueModel, Color)
0222: *
0223: * @since 1.0.3
0224: */
0225: public static void bind(JColorChooser colorChooser,
0226: ValueModel valueModel) {
0227: colorChooser.setSelectionModel(new ColorSelectionAdapter(
0228: valueModel));
0229: }
0230:
0231: /**
0232: * Binds a JColorChooser to the given Color-typed ValueModel.
0233: * The ValueModel must be of type Color and must allow read-access
0234: * to its value. The default color will be used if the valueModel
0235: * returns <code>null</code>.<p>
0236: *
0237: * Since the color chooser is considered a container, not a single
0238: * component, it is not synchronized with the valueModel's
0239: * {@link ComponentValueModel} properties - if any.<p>
0240: *
0241: * <strong>Note:</strong> There's a bug in Java 1.4.2, Java 5 and Java 6
0242: * that affects this binding. The BasicColorChooserUI doesn't listen
0243: * to changes in the selection model, and so the preview panel won't
0244: * update if the selected color changes. As a workaround you can use
0245: * {@link BasicComponentFactory#createColorChooser(ValueModel)},
0246: * or you could use a Look&Feel that fixes the bug mentioned above.
0247: *
0248: * @param colorChooser the color chooser to be bound
0249: * @param valueModel the model that provides non-<code>null</code>
0250: * Color values.
0251: * @param defaultColor the color used if the valueModel returns null
0252: *
0253: * @throws NullPointerException if the color chooser, value model,
0254: * or default color is <code>null</code>
0255: *
0256: * @since 1.1
0257: */
0258: public static void bind(JColorChooser colorChooser,
0259: ValueModel valueModel, Color defaultColor) {
0260: if (defaultColor == null)
0261: throw new NullPointerException(
0262: "The default color must not be null.");
0263:
0264: colorChooser.setSelectionModel(new ColorSelectionAdapter(
0265: valueModel, defaultColor));
0266: }
0267:
0268: /**
0269: * Binds a non-editable JComboBox to the given SelectionInList using
0270: * the SelectionInList's ListModel as list data provider and the
0271: * SelectionInList's selection index holder for the combo box model's
0272: * selected item.<p>
0273: *
0274: * There are a couple of other possibilities to bind a JComboBox.
0275: * See the constructors and the class comment of the
0276: * {@link ComboBoxAdapter}.<p>
0277: *
0278: * Since version 2.0 the combo's <em>enabled</em> and <em>visible</em>
0279: * state is synchronized with the selectionInList's selection holder,
0280: * if it's a {@link ComponentValueModel}.
0281: *
0282: * @param comboBox the combo box to be bound
0283: * @param selectionInList provides the list and selection;
0284: * if the selection holder is a ComponentValueModel, it is synchronized
0285: * with the comboBox properties <em>visible</em> and <em>enabled</em>
0286: * @param <E> the type of the combo box items
0287: *
0288: * @throws NullPointerException if the combo box or the selectionInList
0289: * is <code>null</code>
0290: *
0291: * @see ComboBoxAdapter
0292: *
0293: * @since 1.0.1
0294: */
0295: public static <E> void bind(JComboBox comboBox,
0296: SelectionInList<E> selectionInList) {
0297: if (selectionInList == null)
0298: throw new NullPointerException(
0299: "The SelectionInList must not be null.");
0300:
0301: comboBox.setModel(new ComboBoxAdapter<E>(selectionInList));
0302:
0303: addComponentPropertyHandler(comboBox, selectionInList
0304: .getSelectionHolder());
0305: }
0306:
0307: /**
0308: * Binds the given JFormattedTextField to the specified ValueModel.
0309: * Synchronized the ValueModel's value with the text field's value
0310: * by means of a PropertyConnector.
0311: *
0312: * @param textField the JFormattedTextField that is to be bound
0313: * @param valueModel the model that provides the value
0314: *
0315: * @throws NullPointerException if the text field or valueModel
0316: * is <code>null</code>
0317: */
0318: public static void bind(JFormattedTextField textField,
0319: ValueModel valueModel) {
0320: bind(textField, "value", valueModel);
0321: }
0322:
0323: /**
0324: * Binds the given JLabel to the specified ValueModel.
0325: *
0326: * @param label a label that shall be bound to the given value model
0327: * @param valueModel the model that provides the value
0328: *
0329: * @throws NullPointerException if the label or valueModel is <code>null</code>
0330: */
0331: public static void bind(JLabel label, ValueModel valueModel) {
0332: bind(label, "text", valueModel);
0333: }
0334:
0335: /**
0336: * Binds a JList to the given SelectionInList using the SelectionInList's
0337: * ListModel as list data provider and the SelectionInList's selection
0338: * index holder for the selection model.<p>
0339: *
0340: * Since version 2.0 the list's <em>enabled</em> and <em>visible</em>
0341: * state is synchronized with the selectionInList's selection holder,
0342: * if it's a {@link ComponentValueModel}.
0343: *
0344: * @param list the list to be bound
0345: * @param selectionInList provides the list and selection;
0346: * if the selection holder is a ComponentValueModel, it is synchronized
0347: * with the list properties <em>visible</em> and <em>enabled</em>
0348: * @param <E> the type of the list items
0349: *
0350: * @throws NullPointerException if the list or the selectionInList
0351: * is <code>null</code>
0352: */
0353: public static <E> void bind(JList list,
0354: SelectionInList<E> selectionInList) {
0355: if (selectionInList == null)
0356: throw new NullPointerException(
0357: "The SelectionInList must not be null.");
0358:
0359: list.setModel(selectionInList);
0360: list.setSelectionModel(new SingleListSelectionAdapter(
0361: selectionInList.getSelectionIndexHolder()));
0362:
0363: addComponentPropertyHandler(list, selectionInList
0364: .getSelectionHolder());
0365: }
0366:
0367: /**
0368: * Binds a JRadioButton to the given ValueModel and retains the enablement
0369: * state. The bound radio button is selected if and only if the model's
0370: * value equals the specified choice value.<p>
0371: *
0372: * The model is converted to the required interface
0373: * ToggleButtonModel using a RadioButtonAdapter.
0374: *
0375: * @param radioButton the radio button to be bound to the given model
0376: * @param model the model that provides the current choice
0377: * @param choice this button's value
0378: *
0379: * @throws NullPointerException if the valueModel is <code>null</code>
0380: */
0381: public static void bind(JRadioButton radioButton, ValueModel model,
0382: Object choice) {
0383: boolean enabled = radioButton.getModel().isEnabled();
0384: radioButton.setModel(new RadioButtonAdapter(model, choice));
0385: radioButton.setEnabled(enabled);
0386:
0387: addComponentPropertyHandler(radioButton, model);
0388: }
0389:
0390: /**
0391: * Binds a JRadioButtonMenuItem to the given ValueModel and retains
0392: * the enablement state. The bound menu item is selected if and only if
0393: * the model's value equals the specified choice.<p>
0394: *
0395: * <strong>Note:</strong> For users of the JGoodies UIF (user interface
0396: * framework) the recommended way to create and bind radio button menu items
0397: * is the class <code>com.jgoodies.uif.ToggleAction</code>.<p>
0398: *
0399: * The model is converted to the required interface
0400: * ToggleButtonModel using a RadioButtonAdapter.
0401: *
0402: * @param radioButtonMenuItem the radio item to be bound to the given model
0403: * @param model the model that provides the current choice
0404: * @param choice this button's value
0405: *
0406: * @throws NullPointerException if the valueModel is <code>null</code>
0407: */
0408: public static void bind(JRadioButtonMenuItem radioButtonMenuItem,
0409: ValueModel model, Object choice) {
0410: boolean enabled = radioButtonMenuItem.getModel().isEnabled();
0411: radioButtonMenuItem.setModel(new RadioButtonAdapter(model,
0412: choice));
0413: radioButtonMenuItem.setEnabled(enabled);
0414:
0415: addComponentPropertyHandler(radioButtonMenuItem, model);
0416: }
0417:
0418: /**
0419: * Binds a text area to the given ValueModel.
0420: * The model is updated on every character typed.<p>
0421: *
0422: * TODO: Consider changing the semantics to commit on focus lost.
0423: * This would be consistent with the text component vending factory methods
0424: * in the BasicComponentFactory that have no boolean parameter.
0425: *
0426: * @param textArea the text area to be bound to the value model
0427: * @param valueModel the model that provides the text value
0428: *
0429: * @throws NullPointerException if the text component or valueModel
0430: * is <code>null</code>
0431: */
0432: public static void bind(JTextArea textArea, ValueModel valueModel) {
0433: bind(textArea, valueModel, false);
0434: }
0435:
0436: /**
0437: * Binds a text area to the given ValueModel.
0438: * The model can be updated on focus lost or on every character typed.
0439: * The DocumentAdapter used in this binding doesn't filter newlines.
0440: *
0441: * @param textArea the text area to be bound to the value model
0442: * @param valueModel the model that provides the text value
0443: * @param commitOnFocusLost true to commit text changes on focus lost,
0444: * false to commit text changes on every character typed
0445: *
0446: * @throws NullPointerException if the text component or valueModel
0447: * is <code>null</code>
0448: */
0449: public static void bind(JTextArea textArea, ValueModel valueModel,
0450: boolean commitOnFocusLost) {
0451: if (valueModel == null)
0452: throw new NullPointerException(
0453: "The value model must not be null.");
0454:
0455: ValueModel textModel;
0456: if (commitOnFocusLost) {
0457: textModel = createCommitOnFocusLostModel(valueModel,
0458: textArea);
0459: textArea.putClientProperty(COMMIT_ON_FOCUS_LOST_MODEL_KEY,
0460: textModel);
0461: } else {
0462: textModel = valueModel;
0463: }
0464: TextComponentConnector connector = new TextComponentConnector(
0465: textModel, textArea);
0466: connector.updateTextComponent();
0467: addComponentPropertyHandler(textArea, valueModel);
0468: }
0469:
0470: /**
0471: * Bind a text fields or password field to the given ValueModel.
0472: * The model is updated on every character typed.<p>
0473: *
0474: * <strong>Security Note: </strong> If you use this method to bind a
0475: * JPasswordField, the field's password will be requested as Strings
0476: * from the field and will be held as String by the given ValueModel.
0477: * These password String could potentially be observed in a security fraud.
0478: * For stronger security it is recommended to request a character array
0479: * from the JPasswordField and clear the array after use by setting
0480: * each character to zero. Method {@link JPasswordField#getPassword()}
0481: * return's the field's password as a character array.<p>
0482: *
0483: * TODO: Consider changing the semantics to commit on focus lost.
0484: * This would be consistent with the text component vending factory methods
0485: * in the BasicComponentFactory that have no boolean parameter.
0486: *
0487: * @param textField the text field to be bound to the value model
0488: * @param valueModel the model that provides the text value
0489: *
0490: * @throws NullPointerException if the text component or valueModel
0491: * is <code>null</code>
0492: *
0493: * @see JPasswordField#getPassword()
0494: */
0495: public static void bind(JTextField textField, ValueModel valueModel) {
0496: bind(textField, valueModel, false);
0497: }
0498:
0499: /**
0500: * Binds a text field or password field to the given ValueModel.
0501: * The model can be updated on focus lost or on every character typed.<p>
0502: *
0503: * <strong>Security Note: </strong> If you use this method to bind a
0504: * JPasswordField, the field's password will be requested as Strings
0505: * from the field and will be held as String by the given ValueModel.
0506: * These password String could potentially be observed in a security fraud.
0507: * For stronger security it is recommended to request a character array
0508: * from the JPasswordField and clear the array after use by setting
0509: * each character to zero. Method {@link JPasswordField#getPassword()}
0510: * return's the field's password as a character array.
0511: *
0512: * @param textField the text field to be bound to the value model
0513: * @param valueModel the model that provides the text value
0514: * @param commitOnFocusLost true to commit text changes on focus lost,
0515: * false to commit text changes on every character typed
0516: *
0517: * @throws NullPointerException if the text component or valueModel
0518: * is <code>null</code>
0519: *
0520: * @see JPasswordField#getPassword()
0521: */
0522: public static void bind(JTextField textField,
0523: ValueModel valueModel, boolean commitOnFocusLost) {
0524: if (valueModel == null)
0525: throw new NullPointerException(
0526: "The value model must not be null.");
0527:
0528: ValueModel textModel;
0529: if (commitOnFocusLost) {
0530: textModel = createCommitOnFocusLostModel(valueModel,
0531: textField);
0532: textField.putClientProperty(COMMIT_ON_FOCUS_LOST_MODEL_KEY,
0533: textModel);
0534: } else {
0535: textModel = valueModel;
0536: }
0537: TextComponentConnector connector = new TextComponentConnector(
0538: textModel, textField);
0539: connector.updateTextComponent();
0540: addComponentPropertyHandler(textField, valueModel);
0541: }
0542:
0543: /**
0544: * Binds the specified property of the given JComponent to the specified
0545: * ValueModel. Synchronizes the ValueModel's value with the component's
0546: * property by means of a PropertyConnector.
0547: *
0548: * @param component the JComponent that is to be bound
0549: * @param propertyName the name of the property to be bound
0550: * @param valueModel the model that provides the value
0551: *
0552: * @throws NullPointerException if the component or value model
0553: * or property name is <code>null</code>
0554: *
0555: * @since 1.2
0556: */
0557: public static void bind(JComponent component, String propertyName,
0558: ValueModel valueModel) {
0559: if (component == null)
0560: throw new NullPointerException(
0561: "The component must not be null.");
0562: if (valueModel == null)
0563: throw new NullPointerException(
0564: "The value model must not be null.");
0565: if (propertyName == null)
0566: throw new NullPointerException(
0567: "The property name must not be null.");
0568:
0569: PropertyConnector.connectAndUpdate(valueModel, component,
0570: propertyName);
0571:
0572: addComponentPropertyHandler(component, valueModel);
0573: }
0574:
0575: // Updating Component State on ComponentValueModel Changes ****************
0576:
0577: /**
0578: * If the given model is a ComponentValueModel, a component property handler
0579: * is registered with this model. This handler updates the component state
0580: * if the ComponentValueModel indicates a change in one of its properties,
0581: * for example: <em>visible</em>, <em>enabled</em>, and <em>editable</em>.<p>
0582: *
0583: * Also the ComponentValueModel and the component handler are stored
0584: * as client properties with the component. This way they can be removed
0585: * later using <code>#removeComponentPropertyHandler</code>.
0586: *
0587: * @param component the component where the handler is registered
0588: * @param valueModel the model to observe
0589: *
0590: * @see #removeComponentPropertyHandler(JComponent)
0591: * @see ComponentValueModel
0592: *
0593: * @since 1.1
0594: */
0595: public static void addComponentPropertyHandler(
0596: JComponent component, ValueModel valueModel) {
0597: if (!(valueModel instanceof ComponentValueModel)) {
0598: return;
0599: }
0600: ComponentValueModel cvm = (ComponentValueModel) valueModel;
0601: PropertyChangeListener componentHandler = new ComponentPropertyHandler(
0602: component);
0603: cvm.addPropertyChangeListener(componentHandler);
0604: component.putClientProperty(COMPONENT_VALUE_MODEL_KEY, cvm);
0605: component.putClientProperty(COMPONENT_PROPERTY_HANDLER_KEY,
0606: componentHandler);
0607:
0608: component.setEnabled(cvm.isEnabled());
0609: component.setVisible(cvm.isVisible());
0610: if (component instanceof JTextComponent) {
0611: ((JTextComponent) component).setEditable(cvm.isEditable());
0612: }
0613: }
0614:
0615: /**
0616: * If the given component holds a ComponentValueModel and
0617: * a ComponentPropertyHandler in its client properties,
0618: * the handler is removed as listener from the model,
0619: * and the model and handler are removed from the client properties.
0620: *
0621: * @param component
0622: *
0623: * @see #addComponentPropertyHandler(JComponent, ValueModel)
0624: * @see ComponentValueModel
0625: *
0626: * @since 1.1
0627: */
0628: public static void removeComponentPropertyHandler(
0629: JComponent component) {
0630: ComponentValueModel componentValueModel = (ComponentValueModel) component
0631: .getClientProperty(COMPONENT_VALUE_MODEL_KEY);
0632: PropertyChangeListener componentHandler = (PropertyChangeListener) component
0633: .getClientProperty(COMPONENT_PROPERTY_HANDLER_KEY);
0634: if ((componentValueModel != null) && (componentHandler != null)) {
0635: componentValueModel
0636: .removePropertyChangeListener(componentHandler);
0637: component
0638: .putClientProperty(COMPONENT_VALUE_MODEL_KEY, null);
0639: component.putClientProperty(COMPONENT_PROPERTY_HANDLER_KEY,
0640: null);
0641: } else if ((componentValueModel == null)
0642: && (componentHandler == null)) {
0643: return;
0644: } else if (componentValueModel != null) {
0645: throw new IllegalStateException(
0646: "The component has a ComponentValueModel stored, "
0647: + "but lacks the ComponentPropertyHandler.");
0648: } else {
0649: throw new IllegalStateException(
0650: "The component has a ComponentPropertyHandler stored, "
0651: + "but lacks the ComponentValueModel.");
0652: }
0653: }
0654:
0655: // Misc *******************************************************************
0656:
0657: /**
0658: * Commits a pending edit - if any. Useful to ensure that edited values
0659: * in bound text components that commit on focus-lost are committed
0660: * before an operation is performed that uses the value to be committed
0661: * after a focus lost.<p>
0662: *
0663: * For example, before you save a form, a value that has been edited
0664: * shall be committed, so the validation can check whether the save
0665: * is allowed or not.
0666: *
0667: * @since 1.2
0668: */
0669: public static void commitImmediately() {
0670: FOCUS_LOST_TRIGGER.triggerCommit();
0671: }
0672:
0673: /**
0674: * Flushes a pending edit in the focused text component - if any.
0675: * Useful to revert edited values in bound text components that
0676: * commit on focus-lost. This operation can be performed on an escape
0677: * key event like the Cancel action in the JFormattedTextField.<p>
0678: *
0679: * Returns whether an edit has been reset. Useful to decide whether
0680: * a key event shall be consumed or not.
0681: *
0682: * @return {@code true} if a pending edit has been reset,
0683: * {@code false} if the focused component isn't buffering or
0684: * doesn't buffer at all
0685: *
0686: * @see #isFocusOwnerBuffering()
0687: *
0688: * @since 2.0.1
0689: */
0690: public static boolean flushImmediately() {
0691: boolean buffering = isFocusOwnerBuffering();
0692: if (buffering) {
0693: FOCUS_LOST_TRIGGER.triggerFlush();
0694: }
0695: return buffering;
0696: }
0697:
0698: /**
0699: * Checks and answers whether the focus owner is a component
0700: * that buffers a pending edit. Useful to enable or disable
0701: * a text component Action that resets the edited value.<p>
0702: *
0703: * See also the JFormattedTextField's internal {@code CancelAction}.
0704: *
0705: * @return {@code true} if the focus owner is a JTextComponent
0706: * that commits on focus-lost and is buffering
0707: *
0708: * @see #flushImmediately()
0709: *
0710: * @since 2.0.1
0711: */
0712: public static boolean isFocusOwnerBuffering() {
0713: Component focusOwner = KeyboardFocusManager
0714: .getCurrentKeyboardFocusManager().getFocusOwner();
0715: if (!(focusOwner instanceof JComponent)) {
0716: return false;
0717: }
0718: Object value = ((JComponent) focusOwner)
0719: .getClientProperty(COMMIT_ON_FOCUS_LOST_MODEL_KEY);
0720: if (!(value instanceof BufferedValueModel)) {
0721: return false;
0722: }
0723: BufferedValueModel commitOnFocusLostModel = (BufferedValueModel) value;
0724: return commitOnFocusLostModel.isBuffering();
0725: }
0726:
0727: // Helper Code ************************************************************
0728:
0729: /**
0730: * Creates and returns a ValueModel that commits its value
0731: * if the given component looses the focus permanently.
0732: * It wraps the underlying ValueModel with a BufferedValueModel
0733: * and delays the value commit until this class' shared FOCUS_LOST_TRIGGER
0734: * commits. This happens, because this class' shared FOCUS_LOST_HANDLER
0735: * is registered with the specified component.
0736: *
0737: * @param valueModel the model that provides the value
0738: * @param component the component that looses the focus
0739: * @return a buffering ValueModel that commits on focus lost
0740: *
0741: * @throws NullPointerException if the value model is <code>null</code>
0742: */
0743: private static ValueModel createCommitOnFocusLostModel(
0744: ValueModel valueModel, Component component) {
0745: if (valueModel == null)
0746: throw new NullPointerException(
0747: "The value model must not be null.");
0748:
0749: ValueModel model = new BufferedValueModel(valueModel,
0750: FOCUS_LOST_TRIGGER);
0751: component.addFocusListener(FOCUS_LOST_HANDLER);
0752: return model;
0753: }
0754:
0755: // Helper Classes *********************************************************
0756:
0757: /**
0758: * Triggers a commit event on permanent focus lost.
0759: */
0760: private static final class FocusLostHandler extends FocusAdapter {
0761:
0762: /**
0763: * Triggers a commit event if the focus lost is permanent.
0764: *
0765: * @param evt the focus lost event
0766: */
0767: @Override
0768: public void focusLost(FocusEvent evt) {
0769: if (!evt.isTemporary()) {
0770: FOCUS_LOST_TRIGGER.triggerCommit();
0771: }
0772: }
0773: }
0774:
0775: /**
0776: * Listens to property changes in a ComponentValueModel and
0777: * updates the associated component state.
0778: *
0779: * @see ComponentValueModel
0780: */
0781: private static final class ComponentPropertyHandler implements
0782: PropertyChangeListener {
0783:
0784: private final JComponent component;
0785:
0786: private ComponentPropertyHandler(JComponent component) {
0787: this .component = component;
0788: }
0789:
0790: public void propertyChange(PropertyChangeEvent evt) {
0791: String propertyName = evt.getPropertyName();
0792: ComponentValueModel model = (ComponentValueModel) evt
0793: .getSource();
0794: if (ComponentValueModel.PROPERTYNAME_ENABLED
0795: .equals(propertyName)) {
0796: component.setEnabled(model.isEnabled());
0797: } else if (ComponentValueModel.PROPERTYNAME_VISIBLE
0798: .equals(propertyName)) {
0799: component.setVisible(model.isVisible());
0800: } else if (ComponentValueModel.PROPERTYNAME_EDITABLE
0801: .equals(propertyName)) {
0802: if (component instanceof JTextComponent) {
0803: ((JTextComponent) component).setEditable(model
0804: .isEditable());
0805: }
0806: }
0807: }
0808: }
0809:
0810: // Helper Code for a Weak Trigger *****************************************
0811:
0812: /**
0813: * Unlike the Trigger class, this implementation uses WeakReferences
0814: * to store value change listeners.
0815: */
0816: private static final class WeakTrigger implements ValueModel {
0817:
0818: private final transient WeakPropertyChangeSupport changeSupport;
0819:
0820: private Boolean value;
0821:
0822: // Instance Creation ******************************************************
0823:
0824: /**
0825: * Constructs a WeakTrigger set to neutral.
0826: */
0827: WeakTrigger() {
0828: value = null;
0829: changeSupport = new WeakPropertyChangeSupport(this );
0830: }
0831:
0832: // ValueModel Implementation **********************************************
0833:
0834: /**
0835: * Returns a Boolean that indicates the current trigger state.
0836: *
0837: * @return a Boolean that indicates the current trigger state
0838: */
0839: public Object getValue() {
0840: return value;
0841: }
0842:
0843: /**
0844: * Sets a new Boolean value and rejects all non-Boolean values.
0845: * Fires no change event if the new value is equal to the
0846: * previously set value.<p>
0847: *
0848: * This method is not intended to be used by API users.
0849: * Instead you should trigger commit and flush events by invoking
0850: * <code>#triggerCommit</code> or <code>#triggerFlush</code>.
0851: *
0852: * @param newValue the Boolean value to be set
0853: * @throws IllegalArgumentException if the newValue is not a Boolean
0854: */
0855: public void setValue(Object newValue) {
0856: if ((newValue != null) && !(newValue instanceof Boolean))
0857: throw new IllegalArgumentException(
0858: "Trigger values must be of type Boolean.");
0859:
0860: Object oldValue = value;
0861: value = (Boolean) newValue;
0862: fireValueChange(oldValue, newValue);
0863: }
0864:
0865: // Change Management ****************************************************
0866:
0867: /**
0868: * Registers the given PropertyChangeListener with this model.
0869: * The listener will be notified if the value has changed.<p>
0870: *
0871: * The PropertyChangeEvents delivered to the listener have the name
0872: * set to "value". In other words, the listeners won't get notified
0873: * when a PropertyChangeEvent is fired that has a null object as
0874: * the name to indicate an arbitrary set of the event source's
0875: * properties have changed.<p>
0876: *
0877: * In the rare case, where you want to notify a PropertyChangeListener
0878: * even with PropertyChangeEvents that have no property name set,
0879: * you can register the listener with #addPropertyChangeListener,
0880: * not #addValueChangeListener.
0881: *
0882: * @param listener the listener to add
0883: *
0884: * @see ValueModel
0885: */
0886: public void addValueChangeListener(
0887: PropertyChangeListener listener) {
0888: if (listener == null) {
0889: return;
0890: }
0891: changeSupport.addPropertyChangeListener("value", listener);
0892: }
0893:
0894: /**
0895: * Removes the given PropertyChangeListener from the model.
0896: *
0897: * @param listener the listener to remove
0898: */
0899: public void removeValueChangeListener(
0900: PropertyChangeListener listener) {
0901: if (listener == null) {
0902: return;
0903: }
0904: changeSupport.removePropertyChangeListener("value",
0905: listener);
0906: }
0907:
0908: /**
0909: * Notifies all listeners that have registered interest for
0910: * notification on this event type. The event instance
0911: * is lazily created using the parameters passed into
0912: * the fire method.
0913: *
0914: * @param oldValue the value before the change
0915: * @param newValue the value after the change
0916: *
0917: * @see java.beans.PropertyChangeSupport
0918: */
0919: private void fireValueChange(Object oldValue, Object newValue) {
0920: changeSupport.firePropertyChange("value", oldValue,
0921: newValue);
0922: }
0923:
0924: // Triggering *************************************************************
0925:
0926: /**
0927: * Triggers a commit event in models that share this Trigger.
0928: * Sets the value to {@code Boolean.TRUE} and ensures that listeners
0929: * are notified about a value change to this new value. If necessary
0930: * the value is temporarily set to {@code null}. This way it minimizes
0931: * the number of PropertyChangeEvents fired by this Trigger.
0932: */
0933: void triggerCommit() {
0934: if (Boolean.TRUE.equals(getValue())) {
0935: setValue(null);
0936: }
0937: setValue(Boolean.TRUE);
0938: }
0939:
0940: /**
0941: * Triggers a flush event in models that share this Trigger.
0942: * Sets the value to {@code Boolean.FALSE} and ensures that listeners
0943: * are notified about a value change to this new value. If necessary
0944: * the value is temporarily set to {@code null}. This way it minimizes
0945: * the number of PropertyChangeEvents fired by this Trigger.
0946: */
0947: void triggerFlush() {
0948: if (Boolean.FALSE.equals(getValue())) {
0949: setValue(null);
0950: }
0951: setValue(Boolean.FALSE);
0952: }
0953:
0954: }
0955:
0956: /**
0957: * Differs from its superclass {@link PropertyChangeSupport} in that it
0958: * uses WeakReferences for registering listeners. It wraps registered
0959: * PropertyChangeListeners with instances of WeakPropertyChangeListener
0960: * and cleans up a list of stale references when firing an event.<p>
0961: *
0962: * TODO: Merge this WeakPropertyChangeSupport with the
0963: * ExtendedPropertyChangeSupport.
0964: */
0965: private static final class WeakPropertyChangeSupport extends
0966: PropertyChangeSupport {
0967:
0968: // Instance Creation ******************************************************
0969:
0970: /**
0971: * Constructs a WeakPropertyChangeSupport object.
0972: *
0973: * @param sourceBean The bean to be given as the source for any events.
0974: */
0975: WeakPropertyChangeSupport(Object sourceBean) {
0976: super (sourceBean);
0977: }
0978:
0979: // Managing Property Change Listeners **********************************
0980:
0981: /** {@inheritDoc} */
0982: @Override
0983: public synchronized void addPropertyChangeListener(
0984: PropertyChangeListener listener) {
0985: if (listener == null)
0986: return;
0987: if (listener instanceof PropertyChangeListenerProxy) {
0988: PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listener;
0989: // Call two argument add method.
0990: addPropertyChangeListener(proxy.getPropertyName(),
0991: (PropertyChangeListener) proxy.getListener());
0992: } else {
0993: super
0994: .addPropertyChangeListener(new WeakPropertyChangeListener(
0995: listener));
0996: }
0997: }
0998:
0999: /** {@inheritDoc} */
1000: @Override
1001: public synchronized void addPropertyChangeListener(
1002: String propertyName, PropertyChangeListener listener) {
1003: if (listener == null)
1004: return;
1005: super .addPropertyChangeListener(propertyName,
1006: new WeakPropertyChangeListener(propertyName,
1007: listener));
1008: }
1009:
1010: /** {@inheritDoc} */
1011: @Override
1012: public synchronized void removePropertyChangeListener(
1013: PropertyChangeListener listener) {
1014: if (listener == null)
1015: return;
1016: if (listener instanceof PropertyChangeListenerProxy) {
1017: PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) listener;
1018: // Call two argument remove method.
1019: removePropertyChangeListener(proxy.getPropertyName(),
1020: (PropertyChangeListener) proxy.getListener());
1021: return;
1022: }
1023: PropertyChangeListener[] listeners = getPropertyChangeListeners();
1024: WeakPropertyChangeListener wpcl;
1025: for (int i = listeners.length - 1; i >= 0; i--) {
1026: if (listeners[i] instanceof PropertyChangeListenerProxy)
1027: continue;
1028: wpcl = (WeakPropertyChangeListener) listeners[i];
1029: if (wpcl.get() == listener) {
1030: // TODO: Should we call here the #clear() method of wpcl???
1031: super .removePropertyChangeListener(wpcl);
1032: break;
1033: }
1034: }
1035: }
1036:
1037: /** {@inheritDoc} */
1038: @Override
1039: public synchronized void removePropertyChangeListener(
1040: String propertyName, PropertyChangeListener listener) {
1041: if (listener == null)
1042: return;
1043: PropertyChangeListener[] listeners = getPropertyChangeListeners(propertyName);
1044: WeakPropertyChangeListener wpcl;
1045: for (int i = listeners.length - 1; i >= 0; i--) {
1046: wpcl = (WeakPropertyChangeListener) listeners[i];
1047: if (wpcl.get() == listener) {
1048: // TODO: Should we call here the #clear() method of wpcl???
1049: super .removePropertyChangeListener(propertyName,
1050: wpcl);
1051: break;
1052: }
1053: }
1054: }
1055:
1056: // Firing Events **********************************************************
1057:
1058: /**
1059: * Fires the specified PropertyChangeEvent to any registered listeners.
1060: *
1061: * @param evt The PropertyChangeEvent object.
1062: *
1063: * @see PropertyChangeSupport#firePropertyChange(PropertyChangeEvent)
1064: */
1065: @Override
1066: public void firePropertyChange(PropertyChangeEvent evt) {
1067: cleanUp();
1068: super .firePropertyChange(evt);
1069: }
1070:
1071: /**
1072: * Reports a bound property update to any registered listeners.
1073: *
1074: * @param propertyName The programmatic name of the property
1075: * that was changed.
1076: * @param oldValue The old value of the property.
1077: * @param newValue The new value of the property.
1078: *
1079: * @see PropertyChangeSupport#firePropertyChange(String, Object, Object)
1080: */
1081: @Override
1082: public void firePropertyChange(String propertyName,
1083: Object oldValue, Object newValue) {
1084: cleanUp();
1085: super .firePropertyChange(propertyName, oldValue, newValue);
1086: }
1087:
1088: static final ReferenceQueue<PropertyChangeListener> QUEUE = new ReferenceQueue<PropertyChangeListener>();
1089:
1090: private static void cleanUp() {
1091: WeakPropertyChangeListener wpcl;
1092: while ((wpcl = (WeakPropertyChangeListener) QUEUE.poll()) != null) {
1093: wpcl.removeListener();
1094: }
1095: }
1096:
1097: void removeWeakPropertyChangeListener(
1098: WeakPropertyChangeListener l) {
1099: if (l.propertyName == null) {
1100: super .removePropertyChangeListener(l);
1101: } else {
1102: super .removePropertyChangeListener(l.propertyName, l);
1103: }
1104: }
1105:
1106: /**
1107: * Wraps a PropertyChangeListener to make it weak.
1108: */
1109: private final class WeakPropertyChangeListener extends
1110: WeakReference<PropertyChangeListener> implements
1111: PropertyChangeListener {
1112:
1113: final String propertyName;
1114:
1115: private WeakPropertyChangeListener(
1116: PropertyChangeListener delegate) {
1117: this (null, delegate);
1118: }
1119:
1120: private WeakPropertyChangeListener(String propertyName,
1121: PropertyChangeListener delegate) {
1122: super (delegate, QUEUE);
1123: this .propertyName = propertyName;
1124: }
1125:
1126: /** {@inheritDoc} */
1127: public void propertyChange(PropertyChangeEvent evt) {
1128: PropertyChangeListener delegate = get();
1129: if (delegate != null) {
1130: delegate.propertyChange(evt);
1131: }
1132: }
1133:
1134: void removeListener() {
1135: removeWeakPropertyChangeListener(this);
1136: }
1137: }
1138: }
1139:
1140: }
|