/* * @(#)JSpinner.java 1.38 04/05/12 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package javax.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; 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.util.HashMap; import sun.text.resources.LocaleData; import javax.accessibility.*; /** * 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 ChangeEvent
s. 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(); **
* 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 JavaBeansTM
* 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
*
* @version 1.38 05/12/04
* @author Hans Muller
* @author Lynn Monsanto (accessibility)
* @since 1.4
*/
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 transient SpinnerModel model;
private JComponent editor;
private ChangeListener modelListener;
private transient ChangeEvent changeEvent;
private boolean editorExplicitlySet = false;
/**
* Constructs a complete spinner with pair of next/previous buttons
* and an editor for the SpinnerModel
.
*/
public JSpinner(SpinnerModel model) {
this.model = model;
this.editor = createEditor(model);
setOpaque(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:
*
SpinnerNumberModel => JSpinner.NumberEditor
* SpinnerDateModel => JSpinner.DateEditor
* SpinnerListModel => JSpinner.ListEditor
* JSpinner.DefaultEditor
* "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) {
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 ChangeListener
s added
* to this JSpinner with addChangeListener().
*
* @return all of the ChangeListener
s added or an empty
* array if no listeners have been added
* @since 1.4
*/
public ChangeListener[] getChangeListeners() {
return (ChangeListener[])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
* This class defines a
* This class is the
* This class ignores
* The default implementation invokes 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 ((Accessible)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 AccessibleExtendedText 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 = null;
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 teh 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 */
}
DefaultEditor
, the
* call if forwarded to the editor, otherwise this does nothing.
*
* @throws ParseException if the currently edited value couldn't
* be commited.
*/
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();
HashMap additionalValues = new HashMap(1);
SpinnerModel model = getModel();
if (model instanceof Serializable) {
additionalValues.put("model", model);
}
s.writeObject(additionalValues);
if (getUIClassID().equals(uiClassID)) {
byte count = JComponent.getWriteObjCounter(this);
JComponent.setWriteObjCounter(this, --count);
if (count == 0 && ui != null) {
ui.installUI(this);
}
}
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
Map additionalValues = (Map)s.readObject();
model = (SpinnerModel)additionalValues.get("model");
}
/**
* 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.
* 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.
* LayoutManager
for it's single
*
JFormattedTextField
child. By default the
* child is just centered with the parents insets.
*/
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);
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.
* Typically the editor's parent is a JSpinner
however
* subclasses of createEditor
method and insert one or more containers
* between the JSpinner
and it's editor.
*
* @return JSpinner
ancestor
* @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 not used
* @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.
* 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
.
* 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();
}
}
/**
* This subclass of javax.swing.DateFormatter maps the minimum/maximum
* properties to te 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;
}
public void setMinimum(Comparable min) {
model.setStart(min);
}
public Comparable getMinimum() {
return model.getStart();
}
public void setMaximum(Comparable max) {
model.setEnd(max);
}
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
.
*/
// 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) {
ResourceBundle r = LocaleData.getLocaleElements(loc);
String[] dateTimePatterns = r.getStringArray("DateTimePatterns");
Object[] dateTimeArgs = {dateTimePatterns[DateFormat.SHORT],
dateTimePatterns[DateFormat.SHORT + 4]};
return MessageFormat.format(dateTimePatterns[8], dateTimeArgs);
}
/**
* 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());
}
public void setMinimum(Comparable min) {
model.setMinimum(min);
}
public Comparable getMinimum() {
return model.getMinimum();
}
public void setMaximum(Comparable max) {
model.setMaximum(max);
}
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
.
*/
// 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.
ResourceBundle rb = LocaleData.getLocaleElements(locale);
String[] all = rb.getStringArray("NumberPatterns");
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);
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());
}
}
/**
* An editor for a JSpinner
whose model is a
* SpinnerListModel
.
*/
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 sublasses
* that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes
* and AccessibleRole.RADIO_BUTTON for radio buttons).
*