com.jgoodies.binding.adapter.Bindings Maven / Gradle / Ivy
Show all versions of jgoodies-binding Show documentation
/*
* Copyright (c) 2002-2013 JGoodies Software GmbH. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Software GmbH nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jgoodies.binding.adapter;
import static com.jgoodies.common.base.Preconditions.checkArgument;
import static com.jgoodies.common.base.Preconditions.checkNotBlank;
import static com.jgoodies.common.base.Preconditions.checkNotNull;
import static com.jgoodies.common.internal.Messages.MUST_NOT_BE_BLANK;
import static com.jgoodies.common.internal.Messages.MUST_NOT_BE_NULL;
import java.awt.Color;
import java.awt.Component;
import java.awt.KeyboardFocusManager;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeListenerProxy;
import java.beans.PropertyChangeSupport;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPasswordField;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListDataListener;
import javax.swing.table.TableModel;
import javax.swing.text.JTextComponent;
import com.jgoodies.binding.beans.PropertyConnector;
import com.jgoodies.binding.internal.TableRowSorterListSelectionModel;
import com.jgoodies.binding.list.SelectionInList;
import com.jgoodies.binding.value.BufferedValueModel;
import com.jgoodies.binding.value.ComponentModel;
import com.jgoodies.binding.value.ComponentValueModel;
import com.jgoodies.binding.value.ValueModel;
/**
* Consists only of static methods that bind Swing components to ValueModels.
* This is one of two helper classes that assist you in establishing a binding:
* 1) this Bindings class binds components that have been created before;
* it wraps ValueModels with the adapters from package
* {@code com.jgoodies.binding.adapter}.
* 2) the BasicComponentFactory creates Swing components that are then
* bound using this Bindings class.
*
* If you have an existing factory that vends Swing components,
* you can use this Bindings class to bind them to ValueModels.
* If you don't have such a factory, you can use the BasicComponentFactory
* or a custom subclass to create and bind Swing components.
*
* The binding process for JCheckBox, JRadioButton, and other AbstractButtons
* retains the former enablement state. Before the new (adapting) model
* is set, the enablement is requested from the model, not the button.
* This enablement is set after the new model has been set.
*
* TODO: Consider adding binding methods for JProgressBar,
* JSlider, JSpinner, and JTabbedPane.
*
* TODO: Consider adding connection methods for pairs of bean properties.
* In addition to the PropertyConnector's {@code connect} method,
* this could add boolean operators such as: not, and, or, nor.
*
* @author Karsten Lentzsch
* @version $Revision: 1.41 $
*
* @see com.jgoodies.binding.value.ValueModel
* @see BasicComponentFactory
*/
public final class Bindings {
/**
* A JTextComponent client property key used to store and retrieve
* the BufferedValueModel associated with text components that
* commit on focus lost.
*
* @see #bind(JTextArea, ValueModel, boolean)
* @see #bind(JTextField, ValueModel, boolean)
* @see #flushImmediately()
* @see BufferedValueModel
*/
private static final String COMMIT_ON_FOCUS_LOST_MODEL_KEY =
"commitOnFocusListModel";
/**
* A JComponent client property key used to store
* and retrieve an associated ComponentValueModel.
*
* @see #addComponentPropertyHandler(JComponent, ValueModel)
* @see #removeComponentPropertyHandler(JComponent)
* @see ComponentValueModel
*/
private static final String COMPONENT_VALUE_MODEL_KEY =
"componentValueModel";
/**
* A JComponent client property key used to store
* and retrieve an associated ComponentPropertyHandler.
*
* @see #addComponentPropertyHandler(JComponent, ValueModel)
* @see #removeComponentPropertyHandler(JComponent)
*/
private static final String COMPONENT_PROPERTY_HANDLER_KEY =
"componentPropertyHandler";
/**
* Triggers a commit in the shared focus lost trigger
* if focus is lost permanently. Shared among all components
* that are configured to commit on focus lost.
*
* @see #createCommitOnFocusLostModel(ValueModel, Component)
*/
static final FocusLostHandler FOCUS_LOST_HANDLER = new FocusLostHandler();
/**
* Holds a weak trigger that is shared by BufferedValueModels
* that commit on permanent focus change.
*
* @see #createCommitOnFocusLostModel(ValueModel, Component)
*/
static final WeakTrigger FOCUS_LOST_TRIGGER = new WeakTrigger();
private Bindings() {
// Suppresses default constructor; prevents instantiation.
}
// Binding Methods ********************************************************
/**
* Binds a JToggleButton to the given ValueModel in check box style.
* The bound button is selected if and only if the model's value
* equals {@code Boolean.TRUE}. Retains the enablement state.
*
* The value model is converted to the required interface
* ToggleButtonModel using a ToggleButtonAdapter.
*
* @param toggleButton the toggle button to be bound
* @param valueModel the model that provides a Boolean value
*
* @throws NullPointerException if the checkBox or valueModel
* is {@code null}
*/
public static void bind(AbstractButton toggleButton, ValueModel valueModel) {
bind(toggleButton, valueModel, Boolean.TRUE, Boolean.FALSE);
}
/**
* Binds a JToggleButton to the given ValueModel in check box style.
* The bound button is selected if and only if the model's value
* equals {@code Boolean.TRUE}. Retains the enablement state.
*
* The value model is converted to the required interface
* ToggleButtonModel using a ToggleButtonAdapter.
*
* @param toggleButton the toggle button to be bound
* @param valueModel the model that provides a Boolean value
* @param selectedValue the model's value if the button is selected
* @param deselectedValue the model's value if the button is deselected
*
* @throws NullPointerException if the checkBox or valueModel
* is {@code null}
*/
public static void bind(AbstractButton toggleButton,
ValueModel valueModel, Object selectedValue, Object deselectedValue) {
ButtonModel oldModel = toggleButton.getModel();
String oldActionCommand = oldModel.getActionCommand();
boolean oldEnabled = oldModel.isEnabled();
int oldMnemonic = oldModel.getMnemonic();
int oldMnemonicIndex = toggleButton.getDisplayedMnemonicIndex();
ButtonModel newModel = new ToggleButtonAdapter(
valueModel, selectedValue, deselectedValue);
newModel.setActionCommand(oldActionCommand);
newModel.setEnabled(oldEnabled);
newModel.setMnemonic(oldMnemonic);
toggleButton.setModel(newModel);
toggleButton.setDisplayedMnemonicIndex(oldMnemonicIndex);
addComponentPropertyHandler(toggleButton, valueModel);
}
/**
* Binds a JToggleButton to the given ValueModel in radio button style.
* The bound button is selected if and only if the model's value
* equals the specified choice value. Retains the enablement state.
*
* The model is converted to the required interface
* ToggleButtonModel using a RadioButtonAdapter.
*
* @param toggleButton the button to be bound to the given model
* @param model the model that provides the current choice
* @param choice this button's value
*
* @throws NullPointerException if the valueModel is {@code null}
*/
public static void bind(AbstractButton toggleButton, ValueModel model, Object choice) {
ButtonModel oldModel = toggleButton.getModel();
String oldActionCommand = oldModel.getActionCommand();
boolean oldEnabled = oldModel.isEnabled();
int oldMnemonic = oldModel.getMnemonic();
int oldMnemonicIndex = toggleButton.getDisplayedMnemonicIndex();
ButtonModel newModel = new RadioButtonAdapter(model, choice);
newModel.setActionCommand(oldActionCommand);
newModel.setEnabled(oldEnabled);
newModel.setMnemonic(oldMnemonic);
toggleButton.setModel(newModel);
toggleButton.setDisplayedMnemonicIndex(oldMnemonicIndex);
addComponentPropertyHandler(toggleButton, model);
}
/**
* Binds a JColorChooser to the given Color-typed ValueModel.
* The ValueModel must be of type Color and must
* allow read-access to its value.
*
* Also, it is strongly recommended (though not required) that
* the underlying ValueModel provides only non-null values.
* This is so because the ColorSelectionModel behavior is undefined
* for {@code null} values and it may have unpredictable results.
* To avoid these problems, you may bind the ColorChooser with
* a default color using {@link #bind(JColorChooser, ValueModel, Color)}.
*
* Since the color chooser is considered a container, not a single
* component, it is not synchronized with the valueModel's
* {@link ComponentValueModel} properties - if any.
*
* Note: There's a bug in Java 1.4.2, Java 5 and Java 6
* that affects this binding. The BasicColorChooserUI doesn't listen
* to changes in the selection model, and so the preview panel won't
* update if the selected color changes. As a workaround you can use
* {@link BasicComponentFactory#createColorChooser(ValueModel)},
* or you could use a Look&Feel that fixes the bug mentioned above.
*
* @param colorChooser the color chooser to be bound
* @param valueModel the model that provides non-{@code null}
* Color values.
*
* @throws NullPointerException if the color chooser or value model
* is {@code null}
*
* @see #bind(JColorChooser, ValueModel, Color)
*
* @since 1.0.3
*/
public static void bind(JColorChooser colorChooser, ValueModel valueModel) {
colorChooser.setSelectionModel(new ColorSelectionAdapter(valueModel));
}
/**
* Binds a JColorChooser to the given Color-typed ValueModel.
* The ValueModel must be of type Color and must allow read-access
* to its value. The default color will be used if the valueModel
* returns {@code null}.
*
* Since the color chooser is considered a container, not a single
* component, it is not synchronized with the valueModel's
* {@link ComponentValueModel} properties - if any.
*
* Note: There's a bug in Java 1.4.2, Java 5 and Java 6
* that affects this binding. The BasicColorChooserUI doesn't listen
* to changes in the selection model, and so the preview panel won't
* update if the selected color changes. As a workaround you can use
* {@link BasicComponentFactory#createColorChooser(ValueModel)},
* or you could use a Look&Feel that fixes the bug mentioned above.
*
* @param colorChooser the color chooser to be bound
* @param valueModel the model that provides non-{@code null}
* Color values.
* @param defaultColor the color used if the valueModel returns null
*
* @throws NullPointerException if the color chooser, value model,
* or default color is {@code null}
*
* @since 1.1
*/
public static void bind(JColorChooser colorChooser, ValueModel valueModel, Color defaultColor) {
checkNotNull(defaultColor, "The default color must not be null.");
colorChooser.setSelectionModel(new ColorSelectionAdapter(valueModel, defaultColor));
}
/**
* Binds a non-editable JComboBox to the given ComboBoxModel.
*
* @param comboBox the combo box to be bound
* @param model provides the data and selection
*
* @throws NullPointerException if the combo box or model
* is {@code null}
*
* @see ComboBoxAdapter
*
* @since 2.7
*/
public static void bind(JComboBox comboBox, ComboBoxModel model) {
checkNotNull(model, "The combo box model must not be null.");
comboBox.setModel(model);
}
/**
* Binds a JComboBox to the given ComboBoxModel using the specified
* text to represent {@code null}.
*
* @param comboBox the combo box to be bound
* @param model provides the data and selection
* @param nullText the text used to represent {@code null}
*
* @throws NullPointerException if the combo box or model
* is {@code null}
*
* @since 2.7
*/
public static void bind(JComboBox comboBox, ComboBoxModel model, String nullText) {
checkNotNull(model, "The combo box model must not be null.");
final NullElement nullElement = new NullElement(nullText);
comboBox.setModel(
new NullElementComboBoxModel(model, nullElement));
comboBox.setRenderer(
new NullElementComboBoxRenderer(comboBox.getRenderer(), nullElement));
}
/**
* Binds a non-editable JComboBox to the given SelectionInList using
* the SelectionInList's ListModel as list data provider and the
* SelectionInList's selection index holder for the combo box model's
* selected item.
*
* There are a couple of other possibilities to bind a JComboBox.
* See the constructors and the class comment of the
* {@link ComboBoxAdapter}.
*
* Since version 2.0 the combo's enabled and visible
* state is synchronized with the selectionInList's selection holder,
* if it's a {@link ComponentValueModel}.
*
* @param comboBox the combo box to be bound
* @param selectionInList provides the list and selection;
* if the selection holder is a ComponentValueModel, it is synchronized
* with the comboBox properties visible and enabled
* @param the type of the combo box items
*
* @throws NullPointerException if the combo box or the selectionInList
* is {@code null}
*
* @see ComboBoxAdapter
*
* @since 1.0.1
*/
public static void bind(JComboBox comboBox, SelectionInList selectionInList) {
checkNotNull(selectionInList, "The SelectionInList must not be null.");
comboBox.setModel(new ComboBoxAdapter(selectionInList));
addComponentPropertyHandler(comboBox, selectionInList.getSelectionHolder());
}
/**
* Binds a non-editable JComboBox to the given SelectionInList using
* the SelectionInList's ListModel as list data provider and the
* SelectionInList's selection index holder for the combo box model's
* selected item.
*
* There are a couple of other possibilities to bind a JComboBox.
* See the constructors and the class comment of the
* {@link ComboBoxAdapter}.
*
* Since version 2.0 the combo's enabled and visible
* state is synchronized with the selectionInList's selection holder,
* if it's a {@link ComponentValueModel}.
*
* @param comboBox the combo box to be bound
* @param selectionInList provides the list and selection;
* if the selection holder is a ComponentValueModel, it is synchronized
* with the comboBox properties visible and enabled
* @param the type of the combo box items
* @param nullText the text used to represent {@code null}
*
* @throws NullPointerException if {@code combo box},
* {@code selectionInList}, or {@code nullText} is {@code null}
*
* @see ComboBoxAdapter
*
* @since 2.3
*/
public static void bind(
JComboBox comboBox,
SelectionInList selectionInList,
String nullText) {
checkNotNull(nullText, "The null text must not be null.");
bind(comboBox, selectionInList);
final NullElement nullElement = new NullElement(nullText);
comboBox.setModel(
new NullElementComboBoxModel(
comboBox.getModel(), nullElement));
comboBox.setRenderer(
new NullElementComboBoxRenderer(
comboBox.getRenderer(), nullElement));
}
/**
* Binds the given JFormattedTextField to the specified ValueModel.
* Synchronized the ValueModel's value with the text field's value
* by means of a PropertyConnector.
*
* @param textField the JFormattedTextField that is to be bound
* @param valueModel the model that provides the value
*
* @throws NullPointerException if the text field or valueModel
* is {@code null}
*/
public static void bind(JFormattedTextField textField, ValueModel valueModel) {
bind(textField, "value", valueModel);
}
/**
* Binds the given JLabel to the specified ValueModel.
*
* @param label a label that shall be bound to the given value model
* @param valueModel the model that provides the value
*
* @throws NullPointerException if the label or valueModel is {@code null}
*/
public static void bind(JLabel label, ValueModel valueModel) {
bind(label, "text", valueModel);
}
/**
* Binds a JList to the given SelectionInList using the SelectionInList's
* ListModel as list data provider and the SelectionInList's selection
* index holder for the selection model.
*
* Since version 2.0 the list's enabled and visible
* state is synchronized with the selectionInList's selection holder,
* if it's a {@link ComponentValueModel}.
*
* @param list the list to be bound
* @param selectionInList provides the list and selection;
* if the selection holder is a ComponentValueModel, it is synchronized
* with the list properties visible and enabled
* @param the type of the list items
*
* @throws NullPointerException if the list or the selectionInList
* is {@code null}
*/
public static void bind(JList list, SelectionInList selectionInList) {
checkNotNull(selectionInList, "The SelectionInList must not be null.");
list.setModel(selectionInList);
list.setSelectionModel(
new SingleListSelectionAdapter(
selectionInList.getSelectionIndexHolder()));
addComponentPropertyHandler(list, selectionInList.getSelectionHolder());
}
/**
* Sets the given SelectionInList as the table's row-data provider,
* and binds the SelectionInList's selection index to the table's selection.
* As a precondition, the table's TableModel must implement
* {@link ListModelBindable}.
*
* Since version 2.9 this binding supports JTable sorting.
* If a JTable has a RowSorter, this method sets a
* {@link TableRowSorterListSelectionModel} as {@code table}'s
* selection model that is then synchronized with the selection index
* holder of the given {@code selectionInList}.
*
* @param table the table to be bound
* @param selectionInList a ListModel plus selection index
*
* @throws NullPointerException if {@code selectionInList} is {@code null}
* @throws IllegalArgumentException if the table's model
* does not implement ListModelBindable
*
* @since 2.2
*/
public static void bind(
JTable table,
SelectionInList selectionInList) {
ListModel listModel = checkNotNull(selectionInList, "The SelectionInList must not be null.");
ListSelectionModel listSelectionModel =
new SingleListSelectionAdapter(selectionInList.getSelectionIndexHolder());
bind(table, listModel, listSelectionModel);
}
/**
* Sets the given ListModel as the table's underlying row-data provider,
* and sets the given ListSelectionModel in the table. As a precondition,
* the table's TableModel must implement {@link ListModelBindable}.
*
* Since version 2.9 this binding supports JTable sorting.
* If a JTable has a RowSorter, this method sets a
* {@link TableRowSorterListSelectionModel} as {@code table}'s
* selection model that is then synchronized with the given
* {@code listSelectionModel}.
*
* @param table the table to be bound
* @param listModel provides the table model's row data
* @param listSelectionModel manages the list selection
*
* @throws IllegalArgumentException if the table's model
* does not implement ListModelBindable
* @throws NullPointerException if {@code listSelectionModel} is {@code null}
*
* @since 2.2
*/
public static void bind(
JTable table,
ListModel listModel, ListSelectionModel listSelectionModel) {
TableModel tableModel = table.getModel();
checkNotNull(listSelectionModel, "The ListSelectionModel must not be null.");
checkArgument(tableModel instanceof ListModelBindable, "The table's TableModel must implement ListModelBindable to be bound.");
((ListModelBindable) tableModel).setListModel(listModel);
if (table.getRowSorter() != null) {
table.setSelectionModel(new TableRowSorterListSelectionModel(listSelectionModel, table));
} else {
table.setSelectionModel(listSelectionModel);
}
}
/**
* Binds a text area to the given ValueModel.
* The model is updated on every character typed.
*
* TODO: Consider changing the semantics to commit on focus lost.
* This would be consistent with the text component vending factory methods
* in the BasicComponentFactory that have no boolean parameter.
*
* @param textArea the text area to be bound to the value model
* @param valueModel the model that provides the text value
*
* @throws NullPointerException if the text component or valueModel
* is {@code null}
*/
public static void bind(
JTextArea textArea,
ValueModel valueModel) {
bind(textArea, valueModel, false);
}
/**
* Binds a text area to the given ValueModel.
* The model can be updated on focus lost or on every character typed.
* The DocumentAdapter used in this binding doesn't filter newlines.
*
* @param textArea the text area to be bound to the value model
* @param valueModel the model that provides the text value
* @param commitOnFocusLost true to commit text changes on focus lost,
* false to commit text changes on every character typed
*
* @throws NullPointerException if the text component or valueModel
* is {@code null}
*/
public static void bind(
JTextArea textArea,
ValueModel valueModel,
boolean commitOnFocusLost) {
checkNotNull(valueModel, "The value model must not be null.");
ValueModel textModel;
if (commitOnFocusLost) {
textModel = createCommitOnFocusLostModel(valueModel, textArea);
textArea.putClientProperty(COMMIT_ON_FOCUS_LOST_MODEL_KEY, textModel);
} else {
textModel = valueModel;
}
TextComponentConnector connector = new TextComponentConnector(textModel, textArea);
connector.updateTextComponent();
addComponentPropertyHandler(textArea, valueModel);
}
/**
* Bind a text fields or password field to the given ValueModel.
* The model is updated on every character typed.
*
* Security Note: If you use this method to bind a
* JPasswordField, the field's password will be requested as Strings
* from the field and will be held as String by the given ValueModel.
* These password String could potentially be observed in a security fraud.
* For stronger security it is recommended to request a character array
* from the JPasswordField and clear the array after use by setting
* each character to zero. Method {@link JPasswordField#getPassword()}
* return's the field's password as a character array.
*
* TODO: Consider changing the semantics to commit on focus lost.
* This would be consistent with the text component vending factory methods
* in the BasicComponentFactory that have no boolean parameter.
*
* @param textField the text field to be bound to the value model
* @param valueModel the model that provides the text value
*
* @throws NullPointerException if the text component or valueModel
* is {@code null}
*
* @see JPasswordField#getPassword()
*/
public static void bind(
JTextField textField,
ValueModel valueModel) {
bind(textField, valueModel, false);
}
/**
* Binds a text field or password field to the given ValueModel.
* The model can be updated on focus lost or on every character typed.
*
* Security Note: If you use this method to bind a
* JPasswordField, the field's password will be requested as Strings
* from the field and will be held as String by the given ValueModel.
* These password String could potentially be observed in a security fraud.
* For stronger security it is recommended to request a character array
* from the JPasswordField and clear the array after use by setting
* each character to zero. Method {@link JPasswordField#getPassword()}
* return's the field's password as a character array.
*
* @param textField the text field to be bound to the value model
* @param valueModel the model that provides the text value
* @param commitOnFocusLost true to commit text changes on focus lost,
* false to commit text changes on every character typed
*
* @throws NullPointerException if the text component or valueModel
* is {@code null}
*
* @see JPasswordField#getPassword()
*/
public static void bind(
JTextField textField,
ValueModel valueModel,
boolean commitOnFocusLost) {
checkNotNull(valueModel, "The value model must not be null.");
ValueModel textModel;
if (commitOnFocusLost) {
textModel = createCommitOnFocusLostModel(valueModel, textField);
textField.putClientProperty(COMMIT_ON_FOCUS_LOST_MODEL_KEY, textModel);
} else {
textModel = valueModel;
}
TextComponentConnector connector = new TextComponentConnector(textModel, textField);
connector.updateTextComponent();
addComponentPropertyHandler(textField, valueModel);
}
/**
* Binds the specified property of the given JComponent to the specified
* ValueModel. Synchronizes the ValueModel's value with the component's
* property by means of a PropertyConnector.
*
* @param component the JComponent that is to be bound
* @param propertyName the name of the property to be bound
* @param valueModel the model that provides the value
*
* @throws NullPointerException if the component or value model
* or property name is {@code null}
* @throws IllegalArgumentException if {@code propertyName} is blank or whitespace
*
* @since 1.2
*/
public static void bind(JComponent component, String propertyName, ValueModel valueModel) {
checkNotNull(component, MUST_NOT_BE_NULL, "component");
checkNotNull(valueModel, MUST_NOT_BE_NULL, "value model");
checkNotBlank(propertyName, MUST_NOT_BE_BLANK, "property name");
PropertyConnector.connectAndUpdate(valueModel, component, propertyName);
addComponentPropertyHandler(component, valueModel);
}
// Updating Component State on ComponentValueModel Changes ****************
/**
* If the given model is a ComponentValueModel, a component property handler
* is registered with this model. This handler updates the component state
* if the ComponentValueModel indicates a change in one of its properties,
* for example: visible, enabled, and editable.
*
* Also the ComponentValueModel and the component handler are stored
* as client properties with the component. This way they can be removed
* later using {@code #removeComponentPropertyHandler}.
*
* @param component the component where the handler is registered
* @param valueModel the model to observe
*
* @see #removeComponentPropertyHandler(JComponent)
* @see ComponentValueModel
*
* @since 1.1
*/
public static void addComponentPropertyHandler(JComponent component, ValueModel valueModel) {
if (!(valueModel instanceof ComponentValueModel)) {
return;
}
ComponentValueModel cvm = (ComponentValueModel) valueModel;
PropertyChangeListener componentHandler = new ComponentPropertyHandler(component);
cvm.addPropertyChangeListener(componentHandler);
component.putClientProperty(COMPONENT_VALUE_MODEL_KEY, cvm);
component.putClientProperty(COMPONENT_PROPERTY_HANDLER_KEY, componentHandler);
component.setEnabled(cvm.isEnabled());
component.setVisible(cvm.isVisible());
if (component instanceof JTextComponent) {
((JTextComponent) component).setEditable(cvm.isEditable());
}
}
/**
* If the given component holds a ComponentValueModel and
* a ComponentPropertyHandler in its client properties,
* the handler is removed as listener from the model,
* and the model and handler are removed from the client properties.
*
* @param component the component to remove the listener from
*
* @see #addComponentPropertyHandler(JComponent, ValueModel)
* @see ComponentValueModel
*
* @since 1.1
*/
public static void removeComponentPropertyHandler(JComponent component) {
ComponentValueModel componentValueModel = (ComponentValueModel) component.getClientProperty(
COMPONENT_VALUE_MODEL_KEY);
PropertyChangeListener componentHandler = (PropertyChangeListener) component.getClientProperty(
COMPONENT_PROPERTY_HANDLER_KEY);
if (componentValueModel != null && componentHandler != null) {
componentValueModel.removePropertyChangeListener(componentHandler);
component.putClientProperty(COMPONENT_VALUE_MODEL_KEY, null);
component.putClientProperty(COMPONENT_PROPERTY_HANDLER_KEY, null);
} else if (componentValueModel == null && componentHandler == null) {
return;
} else if (componentValueModel != null) {
throw new IllegalStateException(
"The component has a ComponentValueModel stored, "
+ "but lacks the ComponentPropertyHandler.");
} else {
throw new IllegalStateException(
"The component has a ComponentPropertyHandler stored, "
+ "but lacks the ComponentValueModel.");
}
}
// Misc *******************************************************************
/**
* Commits a pending edit - if any. Useful to ensure that edited values
* in bound text components that commit on focus-lost are committed
* before an operation is performed that uses the value to be committed
* after a focus lost.
*
* For example, before you save a form, a value that has been edited
* shall be committed, so the validation can check whether the save
* is allowed or not.
*
* @since 1.2
*/
public static void commitImmediately() {
FOCUS_LOST_TRIGGER.triggerCommit();
}
/**
* Flushes a pending edit in the focused text component - if any.
* Useful to revert edited values in bound text components that
* commit on focus-lost. This operation can be performed on an escape
* key event like the Cancel action in the JFormattedTextField.
*
* Returns whether an edit has been reset. Useful to decide whether
* a key event shall be consumed or not.
*
* @return {@code true} if a pending edit has been reset,
* {@code false} if the focused component isn't buffering or
* doesn't buffer at all
*
* @see #isFocusOwnerBuffering()
*
* @since 2.0.1
*/
public static boolean flushImmediately() {
boolean buffering = isFocusOwnerBuffering();
if (buffering) {
FOCUS_LOST_TRIGGER.triggerFlush();
}
return buffering;
}
/**
* Checks and answers whether the focus owner is a component
* that buffers a pending edit. Useful to enable or disable
* a text component Action that resets the edited value.
*
* See also the JFormattedTextField's internal {@code CancelAction}.
*
* @return {@code true} if the focus owner is a JTextComponent
* that commits on focus-lost and is buffering
*
* @see #flushImmediately()
*
* @since 2.0.1
*/
public static boolean isFocusOwnerBuffering() {
Component focusOwner =
KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
if (!(focusOwner instanceof JComponent)) {
return false;
}
Object value = ((JComponent) focusOwner).getClientProperty(
COMMIT_ON_FOCUS_LOST_MODEL_KEY);
if (!(value instanceof BufferedValueModel)) {
return false;
}
BufferedValueModel commitOnFocusLostModel =
(BufferedValueModel) value;
return commitOnFocusLostModel.isBuffering();
}
// Helper Code ************************************************************
/**
* Creates and returns a ValueModel that commits its value
* if the given component looses the focus permanently.
* It wraps the underlying ValueModel with a BufferedValueModel
* and delays the value commit until this class' shared FOCUS_LOST_TRIGGER
* commits. This happens, because this class' shared FOCUS_LOST_HANDLER
* is registered with the specified component.
*
* @param valueModel the model that provides the value
* @param component the component that looses the focus
* @return a buffering ValueModel that commits on focus lost
*
* @throws NullPointerException if the value model is {@code null}
*/
private static ValueModel createCommitOnFocusLostModel(
ValueModel valueModel,
Component component) {
checkNotNull(valueModel, "The value model must not be null.");
ValueModel model = new BufferedValueModel(valueModel, FOCUS_LOST_TRIGGER);
component.addFocusListener(FOCUS_LOST_HANDLER);
return model;
}
// Helper Classes *********************************************************
/**
* Triggers a commit event on permanent focus lost.
*/
private static final class FocusLostHandler extends FocusAdapter {
/**
* Triggers a commit event if the focus lost is permanent.
*
* @param evt the focus lost event
*/
@Override
public void focusLost(FocusEvent evt) {
if (!evt.isTemporary()) {
FOCUS_LOST_TRIGGER.triggerCommit();
}
}
}
/**
* Listens to property changes in a ComponentValueModel and
* updates the associated component state.
*
* @see ComponentValueModel
*/
private static final class ComponentPropertyHandler implements PropertyChangeListener {
private final JComponent component;
private ComponentPropertyHandler(JComponent component) {
this.component = component;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
ComponentValueModel model = (ComponentValueModel) evt.getSource();
if (ComponentModel.PROPERTY_ENABLED.equals(propertyName)) {
component.setEnabled(model.isEnabled());
} else if (ComponentModel.PROPERTY_VISIBLE.equals(propertyName)) {
component.setVisible(model.isVisible());
} else if (ComponentModel.PROPERTY_EDITABLE.equals(propertyName)) {
if (component instanceof JTextComponent) {
((JTextComponent) component).setEditable(model.isEditable());
}
}
}
}
// Helper Classes for ComboBox Null Elements ******************************
/**
* An Object that represents itself using a given string.
* Used by the {@link NullElementComboBoxModel} as element
* that represents the {@code null} value.
* And used by the {@link NullElementComboBoxRenderer}
* to identify the special element.
*/
private static final class NullElement {
private final String text;
// Instance Creation **************************************************
NullElement(String text) {
this.text = checkNotNull(text, "The text must not be null.");
}
// Overriding Object Behavior *****************************************
@Override
public String toString() {
return text;
}
}
/**
* Wraps a given ComboBoxModel and adds an special element that is mapped
* to {@code null}.
*/
private static final class NullElementComboBoxModel implements ComboBoxModel {
/**
* Holds the wrapped model that is used to delegate requests
* related to all elements except the special null element.
*/
private final ComboBoxModel delegate;
private final Object nullElement;
// Instance Creation --------------------------------------------------
NullElementComboBoxModel(ComboBoxModel delegate, Object nullElement) {
this.delegate = checkNotNull(delegate, "The wrapped ComboBoxModel must not be null.");
this.nullElement = checkNotNull(nullElement, "The null element must not be null.");
}
// ComboBoxModel Implementation ---------------------------------------
@Override
public int getSize() {
return delegate.getSize() + 1;
}
@Override
public Object getElementAt(int index) {
return index == 0 ? nullElement : delegate.getElementAt(index - 1);
}
@Override
public Object getSelectedItem() {
Object selected = delegate.getSelectedItem();
return selected == null ? nullElement : selected;
}
@Override
public void setSelectedItem(Object anItem) {
delegate.setSelectedItem(anItem != nullElement ? anItem : null);
}
@Override
public void addListDataListener(ListDataListener l) {
delegate.addListDataListener(l);
}
@Override
public void removeListDataListener(ListDataListener l) {
delegate.removeListDataListener(l);
}
}
/**
* Wraps a given ListCellRenderer and adds a special rendering behavior
* for the {@link NullElementComboBoxModel}'s null element.
*/
private static final class NullElementComboBoxRenderer extends DefaultListCellRenderer {
/**
* Holds the wrapped renderer that is used to delegate requests
* related to all elements except the special null element.
*/
private final ListCellRenderer delegate;
private final NullElement nullElement;
// Instance Creation --------------------------------------------------
NullElementComboBoxRenderer(
ListCellRenderer delegate,
NullElement nullElement) {
this.delegate = checkNotNull(delegate, "The wrapped ListCellRenderer must not be null.");
this.nullElement = checkNotNull(nullElement, "The null element must not be null.");
}
// Overriding Superclass Behavior -------------------------------------
@Override
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
return value == nullElement
? super.getListCellRendererComponent(list, nullElement, index, isSelected, cellHasFocus)
: delegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
// Helper Code for a Weak Trigger *****************************************
/**
* Unlike the Trigger class, this implementation uses WeakReferences
* to store value change listeners.
*/
private static final class WeakTrigger implements ValueModel {
private final transient WeakPropertyChangeSupport changeSupport;
private Boolean value;
// Instance Creation ******************************************************
/**
* Constructs a WeakTrigger set to neutral.
*/
WeakTrigger() {
value = null;
changeSupport = new WeakPropertyChangeSupport(this);
}
// ValueModel Implementation **********************************************
/**
* Returns a Boolean that indicates the current trigger state.
*
* @return a Boolean that indicates the current trigger state
*/
@Override
public Object getValue() {
return value;
}
/**
* Sets a new Boolean value and rejects all non-Boolean values.
* Fires no change event if the new value is equal to the
* previously set value.
*
* This method is not intended to be used by API users.
* Instead you should trigger commit and flush events by invoking
* {@code #triggerCommit} or {@code #triggerFlush}.
*
* @param newValue the Boolean value to be set
* @throws IllegalArgumentException if the newValue is not a Boolean
*/
@Override
public void setValue(Object newValue) {
checkArgument(newValue == null || newValue instanceof Boolean,
"Trigger values must be of type Boolean.");
Object oldValue = value;
value = (Boolean) newValue;
fireValueChange(oldValue, newValue);
}
// Change Management ****************************************************
/**
* Registers the given PropertyChangeListener with this model.
* The listener will be notified if the value has changed.
*
* The PropertyChangeEvents delivered to the listener have the name
* set to "value". In other words, the listeners won't get notified
* when a PropertyChangeEvent is fired that has a null object as
* the name to indicate an arbitrary set of the event source's
* properties have changed.
*
* In the rare case, where you want to notify a PropertyChangeListener
* even with PropertyChangeEvents that have no property name set,
* you can register the listener with #addPropertyChangeListener,
* not #addValueChangeListener.
*
* @param listener the listener to add
*
* @see ValueModel
*/
@Override
public void addValueChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
changeSupport.addPropertyChangeListener("value", listener);
}
/**
* Removes the given PropertyChangeListener from the model.
*
* @param listener the listener to remove
*/
@Override
public void removeValueChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
changeSupport.removePropertyChangeListener("value", listener);
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method.
*
* @param oldValue the value before the change
* @param newValue the value after the change
*
* @see java.beans.PropertyChangeSupport
*/
private void fireValueChange(Object oldValue, Object newValue) {
changeSupport.firePropertyChange("value", oldValue, newValue);
}
// Triggering *************************************************************
/**
* Triggers a commit event in models that share this Trigger.
* Sets the value to {@code Boolean.TRUE} and ensures that listeners
* are notified about a value change to this new value. If necessary
* the value is temporarily set to {@code null}. This way it minimizes
* the number of PropertyChangeEvents fired by this Trigger.
*/
void triggerCommit() {
if (Boolean.TRUE.equals(getValue())) {
setValue(null);
}
setValue(Boolean.TRUE);
}
/**
* Triggers a flush event in models that share this Trigger.
* Sets the value to {@code Boolean.FALSE} and ensures that listeners
* are notified about a value change to this new value. If necessary
* the value is temporarily set to {@code null}. This way it minimizes
* the number of PropertyChangeEvents fired by this Trigger.
*/
void triggerFlush() {
if (Boolean.FALSE.equals(getValue())) {
setValue(null);
}
setValue(Boolean.FALSE);
}
}
/**
* Differs from its superclass {@link PropertyChangeSupport} in that it
* uses WeakReferences for registering listeners. It wraps registered
* PropertyChangeListeners with instances of WeakPropertyChangeListener
* and cleans up a list of stale references when firing an event.
*
* TODO: Merge this WeakPropertyChangeSupport with the
* ExtendedPropertyChangeSupport.
*/
private static final class WeakPropertyChangeSupport extends PropertyChangeSupport {
// Instance Creation ***************************************************
/**
* Constructs a WeakPropertyChangeSupport object.
*
* @param sourceBean The bean to be given as the source for any events.
*/
WeakPropertyChangeSupport(
Object sourceBean) {
super(sourceBean);
}
// Managing Property Change Listeners **********************************
@Override
public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
if (listener instanceof PropertyChangeListenerProxy) {
PropertyChangeListenerProxy proxy =
(PropertyChangeListenerProxy) listener;
// Call two argument add method.
addPropertyChangeListener(proxy.getPropertyName(),
(PropertyChangeListener) proxy.getListener());
} else {
super.addPropertyChangeListener(new WeakPropertyChangeListener(listener));
}
}
@Override
public synchronized void addPropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
if (listener == null) {
return;
}
super.addPropertyChangeListener(propertyName,
new WeakPropertyChangeListener(propertyName, listener));
}
@Override
public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
if (listener instanceof PropertyChangeListenerProxy) {
PropertyChangeListenerProxy proxy =
(PropertyChangeListenerProxy) listener;
// Call two argument remove method.
removePropertyChangeListener(proxy.getPropertyName(),
(PropertyChangeListener) proxy.getListener());
return;
}
PropertyChangeListener[] listeners = getPropertyChangeListeners();
WeakPropertyChangeListener wpcl;
for (int i = listeners.length - 1; i >= 0; i--) {
if (listeners[i] instanceof PropertyChangeListenerProxy) {
continue;
}
wpcl = (WeakPropertyChangeListener) listeners[i];
if (wpcl.get() == listener) {
// TODO: Should we call here the #clear() method of wpcl???
super.removePropertyChangeListener(wpcl);
break;
}
}
}
@Override
public synchronized void removePropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
if (listener == null) {
return;
}
PropertyChangeListener[] listeners = getPropertyChangeListeners(propertyName);
WeakPropertyChangeListener wpcl;
for (int i = listeners.length - 1; i >= 0; i--) {
wpcl = (WeakPropertyChangeListener) listeners[i];
if (wpcl.get() == listener) {
// TODO: Should we call here the #clear() method of wpcl???
super.removePropertyChangeListener(propertyName, wpcl);
break;
}
}
}
// Firing Events **********************************************************
/**
* Fires the specified PropertyChangeEvent to any registered listeners.
*
* @param evt The PropertyChangeEvent object.
*
* @see PropertyChangeSupport#firePropertyChange(PropertyChangeEvent)
*/
@Override
public void firePropertyChange(PropertyChangeEvent evt) {
cleanUp();
super.firePropertyChange(evt);
}
/**
* Reports a bound property update to any registered listeners.
*
* @param propertyName The programmatic name of the property
* that was changed.
* @param oldValue The old value of the property.
* @param newValue The new value of the property.
*
* @see PropertyChangeSupport#firePropertyChange(String, Object, Object)
*/
@Override
public void firePropertyChange(
String propertyName,
Object oldValue,
Object newValue) {
cleanUp();
super.firePropertyChange(propertyName, oldValue, newValue);
}
static final ReferenceQueue QUEUE =
new ReferenceQueue();
private static void cleanUp() {
WeakPropertyChangeListener wpcl;
while ((wpcl = (WeakPropertyChangeListener) QUEUE.poll()) != null) {
wpcl.removeListener();
}
}
void removeWeakPropertyChangeListener(WeakPropertyChangeListener l) {
if (l.propertyName == null) {
super.removePropertyChangeListener(l);
} else {
super.removePropertyChangeListener(l.propertyName, l);
}
}
/**
* Wraps a PropertyChangeListener to make it weak.
*/
private final class WeakPropertyChangeListener extends WeakReference
implements PropertyChangeListener {
final String propertyName;
private WeakPropertyChangeListener(PropertyChangeListener delegate) {
this(null, delegate);
}
private WeakPropertyChangeListener(String propertyName,
PropertyChangeListener delegate) {
super(delegate, QUEUE);
this.propertyName = propertyName;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
PropertyChangeListener delegate = get();
if (delegate != null) {
delegate.propertyChange(evt);
}
}
void removeListener() {
removeWeakPropertyChangeListener(this);
}
}
}
}