/* * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import javax.swing.text.*; import javax.swing.plaf.SpinnerUI; import java.util.*; import java.beans.*; import java.text.*; import java.io.*; import java.text.spi.DateFormatProvider; import java.text.spi.NumberFormatProvider; import javax.accessibility.*; import sun.util.locale.provider.LocaleProviderAdapter; import sun.util.locale.provider.LocaleResources; import sun.util.locale.provider.LocaleServiceProviderPool; /** * A single line input field that lets the user select a * number or an object value from an ordered sequence. Spinners typically * provide a pair of tiny arrow buttons for stepping through the elements * of the sequence. The keyboard up/down arrow keys also cycle through the * elements. The user may also be allowed to type a (legal) value directly * into the spinner. Although combo boxes provide similar functionality, * spinners are sometimes preferred because they don't require a drop down list * that can obscure important data. *

* A JSpinner's sequence value is defined by its * SpinnerModel. * The model can be specified as a constructor argument and * changed with the model property. SpinnerModel * classes for some common types are provided: SpinnerListModel, * SpinnerNumberModel, and SpinnerDateModel. *

* A JSpinner has a single child component that's * responsible for displaying * and potentially changing the current element or value of * the model, which is called the editor. The editor is created * by the JSpinner's constructor and can be changed with the * editor property. The JSpinner's editor stays * in sync with the model by listening for ChangeEvents. If the * user has changed the value displayed by the editor it is * possible for the model's value to differ from that of * the editor. To make sure the model has the same * value as the editor use the commitEdit method, eg: *

 *   try {
 *       spinner.commitEdit();
 *   }
 *   catch (ParseException pe) {{
 *       // Edited value is invalid, spinner.getValue() will return
 *       // the last valid value, you could revert the spinner to show that:
 *       JComponent editor = spinner.getEditor()
 *       if (editor instanceof DefaultEditor) {
 *           ((DefaultEditor)editor).getTextField().setValue(spinner.getValue();
 *       }
 *       // reset the value to some known value:
 *       spinner.setValue(fallbackValue);
 *       // or treat the last valid value as the current, in which
 *       // case you don't need to do anything.
 *   }
 *   return spinner.getValue();
 * 
*

* For information and examples of using spinner see * How to Use Spinners, * a section in The Java Tutorial. *

* Warning: Swing is not thread safe. For more * information see Swing's Threading * Policy. *

* Warning: * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is * appropriate for short term storage or RMI between applications running * the same version of Swing. As of 1.4, support for long term storage * of all JavaBeans™ * has been added to the java.beans package. * Please see {@link java.beans.XMLEncoder}. * * @beaninfo * attribute: isContainer false * description: A single line input field that lets the user select a * number or an object value from an ordered set. * * @see SpinnerModel * @see AbstractSpinnerModel * @see SpinnerListModel * @see SpinnerNumberModel * @see SpinnerDateModel * @see JFormattedTextField * * @author Hans Muller * @author Lynn Monsanto (accessibility) * @since 1.4 */ @SuppressWarnings("serial") // Same-version serialization only public class JSpinner extends JComponent implements Accessible { /** * @see #getUIClassID * @see #readObject */ private static final String uiClassID = "SpinnerUI"; private static final Action DISABLED_ACTION = new DisabledAction(); private SpinnerModel model; private JComponent editor; private ChangeListener modelListener; private transient ChangeEvent changeEvent; private boolean editorExplicitlySet = false; /** * Constructs a spinner for the given model. The spinner has * a set of previous/next buttons, and an editor appropriate * for the model. * * @throws NullPointerException if the model is {@code null} */ public JSpinner(SpinnerModel model) { if (model == null) { throw new NullPointerException("model cannot be null"); } this.model = model; this.editor = createEditor(model); setUIProperty("opaque",true); updateUI(); } /** * Constructs a spinner with an Integer SpinnerNumberModel * with initial value 0 and no minimum or maximum limits. */ public JSpinner() { this(new SpinnerNumberModel()); } /** * Returns the look and feel (L&F) object that renders this component. * * @return the SpinnerUI object that renders this component */ public SpinnerUI getUI() { return (SpinnerUI)ui; } /** * Sets the look and feel (L&F) object that renders this component. * * @param ui the SpinnerUI L&F object * @see UIDefaults#getUI */ public void setUI(SpinnerUI ui) { super.setUI(ui); } /** * Returns the suffix used to construct the name of the look and feel * (L&F) class used to render this component. * * @return the string "SpinnerUI" * @see JComponent#getUIClassID * @see UIDefaults#getUI */ public String getUIClassID() { return uiClassID; } /** * Resets the UI property with the value from the current look and feel. * * @see UIManager#getUI */ public void updateUI() { setUI((SpinnerUI)UIManager.getUI(this)); invalidate(); } /** * This method is called by the constructors to create the * JComponent * that displays the current value of the sequence. The editor may * also allow the user to enter an element of the sequence directly. * An editor must listen for ChangeEvents on the * model and keep the value it displays * in sync with the value of the model. *

* Subclasses may override this method to add support for new * SpinnerModel classes. Alternatively one can just * replace the editor created here with the setEditor * method. The default mapping from model type to editor is: *

* * @return a component that displays the current value of the sequence * @param model the value of getModel * @see #getModel * @see #setEditor */ protected JComponent createEditor(SpinnerModel model) { if (model instanceof SpinnerDateModel) { return new DateEditor(this); } else if (model instanceof SpinnerListModel) { return new ListEditor(this); } else if (model instanceof SpinnerNumberModel) { return new NumberEditor(this); } else { return new DefaultEditor(this); } } /** * Changes the model that represents the value of this spinner. * If the editor property has not been explicitly set, * the editor property is (implicitly) set after the "model" * PropertyChangeEvent has been fired. The editor * property is set to the value returned by createEditor, * as in: *
     * setEditor(createEditor(model));
     * 
* * @param model the new SpinnerModel * @see #getModel * @see #getEditor * @see #setEditor * @throws IllegalArgumentException if model is null * * @beaninfo * bound: true * attribute: visualUpdate true * description: Model that represents the value of this spinner. */ public void setModel(SpinnerModel model) { if (model == null) { throw new IllegalArgumentException("null model"); } if (!model.equals(this.model)) { SpinnerModel oldModel = this.model; this.model = model; if (modelListener != null) { oldModel.removeChangeListener(modelListener); this.model.addChangeListener(modelListener); } firePropertyChange("model", oldModel, model); if (!editorExplicitlySet) { setEditor(createEditor(model)); // sets editorExplicitlySet true editorExplicitlySet = false; } repaint(); revalidate(); } } /** * Returns the SpinnerModel that defines * this spinners sequence of values. * * @return the value of the model property * @see #setModel */ public SpinnerModel getModel() { return model; } /** * Returns the current value of the model, typically * this value is displayed by the editor. If the * user has changed the value displayed by the editor it is * possible for the model's value to differ from that of * the editor, refer to the class level javadoc for examples * of how to deal with this. *

* This method simply delegates to the model. * It is equivalent to: *

     * getModel().getValue()
     * 
* * @see #setValue * @see SpinnerModel#getValue */ public Object getValue() { return getModel().getValue(); } /** * Changes current value of the model, typically * this value is displayed by the editor. * If the SpinnerModel implementation * doesn't support the specified value then an * IllegalArgumentException is thrown. *

* This method simply delegates to the model. * It is equivalent to: *

     * getModel().setValue(value)
     * 
* * @throws IllegalArgumentException if value isn't allowed * @see #getValue * @see SpinnerModel#setValue */ public void setValue(Object value) { getModel().setValue(value); } /** * Returns the object in the sequence that comes after the object returned * by getValue(). If the end of the sequence has been reached * then return null. * Calling this method does not effect value. *

* This method simply delegates to the model. * It is equivalent to: *

     * getModel().getNextValue()
     * 
* * @return the next legal value or null if one doesn't exist * @see #getValue * @see #getPreviousValue * @see SpinnerModel#getNextValue */ public Object getNextValue() { return getModel().getNextValue(); } /** * We pass Change events along to the listeners with the * the slider (instead of the model itself) as the event source. */ private class ModelListener implements ChangeListener, Serializable { public void stateChanged(ChangeEvent e) { fireStateChanged(); } } /** * Adds a listener to the list that is notified each time a change * to the model occurs. The source of ChangeEvents * delivered to ChangeListeners will be this * JSpinner. Note also that replacing the model * will not affect listeners added directly to JSpinner. * Applications can add listeners to the model directly. In that * case is that the source of the event would be the * SpinnerModel. * * @param listener the ChangeListener to add * @see #removeChangeListener * @see #getModel */ public void addChangeListener(ChangeListener listener) { if (modelListener == null) { modelListener = new ModelListener(); getModel().addChangeListener(modelListener); } listenerList.add(ChangeListener.class, listener); } /** * Removes a ChangeListener from this spinner. * * @param listener the ChangeListener to remove * @see #fireStateChanged * @see #addChangeListener */ public void removeChangeListener(ChangeListener listener) { listenerList.remove(ChangeListener.class, listener); } /** * Returns an array of all the ChangeListeners added * to this JSpinner with addChangeListener(). * * @return all of the ChangeListeners added or an empty * array if no listeners have been added * @since 1.4 */ public ChangeListener[] getChangeListeners() { return listenerList.getListeners(ChangeListener.class); } /** * Sends a ChangeEvent, whose source is this * JSpinner, to each ChangeListener. * When a ChangeListener has been added * to the spinner, this method method is called each time * a ChangeEvent is received from the model. * * @see #addChangeListener * @see #removeChangeListener * @see EventListenerList */ protected void fireStateChanged() { Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ChangeListener.class) { if (changeEvent == null) { changeEvent = new ChangeEvent(this); } ((ChangeListener)listeners[i+1]).stateChanged(changeEvent); } } } /** * Returns the object in the sequence that comes * before the object returned by getValue(). * If the end of the sequence has been reached then * return null. Calling this method does * not effect value. *

* This method simply delegates to the model. * It is equivalent to: *

     * getModel().getPreviousValue()
     * 
* * @return the previous legal value or null * if one doesn't exist * @see #getValue * @see #getNextValue * @see SpinnerModel#getPreviousValue */ public Object getPreviousValue() { return getModel().getPreviousValue(); } /** * Changes the JComponent that displays the current value * of the SpinnerModel. It is the responsibility of this * method to disconnect the old editor from the model and to * connect the new editor. This may mean removing the * old editors ChangeListener from the model or the * spinner itself and adding one for the new editor. * * @param editor the new editor * @see #getEditor * @see #createEditor * @see #getModel * @throws IllegalArgumentException if editor is null * * @beaninfo * bound: true * attribute: visualUpdate true * description: JComponent that displays the current value of the model */ public void setEditor(JComponent editor) { if (editor == null) { throw new IllegalArgumentException("null editor"); } if (!editor.equals(this.editor)) { JComponent oldEditor = this.editor; this.editor = editor; if (oldEditor instanceof DefaultEditor) { ((DefaultEditor)oldEditor).dismiss(this); } editorExplicitlySet = true; firePropertyChange("editor", oldEditor, editor); revalidate(); repaint(); } } /** * Returns the component that displays and potentially * changes the model's value. * * @return the component that displays and potentially * changes the model's value * @see #setEditor * @see #createEditor */ public JComponent getEditor() { return editor; } /** * Commits the currently edited value to the SpinnerModel. *

* If the editor is an instance of DefaultEditor, the * call if forwarded to the editor, otherwise this does nothing. * * @throws ParseException if the currently edited value couldn't * be committed. */ public void commitEdit() throws ParseException { JComponent editor = getEditor(); if (editor instanceof DefaultEditor) { ((DefaultEditor)editor).commitEdit(); } } /* * See readObject and writeObject in JComponent for more * information about serialization in Swing. * * @param s Stream to write to */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); if (getUIClassID().equals(uiClassID)) { byte count = JComponent.getWriteObjCounter(this); JComponent.setWriteObjCounter(this, --count); if (count == 0 && ui != null) { ui.installUI(this); } } } /** * A simple base class for more specialized editors * that displays a read-only view of the model's current * value with a JFormattedTextField. Subclasses * can configure the JFormattedTextField to create * an editor that's appropriate for the type of model they * support and they may want to override * the stateChanged and propertyChanged * methods, which keep the model and the text field in sync. *

* This class defines a dismiss method that removes the * editors ChangeListener from the JSpinner * that it's part of. The setEditor method knows about * DefaultEditor.dismiss, so if the developer * replaces an editor that's derived from JSpinner.DefaultEditor * its ChangeListener connection back to the * JSpinner will be removed. However after that, * it's up to the developer to manage their editor listeners. * Similarly, if a subclass overrides createEditor, * it's up to the subclasser to deal with their editor * subsequently being replaced (with setEditor). * We expect that in most cases, and in editor installed * with setEditor or created by a createEditor * override, will not be replaced anyway. *

* This class is the LayoutManager for it's single * JFormattedTextField child. By default the * child is just centered with the parents insets. * @since 1.4 */ public static class DefaultEditor extends JPanel implements ChangeListener, PropertyChangeListener, LayoutManager { /** * Constructs an editor component for the specified JSpinner. * This DefaultEditor is it's own layout manager and * it is added to the spinner's ChangeListener list. * The constructor creates a single JFormattedTextField child, * initializes it's value to be the spinner model's current value * and adds it to this DefaultEditor. * * @param spinner the spinner whose model this editor will monitor * @see #getTextField * @see JSpinner#addChangeListener */ public DefaultEditor(JSpinner spinner) { super(null); JFormattedTextField ftf = new JFormattedTextField(); ftf.setName("Spinner.formattedTextField"); ftf.setValue(spinner.getValue()); ftf.addPropertyChangeListener(this); ftf.setEditable(false); ftf.setInheritsPopupMenu(true); String toolTipText = spinner.getToolTipText(); if (toolTipText != null) { ftf.setToolTipText(toolTipText); } add(ftf); setLayout(this); spinner.addChangeListener(this); // We want the spinner's increment/decrement actions to be // active vs those of the JFormattedTextField. As such we // put disabled actions in the JFormattedTextField's actionmap. // A binding to a disabled action is treated as a nonexistant // binding. ActionMap ftfMap = ftf.getActionMap(); if (ftfMap != null) { ftfMap.put("increment", DISABLED_ACTION); ftfMap.put("decrement", DISABLED_ACTION); } } /** * Disconnect this editor from the specified * JSpinner. By default, this method removes * itself from the spinners ChangeListener list. * * @param spinner the JSpinner to disconnect this * editor from; the same spinner as was passed to the constructor. */ public void dismiss(JSpinner spinner) { spinner.removeChangeListener(this); } /** * Returns the JSpinner ancestor of this editor or * null if none of the ancestors are a * JSpinner. * Typically the editor's parent is a JSpinner however * subclasses of JSpinner may override the * the createEditor method and insert one or more containers * between the JSpinner and it's editor. * * @return JSpinner ancestor; null * if none of the ancestors are a JSpinner * * @see JSpinner#createEditor */ public JSpinner getSpinner() { for (Component c = this; c != null; c = c.getParent()) { if (c instanceof JSpinner) { return (JSpinner)c; } } return null; } /** * Returns the JFormattedTextField child of this * editor. By default the text field is the first and only * child of editor. * * @return the JFormattedTextField that gives the user * access to the SpinnerDateModel's value. * @see #getSpinner * @see #getModel */ public JFormattedTextField getTextField() { return (JFormattedTextField)getComponent(0); } /** * This method is called when the spinner's model's state changes. * It sets the value of the text field to the current * value of the spinners model. * * @param e the ChangeEvent whose source is the * JSpinner whose model has changed. * @see #getTextField * @see JSpinner#getValue */ public void stateChanged(ChangeEvent e) { JSpinner spinner = (JSpinner)(e.getSource()); getTextField().setValue(spinner.getValue()); } /** * Called by the JFormattedTextField * PropertyChangeListener. When the "value" * property changes, which implies that the user has typed a new * number, we set the value of the spinners model. *

* This class ignores PropertyChangeEvents whose * source is not the JFormattedTextField, so subclasses * may safely make this DefaultEditor a * PropertyChangeListener on other objects. * * @param e the PropertyChangeEvent whose source is * the JFormattedTextField created by this class. * @see #getTextField */ public void propertyChange(PropertyChangeEvent e) { JSpinner spinner = getSpinner(); if (spinner == null) { // Indicates we aren't installed anywhere. return; } Object source = e.getSource(); String name = e.getPropertyName(); if ((source instanceof JFormattedTextField) && "value".equals(name)) { Object lastValue = spinner.getValue(); // Try to set the new value try { spinner.setValue(getTextField().getValue()); } catch (IllegalArgumentException iae) { // SpinnerModel didn't like new value, reset try { ((JFormattedTextField)source).setValue(lastValue); } catch (IllegalArgumentException iae2) { // Still bogus, nothing else we can do, the // SpinnerModel and JFormattedTextField are now out // of sync. } } } } /** * This LayoutManager method does nothing. We're * only managing a single child and there's no support * for layout constraints. * * @param name ignored * @param child ignored */ public void addLayoutComponent(String name, Component child) { } /** * This LayoutManager method does nothing. There * isn't any per-child state. * * @param child ignored */ public void removeLayoutComponent(Component child) { } /** * Returns the size of the parents insets. */ private Dimension insetSize(Container parent) { Insets insets = parent.getInsets(); int w = insets.left + insets.right; int h = insets.top + insets.bottom; return new Dimension(w, h); } /** * Returns the preferred size of first (and only) child plus the * size of the parents insets. * * @param parent the Container that's managing the layout * @return the preferred dimensions to lay out the subcomponents * of the specified container. */ public Dimension preferredLayoutSize(Container parent) { Dimension preferredSize = insetSize(parent); if (parent.getComponentCount() > 0) { Dimension childSize = getComponent(0).getPreferredSize(); preferredSize.width += childSize.width; preferredSize.height += childSize.height; } return preferredSize; } /** * Returns the minimum size of first (and only) child plus the * size of the parents insets. * * @param parent the Container that's managing the layout * @return the minimum dimensions needed to lay out the subcomponents * of the specified container. */ public Dimension minimumLayoutSize(Container parent) { Dimension minimumSize = insetSize(parent); if (parent.getComponentCount() > 0) { Dimension childSize = getComponent(0).getMinimumSize(); minimumSize.width += childSize.width; minimumSize.height += childSize.height; } return minimumSize; } /** * Resize the one (and only) child to completely fill the area * within the parents insets. */ public void layoutContainer(Container parent) { if (parent.getComponentCount() > 0) { Insets insets = parent.getInsets(); int w = parent.getWidth() - (insets.left + insets.right); int h = parent.getHeight() - (insets.top + insets.bottom); getComponent(0).setBounds(insets.left, insets.top, w, h); } } /** * Pushes the currently edited value to the SpinnerModel. *

* The default implementation invokes commitEdit on the * JFormattedTextField. * * @throws ParseException if the edited value is not legal */ public void commitEdit() throws ParseException { // If the value in the JFormattedTextField is legal, this will have // the result of pushing the value to the SpinnerModel // by way of the propertyChange method. JFormattedTextField ftf = getTextField(); ftf.commitEdit(); } /** * Returns the baseline. * * @throws IllegalArgumentException {@inheritDoc} * @see javax.swing.JComponent#getBaseline(int,int) * @see javax.swing.JComponent#getBaselineResizeBehavior() * @since 1.6 */ public int getBaseline(int width, int height) { // check size. super.getBaseline(width, height); Insets insets = getInsets(); width = width - insets.left - insets.right; height = height - insets.top - insets.bottom; int baseline = getComponent(0).getBaseline(width, height); if (baseline >= 0) { return baseline + insets.top; } return -1; } /** * Returns an enum indicating how the baseline of the component * changes as the size changes. * * @throws NullPointerException {@inheritDoc} * @see javax.swing.JComponent#getBaseline(int, int) * @since 1.6 */ public BaselineResizeBehavior getBaselineResizeBehavior() { return getComponent(0).getBaselineResizeBehavior(); } } /** * This subclass of javax.swing.DateFormatter maps the minimum/maximum * properties to the start/end properties of a SpinnerDateModel. */ private static class DateEditorFormatter extends DateFormatter { private final SpinnerDateModel model; DateEditorFormatter(SpinnerDateModel model, DateFormat format) { super(format); this.model = model; } @Override @SuppressWarnings("unchecked") public void setMinimum(Comparable min) { model.setStart((Comparable)min); } @Override public Comparable getMinimum() { return model.getStart(); } @Override @SuppressWarnings("unchecked") public void setMaximum(Comparable max) { model.setEnd((Comparable)max); } @Override public Comparable getMaximum() { return model.getEnd(); } } /** * An editor for a JSpinner whose model is a * SpinnerDateModel. The value of the editor is * displayed with a JFormattedTextField whose format * is defined by a DateFormatter instance whose * minimum and maximum properties * are mapped to the SpinnerDateModel. * @since 1.4 */ // PENDING(hmuller): more example javadoc public static class DateEditor extends DefaultEditor { // This is here until SimpleDateFormat gets a constructor that // takes a Locale: 4923525 private static String getDefaultPattern(Locale loc) { LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(DateFormatProvider.class, loc); LocaleResources lr = adapter.getLocaleResources(loc); if (lr == null) { lr = LocaleProviderAdapter.forJRE().getLocaleResources(loc); } return lr.getDateTimePattern(DateFormat.SHORT, DateFormat.SHORT, null); } /** * Construct a JSpinner editor that supports displaying * and editing the value of a SpinnerDateModel * with a JFormattedTextField. This * DateEditor becomes both a ChangeListener * on the spinners model and a PropertyChangeListener * on the new JFormattedTextField. * * @param spinner the spinner whose model this editor will monitor * @exception IllegalArgumentException if the spinners model is not * an instance of SpinnerDateModel * * @see #getModel * @see #getFormat * @see SpinnerDateModel */ public DateEditor(JSpinner spinner) { this(spinner, getDefaultPattern(spinner.getLocale())); } /** * Construct a JSpinner editor that supports displaying * and editing the value of a SpinnerDateModel * with a JFormattedTextField. This * DateEditor becomes both a ChangeListener * on the spinner and a PropertyChangeListener * on the new JFormattedTextField. * * @param spinner the spinner whose model this editor will monitor * @param dateFormatPattern the initial pattern for the * SimpleDateFormat object that's used to display * and parse the value of the text field. * @exception IllegalArgumentException if the spinners model is not * an instance of SpinnerDateModel * * @see #getModel * @see #getFormat * @see SpinnerDateModel * @see java.text.SimpleDateFormat */ public DateEditor(JSpinner spinner, String dateFormatPattern) { this(spinner, new SimpleDateFormat(dateFormatPattern, spinner.getLocale())); } /** * Construct a JSpinner editor that supports displaying * and editing the value of a SpinnerDateModel * with a JFormattedTextField. This * DateEditor becomes both a ChangeListener * on the spinner and a PropertyChangeListener * on the new JFormattedTextField. * * @param spinner the spinner whose model this editor * will monitor * @param format DateFormat object that's used to display * and parse the value of the text field. * @exception IllegalArgumentException if the spinners model is not * an instance of SpinnerDateModel * * @see #getModel * @see #getFormat * @see SpinnerDateModel * @see java.text.SimpleDateFormat */ private DateEditor(JSpinner spinner, DateFormat format) { super(spinner); if (!(spinner.getModel() instanceof SpinnerDateModel)) { throw new IllegalArgumentException( "model not a SpinnerDateModel"); } SpinnerDateModel model = (SpinnerDateModel)spinner.getModel(); DateFormatter formatter = new DateEditorFormatter(model, format); DefaultFormatterFactory factory = new DefaultFormatterFactory( formatter); JFormattedTextField ftf = getTextField(); ftf.setEditable(true); ftf.setFormatterFactory(factory); /* TBD - initializing the column width of the text field * is imprecise and doing it here is tricky because * the developer may configure the formatter later. */ try { String maxString = formatter.valueToString(model.getStart()); String minString = formatter.valueToString(model.getEnd()); ftf.setColumns(Math.max(maxString.length(), minString.length())); } catch (ParseException e) { // PENDING: hmuller } } /** * Returns the java.text.SimpleDateFormat object the * JFormattedTextField uses to parse and format * numbers. * * @return the value of getTextField().getFormatter().getFormat(). * @see #getTextField * @see java.text.SimpleDateFormat */ public SimpleDateFormat getFormat() { return (SimpleDateFormat)((DateFormatter)(getTextField().getFormatter())).getFormat(); } /** * Return our spinner ancestor's SpinnerDateModel. * * @return getSpinner().getModel() * @see #getSpinner * @see #getTextField */ public SpinnerDateModel getModel() { return (SpinnerDateModel)(getSpinner().getModel()); } } /** * This subclass of javax.swing.NumberFormatter maps the minimum/maximum * properties to a SpinnerNumberModel and initializes the valueClass * of the NumberFormatter to match the type of the initial models value. */ private static class NumberEditorFormatter extends NumberFormatter { private final SpinnerNumberModel model; NumberEditorFormatter(SpinnerNumberModel model, NumberFormat format) { super(format); this.model = model; setValueClass(model.getValue().getClass()); } @Override public void setMinimum(Comparable min) { model.setMinimum(min); } @Override public Comparable getMinimum() { return model.getMinimum(); } @Override public void setMaximum(Comparable max) { model.setMaximum(max); } @Override public Comparable getMaximum() { return model.getMaximum(); } } /** * An editor for a JSpinner whose model is a * SpinnerNumberModel. The value of the editor is * displayed with a JFormattedTextField whose format * is defined by a NumberFormatter instance whose * minimum and maximum properties * are mapped to the SpinnerNumberModel. * @since 1.4 */ // PENDING(hmuller): more example javadoc public static class NumberEditor extends DefaultEditor { // This is here until DecimalFormat gets a constructor that // takes a Locale: 4923525 private static String getDefaultPattern(Locale locale) { // Get the pattern for the default locale. LocaleProviderAdapter adapter; adapter = LocaleProviderAdapter.getAdapter(NumberFormatProvider.class, locale); LocaleResources lr = adapter.getLocaleResources(locale); if (lr == null) { lr = LocaleProviderAdapter.forJRE().getLocaleResources(locale); } String[] all = lr.getNumberPatterns(); return all[0]; } /** * Construct a JSpinner editor that supports displaying * and editing the value of a SpinnerNumberModel * with a JFormattedTextField. This * NumberEditor becomes both a ChangeListener * on the spinner and a PropertyChangeListener * on the new JFormattedTextField. * * @param spinner the spinner whose model this editor will monitor * @exception IllegalArgumentException if the spinners model is not * an instance of SpinnerNumberModel * * @see #getModel * @see #getFormat * @see SpinnerNumberModel */ public NumberEditor(JSpinner spinner) { this(spinner, getDefaultPattern(spinner.getLocale())); } /** * Construct a JSpinner editor that supports displaying * and editing the value of a SpinnerNumberModel * with a JFormattedTextField. This * NumberEditor becomes both a ChangeListener * on the spinner and a PropertyChangeListener * on the new JFormattedTextField. * * @param spinner the spinner whose model this editor will monitor * @param decimalFormatPattern the initial pattern for the * DecimalFormat object that's used to display * and parse the value of the text field. * @exception IllegalArgumentException if the spinners model is not * an instance of SpinnerNumberModel or if * decimalFormatPattern is not a legal * argument to DecimalFormat * * @see #getTextField * @see SpinnerNumberModel * @see java.text.DecimalFormat */ public NumberEditor(JSpinner spinner, String decimalFormatPattern) { this(spinner, new DecimalFormat(decimalFormatPattern)); } /** * Construct a JSpinner editor that supports displaying * and editing the value of a SpinnerNumberModel * with a JFormattedTextField. This * NumberEditor becomes both a ChangeListener * on the spinner and a PropertyChangeListener * on the new JFormattedTextField. * * @param spinner the spinner whose model this editor will monitor * @param decimalFormatPattern the initial pattern for the * DecimalFormat object that's used to display * and parse the value of the text field. * @exception IllegalArgumentException if the spinners model is not * an instance of SpinnerNumberModel * * @see #getTextField * @see SpinnerNumberModel * @see java.text.DecimalFormat */ private NumberEditor(JSpinner spinner, DecimalFormat format) { super(spinner); if (!(spinner.getModel() instanceof SpinnerNumberModel)) { throw new IllegalArgumentException( "model not a SpinnerNumberModel"); } SpinnerNumberModel model = (SpinnerNumberModel)spinner.getModel(); NumberFormatter formatter = new NumberEditorFormatter(model, format); DefaultFormatterFactory factory = new DefaultFormatterFactory( formatter); JFormattedTextField ftf = getTextField(); ftf.setEditable(true); ftf.setFormatterFactory(factory); // Change the text orientation for the NumberEditor ftf.setHorizontalAlignment(JTextField.RIGHT); /* TBD - initializing the column width of the text field * is imprecise and doing it here is tricky because * the developer may configure the formatter later. */ try { String maxString = formatter.valueToString(model.getMinimum()); String minString = formatter.valueToString(model.getMaximum()); ftf.setColumns(Math.max(maxString.length(), minString.length())); } catch (ParseException e) { // TBD should throw a chained error here } } /** * Returns the java.text.DecimalFormat object the * JFormattedTextField uses to parse and format * numbers. * * @return the value of getTextField().getFormatter().getFormat(). * @see #getTextField * @see java.text.DecimalFormat */ public DecimalFormat getFormat() { return (DecimalFormat)((NumberFormatter)(getTextField().getFormatter())).getFormat(); } /** * Return our spinner ancestor's SpinnerNumberModel. * * @return getSpinner().getModel() * @see #getSpinner * @see #getTextField */ public SpinnerNumberModel getModel() { return (SpinnerNumberModel)(getSpinner().getModel()); } /** * {@inheritDoc} */ @Override public void setComponentOrientation(ComponentOrientation o) { super.setComponentOrientation(o); getTextField().setHorizontalAlignment( o.isLeftToRight() ? JTextField.RIGHT : JTextField.LEFT); } } /** * An editor for a JSpinner whose model is a * SpinnerListModel. * @since 1.4 */ public static class ListEditor extends DefaultEditor { /** * Construct a JSpinner editor that supports displaying * and editing the value of a SpinnerListModel * with a JFormattedTextField. This * ListEditor becomes both a ChangeListener * on the spinner and a PropertyChangeListener * on the new JFormattedTextField. * * @param spinner the spinner whose model this editor will monitor * @exception IllegalArgumentException if the spinners model is not * an instance of SpinnerListModel * * @see #getModel * @see SpinnerListModel */ public ListEditor(JSpinner spinner) { super(spinner); if (!(spinner.getModel() instanceof SpinnerListModel)) { throw new IllegalArgumentException("model not a SpinnerListModel"); } getTextField().setEditable(true); getTextField().setFormatterFactory(new DefaultFormatterFactory(new ListFormatter())); } /** * Return our spinner ancestor's SpinnerNumberModel. * * @return getSpinner().getModel() * @see #getSpinner * @see #getTextField */ public SpinnerListModel getModel() { return (SpinnerListModel)(getSpinner().getModel()); } /** * ListFormatter provides completion while text is being input * into the JFormattedTextField. Completion is only done if the * user is inserting text at the end of the document. Completion * is done by way of the SpinnerListModel method findNextMatch. */ private class ListFormatter extends JFormattedTextField.AbstractFormatter { private DocumentFilter filter; public String valueToString(Object value) throws ParseException { if (value == null) { return ""; } return value.toString(); } public Object stringToValue(String string) throws ParseException { return string; } protected DocumentFilter getDocumentFilter() { if (filter == null) { filter = new Filter(); } return filter; } private class Filter extends DocumentFilter { public void replace(FilterBypass fb, int offset, int length, String string, AttributeSet attrs) throws BadLocationException { if (string != null && (offset + length) == fb.getDocument().getLength()) { Object next = getModel().findNextMatch( fb.getDocument().getText(0, offset) + string); String value = (next != null) ? next.toString() : null; if (value != null) { fb.remove(0, offset + length); fb.insertString(0, value, null); getFormattedTextField().select(offset + string.length(), value.length()); return; } } super.replace(fb, offset, length, string, attrs); } public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException { replace(fb, offset, 0, string, attr); } } } } /** * An Action implementation that is always disabled. */ private static class DisabledAction implements Action { public Object getValue(String key) { return null; } public void putValue(String key, Object value) { } public void setEnabled(boolean b) { } public boolean isEnabled() { return false; } public void addPropertyChangeListener(PropertyChangeListener l) { } public void removePropertyChangeListener(PropertyChangeListener l) { } public void actionPerformed(ActionEvent ae) { } } ///////////////// // Accessibility support //////////////// /** * Gets the AccessibleContext for the JSpinner * * @return the AccessibleContext for the JSpinner * @since 1.5 */ public AccessibleContext getAccessibleContext() { if (accessibleContext == null) { accessibleContext = new AccessibleJSpinner(); } return accessibleContext; } /** * AccessibleJSpinner implements accessibility * support for the JSpinner class. * @since 1.5 */ protected class AccessibleJSpinner extends AccessibleJComponent implements AccessibleValue, AccessibleAction, AccessibleText, AccessibleEditableText, ChangeListener { private Object oldModelValue = null; /** * AccessibleJSpinner constructor */ protected AccessibleJSpinner() { // model is guaranteed to be non-null oldModelValue = model.getValue(); JSpinner.this.addChangeListener(this); } /** * Invoked when the target of the listener has changed its state. * * @param e a ChangeEvent object. Must not be null. * @throws NullPointerException if the parameter is null. */ public void stateChanged(ChangeEvent e) { if (e == null) { throw new NullPointerException(); } Object newModelValue = model.getValue(); firePropertyChange(ACCESSIBLE_VALUE_PROPERTY, oldModelValue, newModelValue); firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, 0); // entire text may have changed oldModelValue = newModelValue; } /* ===== Begin AccessibleContext methods ===== */ /** * Gets the role of this object. The role of the object is the generic * purpose or use of the class of this object. For example, the role * of a push button is AccessibleRole.PUSH_BUTTON. The roles in * AccessibleRole are provided so component developers can pick from * a set of predefined roles. This enables assistive technologies to * provide a consistent interface to various tweaked subclasses of * components (e.g., use AccessibleRole.PUSH_BUTTON for all components * that act like a push button) as well as distinguish between subclasses * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes * and AccessibleRole.RADIO_BUTTON for radio buttons). *

Note that the AccessibleRole class is also extensible, so * custom component developers can define their own AccessibleRole's * if the set of predefined roles is inadequate. * * @return an instance of AccessibleRole describing the role of the object * @see AccessibleRole */ public AccessibleRole getAccessibleRole() { return AccessibleRole.SPIN_BOX; } /** * Returns the number of accessible children of the object. * * @return the number of accessible children of the object. */ public int getAccessibleChildrenCount() { // the JSpinner has one child, the editor if (editor.getAccessibleContext() != null) { return 1; } return 0; } /** * Returns the specified Accessible child of the object. The Accessible * children of an Accessible object are zero-based, so the first child * of an Accessible child is at index 0, the second child is at index 1, * and so on. * * @param i zero-based index of child * @return the Accessible child of the object * @see #getAccessibleChildrenCount */ public Accessible getAccessibleChild(int i) { // the JSpinner has one child, the editor if (i != 0) { return null; } if (editor.getAccessibleContext() != null) { return (Accessible)editor; } return null; } /* ===== End AccessibleContext methods ===== */ /** * Gets the AccessibleAction associated with this object that supports * one or more actions. * * @return AccessibleAction if supported by object; else return null * @see AccessibleAction */ public AccessibleAction getAccessibleAction() { return this; } /** * Gets the AccessibleText associated with this object presenting * text on the display. * * @return AccessibleText if supported by object; else return null * @see AccessibleText */ public AccessibleText getAccessibleText() { return this; } /* * Returns the AccessibleContext for the JSpinner editor */ private AccessibleContext getEditorAccessibleContext() { if (editor instanceof DefaultEditor) { JTextField textField = ((DefaultEditor)editor).getTextField(); if (textField != null) { return textField.getAccessibleContext(); } } else if (editor instanceof Accessible) { return editor.getAccessibleContext(); } return null; } /* * Returns the AccessibleText for the JSpinner editor */ private AccessibleText getEditorAccessibleText() { AccessibleContext ac = getEditorAccessibleContext(); if (ac != null) { return ac.getAccessibleText(); } return null; } /* * Returns the AccessibleEditableText for the JSpinner editor */ private AccessibleEditableText getEditorAccessibleEditableText() { AccessibleText at = getEditorAccessibleText(); if (at instanceof AccessibleEditableText) { return (AccessibleEditableText)at; } return null; } /** * Gets the AccessibleValue associated with this object. * * @return AccessibleValue if supported by object; else return null * @see AccessibleValue * */ public AccessibleValue getAccessibleValue() { return this; } /* ===== Begin AccessibleValue impl ===== */ /** * Get the value of this object as a Number. If the value has not been * set, the return value will be null. * * @return value of the object * @see #setCurrentAccessibleValue */ public Number getCurrentAccessibleValue() { Object o = model.getValue(); if (o instanceof Number) { return (Number)o; } return null; } /** * Set the value of this object as a Number. * * @param n the value to set for this object * @return true if the value was set; else False * @see #getCurrentAccessibleValue */ public boolean setCurrentAccessibleValue(Number n) { // try to set the new value try { model.setValue(n); return true; } catch (IllegalArgumentException iae) { // SpinnerModel didn't like new value } return false; } /** * Get the minimum value of this object as a Number. * * @return Minimum value of the object; null if this object does not * have a minimum value * @see #getMaximumAccessibleValue */ public Number getMinimumAccessibleValue() { if (model instanceof SpinnerNumberModel) { SpinnerNumberModel numberModel = (SpinnerNumberModel)model; Object o = numberModel.getMinimum(); if (o instanceof Number) { return (Number)o; } } return null; } /** * Get the maximum value of this object as a Number. * * @return Maximum value of the object; null if this object does not * have a maximum value * @see #getMinimumAccessibleValue */ public Number getMaximumAccessibleValue() { if (model instanceof SpinnerNumberModel) { SpinnerNumberModel numberModel = (SpinnerNumberModel)model; Object o = numberModel.getMaximum(); if (o instanceof Number) { return (Number)o; } } return null; } /* ===== End AccessibleValue impl ===== */ /* ===== Begin AccessibleAction impl ===== */ /** * Returns the number of accessible actions available in this object * If there are more than one, the first one is considered the "default" * action of the object. * * Two actions are supported: AccessibleAction.INCREMENT which * increments the spinner value and AccessibleAction.DECREMENT * which decrements the spinner value * * @return the zero-based number of Actions in this object */ public int getAccessibleActionCount() { return 2; } /** * Returns a description of the specified action of the object. * * @param i zero-based index of the actions * @return a String description of the action * @see #getAccessibleActionCount */ public String getAccessibleActionDescription(int i) { if (i == 0) { return AccessibleAction.INCREMENT; } else if (i == 1) { return AccessibleAction.DECREMENT; } return null; } /** * Performs the specified Action on the object * * @param i zero-based index of actions. The first action * (index 0) is AccessibleAction.INCREMENT and the second * action (index 1) is AccessibleAction.DECREMENT. * @return true if the action was performed; otherwise false. * @see #getAccessibleActionCount */ public boolean doAccessibleAction(int i) { if (i < 0 || i > 1) { return false; } Object o; if (i == 0) { o = getNextValue(); // AccessibleAction.INCREMENT } else { o = getPreviousValue(); // AccessibleAction.DECREMENT } // try to set the new value try { model.setValue(o); return true; } catch (IllegalArgumentException iae) { // SpinnerModel didn't like new value } return false; } /* ===== End AccessibleAction impl ===== */ /* ===== Begin AccessibleText impl ===== */ /* * Returns whether source and destination components have the * same window ancestor */ private boolean sameWindowAncestor(Component src, Component dest) { if (src == null || dest == null) { return false; } return SwingUtilities.getWindowAncestor(src) == SwingUtilities.getWindowAncestor(dest); } /** * Given a point in local coordinates, return the zero-based index * of the character under that Point. If the point is invalid, * this method returns -1. * * @param p the Point in local coordinates * @return the zero-based index of the character under Point p; if * Point is invalid return -1. */ public int getIndexAtPoint(Point p) { AccessibleText at = getEditorAccessibleText(); if (at != null && sameWindowAncestor(JSpinner.this, editor)) { // convert point from the JSpinner bounds (source) to // editor bounds (destination) Point editorPoint = SwingUtilities.convertPoint(JSpinner.this, p, editor); if (editorPoint != null) { return at.getIndexAtPoint(editorPoint); } } return -1; } /** * Determines the bounding box of the character at the given * index into the string. The bounds are returned in local * coordinates. If the index is invalid an empty rectangle is * returned. * * @param i the index into the String * @return the screen coordinates of the character's bounding box, * if index is invalid return an empty rectangle. */ public Rectangle getCharacterBounds(int i) { AccessibleText at = getEditorAccessibleText(); if (at != null ) { Rectangle editorRect = at.getCharacterBounds(i); if (editorRect != null && sameWindowAncestor(JSpinner.this, editor)) { // return rectangle in the the JSpinner bounds return SwingUtilities.convertRectangle(editor, editorRect, JSpinner.this); } } return null; } /** * Returns the number of characters (valid indicies) * * @return the number of characters */ public int getCharCount() { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getCharCount(); } return -1; } /** * Returns the zero-based offset of the caret. * * Note: That to the right of the caret will have the same index * value as the offset (the caret is between two characters). * @return the zero-based offset of the caret. */ public int getCaretPosition() { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getCaretPosition(); } return -1; } /** * Returns the String at a given index. * * @param part the CHARACTER, WORD, or SENTENCE to retrieve * @param index an index within the text * @return the letter, word, or sentence */ public String getAtIndex(int part, int index) { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getAtIndex(part, index); } return null; } /** * Returns the String after a given index. * * @param part the CHARACTER, WORD, or SENTENCE to retrieve * @param index an index within the text * @return the letter, word, or sentence */ public String getAfterIndex(int part, int index) { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getAfterIndex(part, index); } return null; } /** * Returns the String before a given index. * * @param part the CHARACTER, WORD, or SENTENCE to retrieve * @param index an index within the text * @return the letter, word, or sentence */ public String getBeforeIndex(int part, int index) { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getBeforeIndex(part, index); } return null; } /** * Returns the AttributeSet for a given character at a given index * * @param i the zero-based index into the text * @return the AttributeSet of the character */ public AttributeSet getCharacterAttribute(int i) { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getCharacterAttribute(i); } return null; } /** * Returns the start offset within the selected text. * If there is no selection, but there is * a caret, the start and end offsets will be the same. * * @return the index into the text of the start of the selection */ public int getSelectionStart() { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getSelectionStart(); } return -1; } /** * Returns the end offset within the selected text. * If there is no selection, but there is * a caret, the start and end offsets will be the same. * * @return the index into the text of the end of the selection */ public int getSelectionEnd() { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getSelectionEnd(); } return -1; } /** * Returns the portion of the text that is selected. * * @return the String portion of the text that is selected */ public String getSelectedText() { AccessibleText at = getEditorAccessibleText(); if (at != null) { return at.getSelectedText(); } return null; } /* ===== End AccessibleText impl ===== */ /* ===== Begin AccessibleEditableText impl ===== */ /** * Sets the text contents to the specified string. * * @param s the string to set the text contents */ public void setTextContents(String s) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.setTextContents(s); } } /** * Inserts the specified string at the given index/ * * @param index the index in the text where the string will * be inserted * @param s the string to insert in the text */ public void insertTextAtIndex(int index, String s) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.insertTextAtIndex(index, s); } } /** * Returns the text string between two indices. * * @param startIndex the starting index in the text * @param endIndex the ending index in the text * @return the text string between the indices */ public String getTextRange(int startIndex, int endIndex) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { return at.getTextRange(startIndex, endIndex); } return null; } /** * Deletes the text between two indices * * @param startIndex the starting index in the text * @param endIndex the ending index in the text */ public void delete(int startIndex, int endIndex) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.delete(startIndex, endIndex); } } /** * Cuts the text between two indices into the system clipboard. * * @param startIndex the starting index in the text * @param endIndex the ending index in the text */ public void cut(int startIndex, int endIndex) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.cut(startIndex, endIndex); } } /** * Pastes the text from the system clipboard into the text * starting at the specified index. * * @param startIndex the starting index in the text */ public void paste(int startIndex) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.paste(startIndex); } } /** * Replaces the text between two indices with the specified * string. * * @param startIndex the starting index in the text * @param endIndex the ending index in the text * @param s the string to replace the text between two indices */ public void replaceText(int startIndex, int endIndex, String s) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.replaceText(startIndex, endIndex, s); } } /** * Selects the text between two indices. * * @param startIndex the starting index in the text * @param endIndex the ending index in the text */ public void selectText(int startIndex, int endIndex) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.selectText(startIndex, endIndex); } } /** * Sets attributes for the text between two indices. * * @param startIndex the starting index in the text * @param endIndex the ending index in the text * @param as the attribute set * @see AttributeSet */ public void setAttributes(int startIndex, int endIndex, AttributeSet as) { AccessibleEditableText at = getEditorAccessibleEditableText(); if (at != null) { at.setAttributes(startIndex, endIndex, as); } } } /* End AccessibleJSpinner */ }