/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package com.bc.ceres.swing.binding; import com.bc.ceres.binding.Property; import com.bc.ceres.binding.PropertyContainer; import com.bc.ceres.binding.PropertyDescriptor; import com.bc.ceres.binding.PropertySet; import com.bc.ceres.core.Assert; import com.bc.ceres.swing.binding.internal.*; import javax.swing.*; import javax.swing.text.JTextComponent; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * A context used to bind Swing components to properties in a value container. * * @author Norman Fomferra * @version $Revision$ $Date$ * @since Ceres 0.6 */ public class BindingContext { private final PropertySet propertySet; private Map<String, BindingImpl> bindings; private Map<String, EnablementImpl> enablements; private ArrayList<BindingProblemListener> bindingProblemListeners; /** * Constructor. Uses an empty, default property set and a * default problem handler which will display an error dialog box on any binding errors. */ public BindingContext() { this(new PropertyContainer()); } /** * Constructor. Uses a default problem handler which will display an error dialog box on any binding errors. * * @param propertySet The property set. */ public BindingContext(PropertySet propertySet) { this(propertySet, new VerbousProblemHandler()); } /** * Constructor. * * @param propertySet The property set. * @param problemHandler A problem handler, or {@code null}. */ public BindingContext(PropertySet propertySet, BindingProblemListener problemHandler) { this.propertySet = propertySet; this.bindings = new HashMap<String, BindingImpl>(17); this.enablements = new HashMap<String, EnablementImpl>(11); if (problemHandler != null) { addProblemListener(problemHandler); } } /** * @return The property set. */ public PropertySet getPropertySet() { return propertySet; } /** * @return {@code true} if this context has problems. * @since Ceres 0.10 */ public boolean hasProblems() { for (Map.Entry<String, BindingImpl> entry : bindings.entrySet()) { if (entry.getValue().getProblem() != null) { return true; } } return false; } /** * @return The array of problems this context might have. * @since Ceres 0.10 */ public BindingProblem[] getProblems() { ArrayList<BindingProblem> list = new ArrayList<BindingProblem>(); for (Map.Entry<String, BindingImpl> entry : bindings.entrySet()) { final BindingProblem problem = entry.getValue().getProblem(); if (problem != null) { list.add(problem); } } return list.toArray(new BindingProblem[list.size()]); } /** * Adds a problem listener to this context. * * @param listener The listener. * @since Ceres 0.10 */ public void addProblemListener(BindingProblemListener listener) { Assert.notNull(listener, "listener"); if (bindingProblemListeners == null) { bindingProblemListeners = new ArrayList<BindingProblemListener>(); } bindingProblemListeners.add(listener); } /** * Removes a problem listener from this context. * * @param listener The listener. * @since Ceres 0.10 */ public void removeProblemListener(BindingProblemListener listener) { Assert.notNull(listener, "listener"); if (bindingProblemListeners != null) { bindingProblemListeners.remove(listener); } } /** * @return The array of problem listeners. * @since Ceres 0.10 */ public BindingProblemListener[] getProblemListeners() { return bindingProblemListeners != null ? bindingProblemListeners.toArray(new BindingProblemListener[bindingProblemListeners.size()]) : new BindingProblemListener[0]; } /** * Adjusts all associated GUI components so that they reflect the * values of the associated value container. * <p/> * * @see ComponentAdapter#adjustComponents() */ public void adjustComponents() { for (Map.Entry<String, BindingImpl> entry : bindings.entrySet()) { entry.getValue().adjustComponents(); } } /** * Gets the binding for the given property name. * * @param propertyName The property name. * @return The binding, or {@code null} if no such exists. */ public Binding getBinding(String propertyName) { Assert.notNull(propertyName, "propertyName"); return bindings.get(propertyName); } /** * Binds the property given by its name to the given component adapter. * <p/> * The method creates a new binding, adds it to this context and calls the follwing methods on * the given component adapter: * <ol> * <li>{@link ComponentAdapter#setBinding(Binding) componentAdapter.setBinding(binding)}</li> * <li>{@link ComponentAdapter#bindComponents() componentAdapter.bindComponents()}</li> * <li>{@link ComponentAdapter#adjustComponents() componentAdapter.adjustComponents(}</li> * </ol> * * @param propertyName The property name. * @param componentAdapter The component adapter. * @return The resulting binding. * @see #unbind(Binding) */ public Binding bind(String propertyName, ComponentAdapter componentAdapter) { Assert.notNull(propertyName, "propertyName"); Assert.notNull(componentAdapter, "componentAdapter"); BindingImpl binding = new BindingImpl(this, propertyName, componentAdapter); addBinding(binding); componentAdapter.setBinding(binding); componentAdapter.bindComponents(); binding.bindProperty(); binding.adjustComponents(); configureComponents(binding); return binding; } /** * Cancels the given binding by calling removing it from this context and finally calling * <ol> * <li>{@link ComponentAdapter#unbindComponents() componentAdapter.unbindComponents()}</li> * <li>{@link ComponentAdapter#setBinding(Binding) componentAdapter.setBinding(null)}</li> * </ol> * * @param binding The binding. * @see #bind(String, ComponentAdapter) */ public void unbind(Binding binding) { Assert.notNull(binding, "binding"); removeBinding(binding.getPropertyName()); if (binding instanceof BindingImpl) { ((BindingImpl) binding).unbindProperty(); } binding.getComponentAdapter().unbindComponents(); binding.getComponentAdapter().setBinding(null); } /** * Binds a property in the value container to a Swing {@code JTextComponent} component. * * @param propertyName The property name. * @param component The Swing component. * @return The resulting binding. */ public Binding bind(final String propertyName, final JTextComponent component) { return bind(propertyName, new TextComponentAdapter(component)); } /** * Binds a property in the value container to a Swing {@code JTextField} component. * * @param propertyName The property name. * @param component The Swing component. * @return The resulting binding. */ public Binding bind(final String propertyName, final JTextField component) { return bind(propertyName, new TextComponentAdapter(component)); } /** * Binds a property in the value container to a Swing {@code JFormattedTextField} component. * * @param propertyName The property name. * @param component The Swing component. * @return The resulting binding. */ public Binding bind(final String propertyName, final JFormattedTextField component) { return bind(propertyName, new FormattedTextFieldAdapter(component)); } /** * Binds a property in the value container to a Swing {@code JCheckBox} component. * * @param propertyName The property name. * @param component The Swing component. * @return The resulting binding. */ public Binding bind(final String propertyName, final JCheckBox component) { return bind(propertyName, new AbstractButtonAdapter(component)); } /** * Binds a property in the value container to a Swing {@code JRadioButton} component. * * @param propertyName The property name. * @param component The Swing component. * @return The resulting binding. */ public Binding bind(String propertyName, JRadioButton component) { return bind(propertyName, new AbstractButtonAdapter(component)); } /** * Binds a property in the value container to a Swing {@code JList} component. * * @param propertyName The property name. * @param component The Swing component. * @param selectionIsValue if {@code true}, the current list selection provides the value, * if {@code false}, the list content is the value. * @return The resulting binding. */ public Binding bind(final String propertyName, final JList component, final boolean selectionIsValue) { if (selectionIsValue) { return bind(propertyName, new ListSelectionAdapter(component)); } else { throw new RuntimeException("not implemented"); } } /** * Binds a property in the value container to a Swing {@code JSpinner} component. * * @param propertyName The property name. * @param component The Swing component. * @return The resulting binding. */ public Binding bind(final String propertyName, final JSpinner component) { return bind(propertyName, new SpinnerAdapter(component)); } /** * Binds a property in the value container to a Swing {@code JComboBox} component. * * @param propertyName The property name. * @param component The Swing component. * @return The resulting binding. */ public Binding bind(final String propertyName, final JComboBox component) { return bind(propertyName, new ComboBoxAdapter(component)); } /** * Binds a property in the value container to a Swing {@code ButtonGroup}. * * @param propertyName The property name. * @param buttonGroup The button group. * @return The resulting binding. */ public Binding bind(final String propertyName, final ButtonGroup buttonGroup) { return bind(propertyName, buttonGroup, ButtonGroupAdapter.createButtonToValueMap(buttonGroup, getPropertySet(), propertyName)); } /** * Binds a property in the value container to a Swing {@code ButtonGroup}. * * @param propertyName The property name. * @param buttonGroup The button group. * @param valueSet The mapping from a button to the actual property value. * @return The resulting binding. */ public Binding bind(final String propertyName, final ButtonGroup buttonGroup, final Map<AbstractButton, Object> valueSet) { ComponentAdapter adapter = new ButtonGroupAdapter(buttonGroup, valueSet); return bind(propertyName, adapter); } /** * Shortcut for {@link com.bc.ceres.binding.PropertyContainer#addPropertyChangeListener(java.beans.PropertyChangeListener) getPropertyContainer().addPropertyChangeListener(l}. * * @param l The property change listener. */ public void addPropertyChangeListener(PropertyChangeListener l) { propertySet.addPropertyChangeListener(l); } /** * Shortcut for {@link com.bc.ceres.binding.PropertyContainer#addPropertyChangeListener(String, java.beans.PropertyChangeListener) getPropertyContainer().addPropertyChangeListener(name, l}. * * @param name The property name. * @param l The property change listener. */ public void addPropertyChangeListener(String name, PropertyChangeListener l) { propertySet.addPropertyChangeListener(name, l); } /** * Shortcut for {@link com.bc.ceres.binding.PropertyContainer#removePropertyChangeListener(java.beans.PropertyChangeListener) getPropertyContainer().removePropertyChangeListener(l}. * * @param l The property change listener. */ public void removePropertyChangeListener(PropertyChangeListener l) { propertySet.removePropertyChangeListener(l); } /** * Shortcut for {@link com.bc.ceres.binding.PropertyContainer#removePropertyChangeListener(String, java.beans.PropertyChangeListener) getPropertyContainer().removePropertyChangeListener(name, l}. * * @param name The property name. * @param l The property change listener. */ public void removePropertyChangeListener(String name, PropertyChangeListener l) { propertySet.removePropertyChangeListener(name, l); } /** * Permits component validation and property changes of the value container * triggered by the given component. * * @param component The component. * @see #preventPropertyChanges(javax.swing.JComponent) * @see #getPropertySet() * @since Ceres 0.10 */ @SuppressWarnings({"MethodMayBeStatic"}) public void permitPropertyChanges(JComponent component) { component.setVerifyInputWhenFocusTarget(true); } /** * Prevents component validation and property changes of the value container * triggered by the given component. * <p/> * For example, if a text component loses keyboard focus because another component requests it, * its text value will be converted, validated and the value container's property will be changed. * For some focus targets, like a dialog's "Cancel" button, this is not desired. * <p/> * By default, component validation and property changes are permitted for most Swing components. * * @param component The component. * @see #permitPropertyChanges(javax.swing.JComponent) * @see #getPropertySet() * @since Ceres 0.10 */ @SuppressWarnings({"MethodMayBeStatic"}) public void preventPropertyChanges(JComponent component) { component.setVerifyInputWhenFocusTarget(false); } private void configureComponents(Binding binding) { final String propertyName = binding.getPropertyName(); final String toolTipTextStr = getToolTipText(propertyName); final boolean enabled = isEnabled(propertyName); final JComponent[] components = binding.getComponents(); JComponent primaryComponent = components[0]; configureComponent(primaryComponent, propertyName, toolTipTextStr, enabled); for (int i = 1; i < components.length; i++) { JComponent component = components[i]; configureComponent(component, propertyName + "." + i, toolTipTextStr, enabled); } } private String getToolTipText(String propertyName) { final Property property = propertySet.getProperty(propertyName); StringBuilder toolTipText = new StringBuilder(32); final PropertyDescriptor propertyDescriptor = property.getDescriptor(); if (propertyDescriptor.getDescription() != null) { toolTipText.append(propertyDescriptor.getDescription()); } if (propertyDescriptor.getUnit() != null && !propertyDescriptor.getUnit().isEmpty()) { toolTipText.append(" ("); toolTipText.append(propertyDescriptor.getUnit()); toolTipText.append(")"); } return toolTipText.toString(); } private boolean isEnabled(String propertyName) { final Property property = propertySet.getProperty(propertyName); final PropertyDescriptor propertyDescriptor = property.getDescriptor(); final Object enabled = propertyDescriptor.getAttribute("enabled"); return enabled == null || !Boolean.FALSE.equals(enabled); } private static void configureComponent(JComponent component, String name, String toolTipText, boolean enabled) { if (component.getName() == null) { component.setName(name); } if (component.getToolTipText() == null && !toolTipText.isEmpty()) { component.setToolTipText(toolTipText); } component.setEnabled(enabled); } /** * Sets the <i>enabled</i> state of the components associated with {@code targetProperty}. * If the current value of {@code sourceProperty} equals {@code sourcePropertyValue} then * the enabled state will be set to the value of {@code enabled}, otherwise it is the negated value * of {@code enabled}. Neither the source property nor the target property need to have an active binding. * * @param targetPropertyName The name of the target property. * @param targetState The enabled state. * @param sourcePropertyName The name of the source property. * @param sourcePropertyValue The value of the source property. */ public Enablement bindEnabledState(final String targetPropertyName, final boolean targetState, final String sourcePropertyName, final Object sourcePropertyValue) { return bindEnabledState(targetPropertyName, targetState, new EqualValuesCondition(sourcePropertyName, sourcePropertyValue)); } /** * Sets the <i>enabled</i> state of the components associated with {@code targetProperty} to {@code targetState} * if a certain {@code condition} is met. * * @param targetPropertyName The name of the target property. * @param targetState The target enabled state. * @param condition The condition. */ public Enablement bindEnabledState(final String targetPropertyName, final boolean targetState, final Enablement.Condition condition) { final EnablementImpl enablement = new EnablementImpl(targetPropertyName, targetState, condition); final Binding binding = getBinding(targetPropertyName); if (binding != null) { activateEnablement(enablement); } enablements.put(targetPropertyName, enablement); return enablement; } public void setComponentsEnabled(String propertyName, boolean enabled) { final JComponent[] components = getBinding(propertyName).getComponents(); for (JComponent component : components) { component.setEnabled(enabled); } } private void setComponentsEnabled(final String targetPropertyName, final boolean targetState, Enablement.Condition condition) { boolean conditionIsTrue = condition.evaluate(this); final JComponent[] components = getBinding(targetPropertyName).getComponents(); for (JComponent component : components) { component.setEnabled(conditionIsTrue ? targetState : !targetState); } } private void addBinding(BindingImpl binding) { bindings.put(binding.getPropertyName(), binding); EnablementImpl enablement = enablements.get(binding.getPropertyName()); if (enablement != null && !enablement.isActive()) { activateEnablement(enablement); } } private void removeBinding(String propertyName) { bindings.remove(propertyName); EnablementImpl enablement = enablements.get(propertyName); if (enablement != null) { deactivateEnablement(enablement); } } private void activateEnablement(EnablementImpl enablement) { enablement.setActive(true); enablement.apply(); enablement.getCondition().install(this, enablement); } private void deactivateEnablement(EnablementImpl enablement) { enablement.setActive(false); enablement.getCondition().uninstall(this, enablement); } public static class VerbousProblemHandler implements BindingProblemListener { @Override public void problemReported(BindingProblem newProblem, BindingProblem oldProblem) { final Binding binding = newProblem.getBinding(); final ComponentAdapter adapter = binding.getComponentAdapter(); final JComponent component = adapter.getComponents()[0]; final Window window = SwingUtilities.windowForComponent(component); JOptionPane.showMessageDialog(window, newProblem.getCause().getMessage(), "Invalid Input", JOptionPane.ERROR_MESSAGE); } @Override public void problemCleared(BindingProblem oldProblem) { } } public static class SilentProblemHandler implements BindingProblemListener { @Override public void problemReported(BindingProblem newProblem, BindingProblem oldProblem) { newProblem.getBinding().adjustComponents(); } @Override public void problemCleared(BindingProblem oldProblem) { } } private class EnablementImpl implements Enablement { private final String targetPropertyName; private final boolean targetState; private final Condition condition; private boolean active; private EnablementImpl(String targetPropertyName, boolean targetState, Condition condition) { this.targetPropertyName = targetPropertyName; this.targetState = targetState; this.condition = condition; } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } public Condition getCondition() { return condition; } @Override public final void propertyChange(PropertyChangeEvent evt) { apply(); } public void apply() { setComponentsEnabled(targetPropertyName, targetState, condition); } } private static class EqualValuesCondition extends Enablement.Condition { private final String sourcePropertyName; private final Object sourcePropertyValue; private EqualValuesCondition(String sourcePropertyName, Object sourcePropertyValue) { this.sourcePropertyName = sourcePropertyName; this.sourcePropertyValue = sourcePropertyValue; } @Override public boolean evaluate(BindingContext bindingContext) { Object propertyValue = bindingContext.getPropertySet().getValue(sourcePropertyName); return propertyValue == sourcePropertyValue || (propertyValue != null && propertyValue.equals(sourcePropertyValue)); } @Override public void install(BindingContext bindingContext, Enablement enablement) { bindingContext.addPropertyChangeListener(sourcePropertyName, enablement); } @Override public void uninstall(BindingContext bindingContext, Enablement enablement) { bindingContext.removePropertyChangeListener(sourcePropertyName, enablement); } } }