/* * Copyright (c) 2007, 2015, Oracle. All rights reserved. * * This software is the proprietary information of Oracle Corporation. * Use is subject to license terms. */ package org.eclipse.persistence.tools.workbench.uitools.swing; import java.awt.Color; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.ParseException; import javax.swing.JFormattedTextField; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.text.DefaultFormatterFactory; import javax.swing.text.NumberFormatter; import org.eclipse.persistence.tools.workbench.uitools.app.PropertyValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.ValueModel; import org.eclipse.persistence.tools.workbench.uitools.app.swing.NumberSpinnerModelAdapter; /** * This handler is responsible to update the text field by showing a default * value if one is defined and when the value holder's value is <code>null</code>. * The default value is either removed on focus gained or added on focus lost if * and only if the value holder's value is <code>null</code>. The default value * is shown as grayed out. * <p> * <b>Note:</b> <code>JSpinner</code> is poorly designed, which makes it harder * to handle the default value. This handler uses a lot of hackery code and * some flags. * * @version 11.0.0 * @since 11.0.0 * @author Pascal Filion */ @SuppressWarnings("nls") public class SpinnerWithDefaultHandler { /** * The holder of the default value. */ private ValueModel defaultValueHolder; /** * Flag used to determine how to convert the value, which is only required * when installing our custom formatter. */ private boolean installingFormatter; /** * Flag used to show an empty string when the spinner receives the focus, * this happens by a mouse click on by the up or down buttons being used. */ private boolean showEmptyString; /** * A custom <code>JSpinner</code> is created in order to prevent the model's * value from being changed during the process of either showing the default * value or clearing it. */ private JSpinner spinner; /** * The text field that will be used to show the default value when the value * holder contains <code>null</code>. */ private JFormattedTextField textField; /** * Flag used to convert the number into the right string representation when * the number holder is receiving a new value. */ private boolean valueBeingChanged; /** * Constant used to determine if the value holder holds <code>null</code> In * that case, the actual default value is used and shown accordingly. */ static final Number DEFAULT_VALUE = -1; /** * Creates a new <code>SpinnerWithDefaultHandler</code>. * * @param subjectHolder The holder of the subject, which is used to either * clear the text field or show the default value * @param valueHolder The holder of the spinner's value * @param defaultValueHolder The holder of the default value * @param minimumValue The lower end of the allowed range * @param maximumValue The higher end of the allowed range * @param stepSize The increment used to increase or decrease the current * value */ public SpinnerWithDefaultHandler(ValueModel subjectHolder, PropertyValueModel valueHolder, ValueModel defaultValueHolder, Comparable<? extends Number> minimumValue, Comparable<? extends Number> maximumValue, Number stepSize) { super(); initialize(subjectHolder, valueHolder, defaultValueHolder, minimumValue, maximumValue, stepSize); } private PropertyChangeListener buildDefaultValuePropertyChangeListener() { return new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { spinnerModel().setDefaultValue((Number) e.getNewValue()); } }; } private FocusListener buildFocusListener() { return new FocusListener() { public void focusGained(FocusEvent e) { updateTextFieldOnFocusGained(); } public void focusLost(FocusEvent e) { if (!e.isTemporary()) { updateTextFieldOnFocusLost(); } } }; } private NumberFormatter buildFormatter(JSpinner.NumberEditor editor) { NumberFormatter oldformatter = (NumberFormatter) textField.getFormatter(); NumberFormatter newFormatter = buildNumberFormatter(editor); newFormatter.setMaximum (oldformatter.getMaximum()); newFormatter.setMinimum (oldformatter.getMinimum()); newFormatter.setValueClass (oldformatter.getValueClass()); newFormatter.setOverwriteMode(oldformatter.getOverwriteMode()); newFormatter.setAllowsInvalid(oldformatter.getAllowsInvalid()); newFormatter.setCommitsOnValidEdit(true); return newFormatter; } private ChangeListener buildModelChangeListener() { return new ChangeListener() { public void stateChanged(ChangeEvent e) { updateTextFieldForegroundColor(); } }; } /** * Creates a custom <code>NumberFormatter</code> that will correctly convert * the value from string into a <code>Number</code> or vice versa and by * handling the default value. * * @param editor The spinner's editor * @return A new <code>NumberFormatter</code> */ private NumberFormatter buildNumberFormatter(JSpinner.NumberEditor editor) { return new NumberFormatter(editor.getFormat()) { /** * {@inheritDoc} */ @Override public Object stringToValue(String text) throws ParseException { // If the text is the default value, return DEFAULT_VALUE to not // change anything, this can happen when the focus is not moved // to the spinner yet and the up/down button was pressed if (defaultValue().equals(text)) { return DEFAULT_VALUE; } // An empty string represent the "null" value so it can be pushed // into the number holder, if DEFAULT_VALUE was used, then the // number holder wouldn't be updated if (text.length() == 0) { return null; } // Leave the default implementation to convert // the number into a string representation return super.stringToValue(text); } /** * {@inheritDoc} */ @Override public String valueToString(Object value) throws ParseException { // While installing the formatter, we need to convert the // numberHolder's value and if the value is null then the default // value needs to be returned if (installingFormatter) { value = spinnerModel().getActualNumber(); if (value == null) { value = DEFAULT_VALUE; } } // The spinner gained focus and the numberHolder's value is null, // we simply need to return an empty string so the user can start // typing a new value else if (showEmptyString) { return ""; } // If the text field has the focus, use the text field's value // TODO // If the value is changed via the up/down buttons, then the value // won't be the same as the spinner model's value, which mean the // text field's text can't be used, the value needs to be converted // into its string reprensation so the text field can reflect the // new value else if (textField.hasFocus() && !valueBeingChanged) { return textField.getText(); } // Convert the default value by using the defaultValueHolder's value // encapsulated by parenthesis if (value == DEFAULT_VALUE) { return defaultValue(); } return super.valueToString(value); } }; } private PropertyChangeListener buildPropertyChangeListener() { return new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { subjectChanged(e.getNewValue() == null); } }; } /** * Creates the spinner that will be used to show or hide the default value. * * @param model The spinner's model * @return a new <code>JSpinner</code> */ protected JSpinner buildSpinner(SpinnerNumberModel model) { return new JSpinner(model); } /** * Returns the default value stored in the default value holder. * * @return The default value returned by the default value holder, * <code>null</code> is never returned */ private String defaultValue() { Number defaultValue = (Number)defaultValueHolder.getValue(); // No default value specified if (defaultValue == null) { return ""; } StringBuilder sb = new StringBuilder(); sb.append('('); sb.append(defaultValue); sb.append(')'); return sb.toString(); } /** * Returns the spinner that was created and initialized to support showing a * default value when the spinner does not have the focus and the value is * <code>null</code>. * * @return The spinner supporting showing a default value */ public final JSpinner getSpinner() { return spinner; } /** * Initializes a new <code>JSpinner</code> and registers the necessary * behavior to allow showing the default value when the current value is * <code>null</code>. * * @param subjectHolder The holder of the subject, which is used to either * clear the text field or show the default value * @param valueHolder The holder of the spinner's value * @param defaultValueHolder The holder of the default value * @param minimumValue The lower end of the allowed range * @param maximumValue The higher end of the allowed range * @param stepSize The increment used to increase or decrease the current * value */ private void initialize(ValueModel subjectHolder, PropertyValueModel valueHolder, ValueModel defaultValueHolder, Comparable minimumValue, Comparable maximumValue, Number stepSize) { this.defaultValueHolder = defaultValueHolder; subjectHolder.addPropertyChangeListener ( ValueModel.VALUE, buildPropertyChangeListener() ); defaultValueHolder.addPropertyChangeListener ( ValueModel.VALUE, buildDefaultValuePropertyChangeListener() ); SpinnerModel model = new SpinnerModel ( valueHolder, defaultValueHolder, minimumValue, maximumValue, stepSize ); spinner = buildSpinner(model); model.addChangeListener(buildModelChangeListener()); JSpinner.NumberEditor editor = (JSpinner.NumberEditor) spinner.getEditor(); textField = editor.getTextField(); textField.setFocusLostBehavior(JFormattedTextField.PERSIST); textField.addFocusListener(buildFocusListener()); installNumberFormatter(valueHolder, editor); updateTextFieldForegroundColor(); } /** * Installs a custom <code>NumberFormatter</code> that will allow to * correctly convert a <code>Number</code> into the appropriate string * representation or from a string value into a <code>Number</code>. * * @param numberHolder The holder of the spinner's value * @param editor The spinner's editor */ private void installNumberFormatter(ValueModel numberHolder, JSpinner.NumberEditor editor) { try { installingFormatter = true; NumberFormatter newFormatter = buildFormatter(editor); // When initializing the spinner, the numberHolder was not hooked yet // which means the value set was -1 if (numberHolder.getValue() != null) { textField.setValue(numberHolder.getValue()); } textField.setFormatterFactory(new DefaultFormatterFactory(newFormatter)); } finally { installingFormatter = false; } } private SpinnerModel spinnerModel() { return (SpinnerModel) spinner.getModel(); } /** * The subject has changed and if it's was nullified, then show the default * value if required. * * @param nullSubject */ private void subjectChanged(boolean nullSubject) { if (nullSubject) { try { showEmptyString = true; textField.setValue(DEFAULT_VALUE); } finally { showEmptyString = false; } } else { spinnerModel().synchronize(spinnerModel().getActualNumber()); } } /** * Updates the foreground color based on spinner's value. If the value is the * default value then the foreground color is changed to grey. */ private void updateTextFieldForegroundColor() { if (spinnerModel().getActualNumber() == null) { textField.setForeground(Color.GRAY); } else { textField.setForeground(UIManager.getColor("TextField.foreground")); } } /** * Clears the default value if the text field contains it. */ private void updateTextFieldOnFocusGained() { if (spinnerModel().getActualNumber() == null) { try { showEmptyString = true; textField.setValue(DEFAULT_VALUE); textField.setForeground(UIManager.getColor("TextField.foreground")); } finally { showEmptyString = false; } } } /** * Shows the default value if the text field contains an empty entry or the * default value (-1), which was manually entered. */ private void updateTextFieldOnFocusLost() { String text = textField.getText(); // If the current text is either the default value (-1), which was // manually entered or it'S blank, then we need to show the default value if ((text.length() == 0) || text.equals(DEFAULT_VALUE.toString())) { textField.setValue(DEFAULT_VALUE); textField.setForeground(Color.GRAY); } } /** * This <code>SpinnerModel</code> adds the support to convert <code>null</code> * to the default value properly and to update the numerous flags required * for supporting the default value displayed as "(default_value)". */ private class SpinnerModel extends NumberSpinnerModelAdapter { /** * The current value that is never <code>null</code> */ private Number value; /** * Creates a new <code>SpinnerModel</code>. * * @param valueHolder The holder of the spinner's value * @param defaultValueHolder The holder of the default value * @param minimumValue The lower end of the allowed range * @param maximumValue The higher end of the allowed range * @param stepSize The increment used to increase or decrease the current * value */ SpinnerModel(PropertyValueModel valueHolder, ValueModel defaultValueHolde, Comparable minimumValue, Comparable maximumValue, Number stepSize) { super(valueHolder, minimumValue, maximumValue, stepSize, (defaultValueHolder.getValue() != null) ? (Number)defaultValueHolder.getValue() : 0); synchronizeValue(); } /** * Returns the actual value, which is stored in the number holder. * * @return The current value, which can be <code>null</code> */ Object getActualNumber() { return numberHolder.getValue(); } /** * {@inheritDoc} */ @Override public Number getNumber() { return value; } /** * {@inheritDoc} */ @Override public Number getValue() { return value; } /** * {@inheritDoc} */ @Override protected void setDefaultValue(Number defaultValue) { super.setDefaultValue(defaultValue); // The current value is null, simply fire a state changed in order to // change the spinner's value and the text field's value if (value == DEFAULT_VALUE) { fireStateChanged(); } } /** * {@inheritDoc} */ @Override public void setValue(Object value) { if ((value != DEFAULT_VALUE) && !installingFormatter) { try { valueBeingChanged = (value != null); showEmptyString = (value == null); this.value = (value == null) ? DEFAULT_VALUE : (Number) value; super.setValue(value); } finally { valueBeingChanged = false; showEmptyString = false; } } } /** * {@inheritDoc} */ @Override protected void synchronize(Object value) { // The number holder can hold null but SpinnerModel doesn't handle // null, simply use a fake number this.value = (value == null) ? DEFAULT_VALUE : (Number)value; super.synchronize(this.value); // If the value is null and the value stored in SpinnerNumberModel // is DEFAULT_VALUE then no event will be fired, fire it ourself so // the spinner can be updated and show the default value if ((value == null) && (getSuperValue() == DEFAULT_VALUE)) { fireStateChanged(); } } private void synchronizeValue() { value = (Number)numberHolder.getValue(); if (value == null) { value = DEFAULT_VALUE; } } } }