1 /*
2 * Copyright 2000-2007 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26 package javax.swing;
27
28 import java.awt;
29 import java.awt.event;
30
31 import javax.swing;
32 import javax.swing.event;
33 import javax.swing.text;
34 import javax.swing.plaf.SpinnerUI;
35
36 import java.util;
37 import java.beans;
38 import java.text;
39 import java.io;
40 import java.util.HashMap;
41 import sun.util.resources.LocaleData;
42
43 import javax.accessibility;
44
45
46 /**
47 * A single line input field that lets the user select a
48 * number or an object value from an ordered sequence. Spinners typically
49 * provide a pair of tiny arrow buttons for stepping through the elements
50 * of the sequence. The keyboard up/down arrow keys also cycle through the
51 * elements. The user may also be allowed to type a (legal) value directly
52 * into the spinner. Although combo boxes provide similar functionality,
53 * spinners are sometimes preferred because they don't require a drop down list
54 * that can obscure important data.
55 * <p>
56 * A <code>JSpinner</code>'s sequence value is defined by its
57 * <code>SpinnerModel</code>.
58 * The <code>model</code> can be specified as a constructor argument and
59 * changed with the <code>model</code> property. <code>SpinnerModel</code>
60 * classes for some common types are provided: <code>SpinnerListModel</code>,
61 * <code>SpinnerNumberModel</code>, and <code>SpinnerDateModel</code>.
62 * <p>
63 * A <code>JSpinner</code> has a single child component that's
64 * responsible for displaying
65 * and potentially changing the current element or <i>value</i> of
66 * the model, which is called the <code>editor</code>. The editor is created
67 * by the <code>JSpinner</code>'s constructor and can be changed with the
68 * <code>editor</code> property. The <code>JSpinner</code>'s editor stays
69 * in sync with the model by listening for <code>ChangeEvent</code>s. If the
70 * user has changed the value displayed by the <code>editor</code> it is
71 * possible for the <code>model</code>'s value to differ from that of
72 * the <code>editor</code>. To make sure the <code>model</code> has the same
73 * value as the editor use the <code>commitEdit</code> method, eg:
74 * <pre>
75 * try {
76 * spinner.commitEdit();
77 * }
78 * catch (ParseException pe) {{
79 * // Edited value is invalid, spinner.getValue() will return
80 * // the last valid value, you could revert the spinner to show that:
81 * JComponent editor = spinner.getEditor()
82 * if (editor instanceof DefaultEditor) {
83 * ((DefaultEditor)editor).getTextField().setValue(spinner.getValue();
84 * }
85 * // reset the value to some known value:
86 * spinner.setValue(fallbackValue);
87 * // or treat the last valid value as the current, in which
88 * // case you don't need to do anything.
89 * }
90 * return spinner.getValue();
91 * </pre>
92 * <p>
93 * For information and examples of using spinner see
94 * <a href="http://java.sun.com/doc/books/tutorial/uiswing/components/spinner.html">How to Use Spinners</a>,
95 * a section in <em>The Java Tutorial.</em>
96 * <p>
97 * <strong>Warning:</strong> Swing is not thread safe. For more
98 * information see <a
99 * href="package-summary.html#threading">Swing's Threading
100 * Policy</a>.
101 * <p>
102 * <strong>Warning:</strong>
103 * Serialized objects of this class will not be compatible with
104 * future Swing releases. The current serialization support is
105 * appropriate for short term storage or RMI between applications running
106 * the same version of Swing. As of 1.4, support for long term storage
107 * of all JavaBeans<sup><font size="-2">TM</font></sup>
108 * has been added to the <code>java.beans</code> package.
109 * Please see {@link java.beans.XMLEncoder}.
110 *
111 * @beaninfo
112 * attribute: isContainer false
113 * description: A single line input field that lets the user select a
114 * number or an object value from an ordered set.
115 *
116 * @see SpinnerModel
117 * @see AbstractSpinnerModel
118 * @see SpinnerListModel
119 * @see SpinnerNumberModel
120 * @see SpinnerDateModel
121 * @see JFormattedTextField
122 *
123 * @author Hans Muller
124 * @author Lynn Monsanto (accessibility)
125 * @since 1.4
126 */
127 public class JSpinner extends JComponent implements Accessible
128 {
129 /**
130 * @see #getUIClassID
131 * @see #readObject
132 */
133 private static final String uiClassID = "SpinnerUI";
134
135 private static final Action DISABLED_ACTION = new DisabledAction();
136
137 private SpinnerModel model;
138 private JComponent editor;
139 private ChangeListener modelListener;
140 private transient ChangeEvent changeEvent;
141 private boolean editorExplicitlySet = false;
142
143
144 /**
145 * Constructs a spinner for the given model. The spinner has
146 * a set of previous/next buttons, and an editor appropriate
147 * for the model.
148 *
149 * @throws NullPointerException if the model is {@code null}
150 */
151 public JSpinner(SpinnerModel model) {
152 if (model == null) {
153 throw new NullPointerException("model cannot be null");
154 }
155 this.model = model;
156 this.editor = createEditor(model);
157 setOpaque(true);
158 updateUI();
159 }
160
161
162 /**
163 * Constructs a spinner with an <code>Integer SpinnerNumberModel</code>
164 * with initial value 0 and no minimum or maximum limits.
165 */
166 public JSpinner() {
167 this(new SpinnerNumberModel());
168 }
169
170
171 /**
172 * Returns the look and feel (L&F) object that renders this component.
173 *
174 * @return the <code>SpinnerUI</code> object that renders this component
175 */
176 public SpinnerUI getUI() {
177 return (SpinnerUI)ui;
178 }
179
180
181 /**
182 * Sets the look and feel (L&F) object that renders this component.
183 *
184 * @param ui the <code>SpinnerUI</code> L&F object
185 * @see UIDefaults#getUI
186 */
187 public void setUI(SpinnerUI ui) {
188 super.setUI(ui);
189 }
190
191
192 /**
193 * Returns the suffix used to construct the name of the look and feel
194 * (L&F) class used to render this component.
195 *
196 * @return the string "SpinnerUI"
197 * @see JComponent#getUIClassID
198 * @see UIDefaults#getUI
199 */
200 public String getUIClassID() {
201 return uiClassID;
202 }
203
204
205
206 /**
207 * Resets the UI property with the value from the current look and feel.
208 *
209 * @see UIManager#getUI
210 */
211 public void updateUI() {
212 setUI((SpinnerUI)UIManager.getUI(this));
213 invalidate();
214 }
215
216
217 /**
218 * This method is called by the constructors to create the
219 * <code>JComponent</code>
220 * that displays the current value of the sequence. The editor may
221 * also allow the user to enter an element of the sequence directly.
222 * An editor must listen for <code>ChangeEvents</code> on the
223 * <code>model</code> and keep the value it displays
224 * in sync with the value of the model.
225 * <p>
226 * Subclasses may override this method to add support for new
227 * <code>SpinnerModel</code> classes. Alternatively one can just
228 * replace the editor created here with the <code>setEditor</code>
229 * method. The default mapping from model type to editor is:
230 * <ul>
231 * <li> <code>SpinnerNumberModel => JSpinner.NumberEditor</code>
232 * <li> <code>SpinnerDateModel => JSpinner.DateEditor</code>
233 * <li> <code>SpinnerListModel => JSpinner.ListEditor</code>
234 * <li> <i>all others</i> => <code>JSpinner.DefaultEditor</code>
235 * </ul>
236 *
237 * @return a component that displays the current value of the sequence
238 * @param model the value of getModel
239 * @see #getModel
240 * @see #setEditor
241 */
242 protected JComponent createEditor(SpinnerModel model) {
243 if (model instanceof SpinnerDateModel) {
244 return new DateEditor(this);
245 }
246 else if (model instanceof SpinnerListModel) {
247 return new ListEditor(this);
248 }
249 else if (model instanceof SpinnerNumberModel) {
250 return new NumberEditor(this);
251 }
252 else {
253 return new DefaultEditor(this);
254 }
255 }
256
257
258 /**
259 * Changes the model that represents the value of this spinner.
260 * If the editor property has not been explicitly set,
261 * the editor property is (implicitly) set after the <code>"model"</code>
262 * <code>PropertyChangeEvent</code> has been fired. The editor
263 * property is set to the value returned by <code>createEditor</code>,
264 * as in:
265 * <pre>
266 * setEditor(createEditor(model));
267 * </pre>
268 *
269 * @param model the new <code>SpinnerModel</code>
270 * @see #getModel
271 * @see #getEditor
272 * @see #setEditor
273 * @throws IllegalArgumentException if model is <code>null</code>
274 *
275 * @beaninfo
276 * bound: true
277 * attribute: visualUpdate true
278 * description: Model that represents the value of this spinner.
279 */
280 public void setModel(SpinnerModel model) {
281 if (model == null) {
282 throw new IllegalArgumentException("null model");
283 }
284 if (!model.equals(this.model)) {
285 SpinnerModel oldModel = this.model;
286 this.model = model;
287 if (modelListener != null) {
288 oldModel.removeChangeListener(modelListener);
289 this.model.addChangeListener(modelListener);
290 }
291 firePropertyChange("model", oldModel, model);
292 if (!editorExplicitlySet) {
293 setEditor(createEditor(model)); // sets editorExplicitlySet true
294 editorExplicitlySet = false;
295 }
296 repaint();
297 revalidate();
298 }
299 }
300
301
302 /**
303 * Returns the <code>SpinnerModel</code> that defines
304 * this spinners sequence of values.
305 *
306 * @return the value of the model property
307 * @see #setModel
308 */
309 public SpinnerModel getModel() {
310 return model;
311 }
312
313
314 /**
315 * Returns the current value of the model, typically
316 * this value is displayed by the <code>editor</code>. If the
317 * user has changed the value displayed by the <code>editor</code> it is
318 * possible for the <code>model</code>'s value to differ from that of
319 * the <code>editor</code>, refer to the class level javadoc for examples
320 * of how to deal with this.
321 * <p>
322 * This method simply delegates to the <code>model</code>.
323 * It is equivalent to:
324 * <pre>
325 * getModel().getValue()
326 * </pre>
327 *
328 * @see #setValue
329 * @see SpinnerModel#getValue
330 */
331 public Object getValue() {
332 return getModel().getValue();
333 }
334
335
336 /**
337 * Changes current value of the model, typically
338 * this value is displayed by the <code>editor</code>.
339 * If the <code>SpinnerModel</code> implementation
340 * doesn't support the specified value then an
341 * <code>IllegalArgumentException</code> is thrown.
342 * <p>
343 * This method simply delegates to the <code>model</code>.
344 * It is equivalent to:
345 * <pre>
346 * getModel().setValue(value)
347 * </pre>
348 *
349 * @throws IllegalArgumentException if <code>value</code> isn't allowed
350 * @see #getValue
351 * @see SpinnerModel#setValue
352 */
353 public void setValue(Object value) {
354 getModel().setValue(value);
355 }
356
357
358 /**
359 * Returns the object in the sequence that comes after the object returned
360 * by <code>getValue()</code>. If the end of the sequence has been reached
361 * then return <code>null</code>.
362 * Calling this method does not effect <code>value</code>.
363 * <p>
364 * This method simply delegates to the <code>model</code>.
365 * It is equivalent to:
366 * <pre>
367 * getModel().getNextValue()
368 * </pre>
369 *
370 * @return the next legal value or <code>null</code> if one doesn't exist
371 * @see #getValue
372 * @see #getPreviousValue
373 * @see SpinnerModel#getNextValue
374 */
375 public Object getNextValue() {
376 return getModel().getNextValue();
377 }
378
379
380 /**
381 * We pass <code>Change</code> events along to the listeners with the
382 * the slider (instead of the model itself) as the event source.
383 */
384 private class ModelListener implements ChangeListener, Serializable {
385 public void stateChanged(ChangeEvent e) {
386 fireStateChanged();
387 }
388 }
389
390
391 /**
392 * Adds a listener to the list that is notified each time a change
393 * to the model occurs. The source of <code>ChangeEvents</code>
394 * delivered to <code>ChangeListeners</code> will be this
395 * <code>JSpinner</code>. Note also that replacing the model
396 * will not affect listeners added directly to JSpinner.
397 * Applications can add listeners to the model directly. In that
398 * case is that the source of the event would be the
399 * <code>SpinnerModel</code>.
400 *
401 * @param listener the <code>ChangeListener</code> to add
402 * @see #removeChangeListener
403 * @see #getModel
404 */
405 public void addChangeListener(ChangeListener listener) {
406 if (modelListener == null) {
407 modelListener = new ModelListener();
408 getModel().addChangeListener(modelListener);
409 }
410 listenerList.add(ChangeListener.class, listener);
411 }
412
413
414
415 /**
416 * Removes a <code>ChangeListener</code> from this spinner.
417 *
418 * @param listener the <code>ChangeListener</code> to remove
419 * @see #fireStateChanged
420 * @see #addChangeListener
421 */
422 public void removeChangeListener(ChangeListener listener) {
423 listenerList.remove(ChangeListener.class, listener);
424 }
425
426
427 /**
428 * Returns an array of all the <code>ChangeListener</code>s added
429 * to this JSpinner with addChangeListener().
430 *
431 * @return all of the <code>ChangeListener</code>s added or an empty
432 * array if no listeners have been added
433 * @since 1.4
434 */
435 public ChangeListener[] getChangeListeners() {
436 return (ChangeListener[])listenerList.getListeners(
437 ChangeListener.class);
438 }
439
440
441 /**
442 * Sends a <code>ChangeEvent</code>, whose source is this
443 * <code>JSpinner</code>, to each <code>ChangeListener</code>.
444 * When a <code>ChangeListener</code> has been added
445 * to the spinner, this method method is called each time
446 * a <code>ChangeEvent</code> is received from the model.
447 *
448 * @see #addChangeListener
449 * @see #removeChangeListener
450 * @see EventListenerList
451 */
452 protected void fireStateChanged() {
453 Object[] listeners = listenerList.getListenerList();
454 for (int i = listeners.length - 2; i >= 0; i -= 2) {
455 if (listeners[i] == ChangeListener.class) {
456 if (changeEvent == null) {
457 changeEvent = new ChangeEvent(this);
458 }
459 ((ChangeListener)listeners[i+1]).stateChanged(changeEvent);
460 }
461 }
462 }
463
464
465 /**
466 * Returns the object in the sequence that comes
467 * before the object returned by <code>getValue()</code>.
468 * If the end of the sequence has been reached then
469 * return <code>null</code>. Calling this method does
470 * not effect <code>value</code>.
471 * <p>
472 * This method simply delegates to the <code>model</code>.
473 * It is equivalent to:
474 * <pre>
475 * getModel().getPreviousValue()
476 * </pre>
477 *
478 * @return the previous legal value or <code>null</code>
479 * if one doesn't exist
480 * @see #getValue
481 * @see #getNextValue
482 * @see SpinnerModel#getPreviousValue
483 */
484 public Object getPreviousValue() {
485 return getModel().getPreviousValue();
486 }
487
488
489 /**
490 * Changes the <code>JComponent</code> that displays the current value
491 * of the <code>SpinnerModel</code>. It is the responsibility of this
492 * method to <i>disconnect</i> the old editor from the model and to
493 * connect the new editor. This may mean removing the
494 * old editors <code>ChangeListener</code> from the model or the
495 * spinner itself and adding one for the new editor.
496 *
497 * @param editor the new editor
498 * @see #getEditor
499 * @see #createEditor
500 * @see #getModel
501 * @throws IllegalArgumentException if editor is <code>null</code>
502 *
503 * @beaninfo
504 * bound: true
505 * attribute: visualUpdate true
506 * description: JComponent that displays the current value of the model
507 */
508 public void setEditor(JComponent editor) {
509 if (editor == null) {
510 throw new IllegalArgumentException("null editor");
511 }
512 if (!editor.equals(this.editor)) {
513 JComponent oldEditor = this.editor;
514 this.editor = editor;
515 if (oldEditor instanceof DefaultEditor) {
516 ((DefaultEditor)oldEditor).dismiss(this);
517 }
518 editorExplicitlySet = true;
519 firePropertyChange("editor", oldEditor, editor);
520 revalidate();
521 repaint();
522 }
523 }
524
525
526 /**
527 * Returns the component that displays and potentially
528 * changes the model's value.
529 *
530 * @return the component that displays and potentially
531 * changes the model's value
532 * @see #setEditor
533 * @see #createEditor
534 */
535 public JComponent getEditor() {
536 return editor;
537 }
538
539
540 /**
541 * Commits the currently edited value to the <code>SpinnerModel</code>.
542 * <p>
543 * If the editor is an instance of <code>DefaultEditor</code>, the
544 * call if forwarded to the editor, otherwise this does nothing.
545 *
546 * @throws ParseException if the currently edited value couldn't
547 * be commited.
548 */
549 public void commitEdit() throws ParseException {
550 JComponent editor = getEditor();
551 if (editor instanceof DefaultEditor) {
552 ((DefaultEditor)editor).commitEdit();
553 }
554 }
555
556
557 /*
558 * See readObject and writeObject in JComponent for more
559 * information about serialization in Swing.
560 *
561 * @param s Stream to write to
562 */
563 private void writeObject(ObjectOutputStream s) throws IOException {
564 s.defaultWriteObject();
565 if (getUIClassID().equals(uiClassID)) {
566 byte count = JComponent.getWriteObjCounter(this);
567 JComponent.setWriteObjCounter(this, --count);
568 if (count == 0 && ui != null) {
569 ui.installUI(this);
570 }
571 }
572 }
573
574
575 /**
576 * A simple base class for more specialized editors
577 * that displays a read-only view of the model's current
578 * value with a <code>JFormattedTextField</code>. Subclasses
579 * can configure the <code>JFormattedTextField</code> to create
580 * an editor that's appropriate for the type of model they
581 * support and they may want to override
582 * the <code>stateChanged</code> and <code>propertyChanged</code>
583 * methods, which keep the model and the text field in sync.
584 * <p>
585 * This class defines a <code>dismiss</code> method that removes the
586 * editors <code>ChangeListener</code> from the <code>JSpinner</code>
587 * that it's part of. The <code>setEditor</code> method knows about
588 * <code>DefaultEditor.dismiss</code>, so if the developer
589 * replaces an editor that's derived from <code>JSpinner.DefaultEditor</code>
590 * its <code>ChangeListener</code> connection back to the
591 * <code>JSpinner</code> will be removed. However after that,
592 * it's up to the developer to manage their editor listeners.
593 * Similarly, if a subclass overrides <code>createEditor</code>,
594 * it's up to the subclasser to deal with their editor
595 * subsequently being replaced (with <code>setEditor</code>).
596 * We expect that in most cases, and in editor installed
597 * with <code>setEditor</code> or created by a <code>createEditor</code>
598 * override, will not be replaced anyway.
599 * <p>
600 * This class is the <code>LayoutManager</code> for it's single
601 * <code>JFormattedTextField</code> child. By default the
602 * child is just centered with the parents insets.
603 * @since 1.4
604 */
605 public static class DefaultEditor extends JPanel
606 implements ChangeListener, PropertyChangeListener, LayoutManager
607 {
608 /**
609 * Constructs an editor component for the specified <code>JSpinner</code>.
610 * This <code>DefaultEditor</code> is it's own layout manager and
611 * it is added to the spinner's <code>ChangeListener</code> list.
612 * The constructor creates a single <code>JFormattedTextField</code> child,
613 * initializes it's value to be the spinner model's current value
614 * and adds it to <code>this</code> <code>DefaultEditor</code>.
615 *
616 * @param spinner the spinner whose model <code>this</code> editor will monitor
617 * @see #getTextField
618 * @see JSpinner#addChangeListener
619 */
620 public DefaultEditor(JSpinner spinner) {
621 super(null);
622
623 JFormattedTextField ftf = new JFormattedTextField();
624 ftf.setName("Spinner.formattedTextField");
625 ftf.setValue(spinner.getValue());
626 ftf.addPropertyChangeListener(this);
627 ftf.setEditable(false);
628 ftf.setInheritsPopupMenu(true);
629
630 String toolTipText = spinner.getToolTipText();
631 if (toolTipText != null) {
632 ftf.setToolTipText(toolTipText);
633 }
634
635 add(ftf);
636
637 setLayout(this);
638 spinner.addChangeListener(this);
639
640 // We want the spinner's increment/decrement actions to be
641 // active vs those of the JFormattedTextField. As such we
642 // put disabled actions in the JFormattedTextField's actionmap.
643 // A binding to a disabled action is treated as a nonexistant
644 // binding.
645 ActionMap ftfMap = ftf.getActionMap();
646
647 if (ftfMap != null) {
648 ftfMap.put("increment", DISABLED_ACTION);
649 ftfMap.put("decrement", DISABLED_ACTION);
650 }
651 }
652
653
654 /**
655 * Disconnect <code>this</code> editor from the specified
656 * <code>JSpinner</code>. By default, this method removes
657 * itself from the spinners <code>ChangeListener</code> list.
658 *
659 * @param spinner the <code>JSpinner</code> to disconnect this
660 * editor from; the same spinner as was passed to the constructor.
661 */
662 public void dismiss(JSpinner spinner) {
663 spinner.removeChangeListener(this);
664 }
665
666
667 /**
668 * Returns the <code>JSpinner</code> ancestor of this editor or
669 * <code>null</code> if none of the ancestors are a
670 * <code>JSpinner</code>.
671 * Typically the editor's parent is a <code>JSpinner</code> however
672 * subclasses of <code>JSpinner</code> may override the
673 * the <code>createEditor</code> method and insert one or more containers
674 * between the <code>JSpinner</code> and it's editor.
675 *
676 * @return <code>JSpinner</code> ancestor; <code>null</code>
677 * if none of the ancestors are a <code>JSpinner</code>
678 *
679 * @see JSpinner#createEditor
680 */
681 public JSpinner getSpinner() {
682 for (Component c = this; c != null; c = c.getParent()) {
683 if (c instanceof JSpinner) {
684 return (JSpinner)c;
685 }
686 }
687 return null;
688 }
689
690
691 /**
692 * Returns the <code>JFormattedTextField</code> child of this
693 * editor. By default the text field is the first and only
694 * child of editor.
695 *
696 * @return the <code>JFormattedTextField</code> that gives the user
697 * access to the <code>SpinnerDateModel's</code> value.
698 * @see #getSpinner
699 * @see #getModel
700 */
701 public JFormattedTextField getTextField() {
702 return (JFormattedTextField)getComponent(0);
703 }
704
705
706 /**
707 * This method is called when the spinner's model's state changes.
708 * It sets the <code>value</code> of the text field to the current
709 * value of the spinners model.
710 *
711 * @param e the <code>ChangeEvent</code> whose source is the
712 * <code>JSpinner</code> whose model has changed.
713 * @see #getTextField
714 * @see JSpinner#getValue
715 */
716 public void stateChanged(ChangeEvent e) {
717 JSpinner spinner = (JSpinner)(e.getSource());
718 getTextField().setValue(spinner.getValue());
719 }
720
721
722 /**
723 * Called by the <code>JFormattedTextField</code>
724 * <code>PropertyChangeListener</code>. When the <code>"value"</code>
725 * property changes, which implies that the user has typed a new
726 * number, we set the value of the spinners model.
727 * <p>
728 * This class ignores <code>PropertyChangeEvents</code> whose
729 * source is not the <code>JFormattedTextField</code>, so subclasses
730 * may safely make <code>this</code> <code>DefaultEditor</code> a
731 * <code>PropertyChangeListener</code> on other objects.
732 *
733 * @param e the <code>PropertyChangeEvent</code> whose source is
734 * the <code>JFormattedTextField</code> created by this class.
735 * @see #getTextField
736 */
737 public void propertyChange(PropertyChangeEvent e)
738 {
739 JSpinner spinner = getSpinner();
740
741 if (spinner == null) {
742 // Indicates we aren't installed anywhere.
743 return;
744 }
745
746 Object source = e.getSource();
747 String name = e.getPropertyName();
748 if ((source instanceof JFormattedTextField) && "value".equals(name)) {
749 Object lastValue = spinner.getValue();
750
751 // Try to set the new value
752 try {
753 spinner.setValue(getTextField().getValue());
754 } catch (IllegalArgumentException iae) {
755 // SpinnerModel didn't like new value, reset
756 try {
757 ((JFormattedTextField)source).setValue(lastValue);
758 } catch (IllegalArgumentException iae2) {
759 // Still bogus, nothing else we can do, the
760 // SpinnerModel and JFormattedTextField are now out
761 // of sync.
762 }
763 }
764 }
765 }
766
767
768 /**
769 * This <code>LayoutManager</code> method does nothing. We're
770 * only managing a single child and there's no support
771 * for layout constraints.
772 *
773 * @param name ignored
774 * @param child ignored
775 */
776 public void addLayoutComponent(String name, Component child) {
777 }
778
779
780 /**
781 * This <code>LayoutManager</code> method does nothing. There
782 * isn't any per-child state.
783 *
784 * @param child ignored
785 */
786 public void removeLayoutComponent(Component child) {
787 }
788
789
790 /**
791 * Returns the size of the parents insets.
792 */
793 private Dimension insetSize(Container parent) {
794 Insets insets = parent.getInsets();
795 int w = insets.left + insets.right;
796 int h = insets.top + insets.bottom;
797 return new Dimension(w, h);
798 }
799
800
801 /**
802 * Returns the preferred size of first (and only) child plus the
803 * size of the parents insets.
804 *
805 * @param parent the Container that's managing the layout
806 * @return the preferred dimensions to lay out the subcomponents
807 * of the specified container.
808 */
809 public Dimension preferredLayoutSize(Container parent) {
810 Dimension preferredSize = insetSize(parent);
811 if (parent.getComponentCount() > 0) {
812 Dimension childSize = getComponent(0).getPreferredSize();
813 preferredSize.width += childSize.width;
814 preferredSize.height += childSize.height;
815 }
816 return preferredSize;
817 }
818
819
820 /**
821 * Returns the minimum size of first (and only) child plus the
822 * size of the parents insets.
823 *
824 * @param parent the Container that's managing the layout
825 * @return the minimum dimensions needed to lay out the subcomponents
826 * of the specified container.
827 */
828 public Dimension minimumLayoutSize(Container parent) {
829 Dimension minimumSize = insetSize(parent);
830 if (parent.getComponentCount() > 0) {
831 Dimension childSize = getComponent(0).getMinimumSize();
832 minimumSize.width += childSize.width;
833 minimumSize.height += childSize.height;
834 }
835 return minimumSize;
836 }
837
838
839 /**
840 * Resize the one (and only) child to completely fill the area
841 * within the parents insets.
842 */
843 public void layoutContainer(Container parent) {
844 if (parent.getComponentCount() > 0) {
845 Insets insets = parent.getInsets();
846 int w = parent.getWidth() - (insets.left + insets.right);
847 int h = parent.getHeight() - (insets.top + insets.bottom);
848 getComponent(0).setBounds(insets.left, insets.top, w, h);
849 }
850 }
851
852 /**
853 * Pushes the currently edited value to the <code>SpinnerModel</code>.
854 * <p>
855 * The default implementation invokes <code>commitEdit</code> on the
856 * <code>JFormattedTextField</code>.
857 *
858 * @throws ParseException if the edited value is not legal
859 */
860 public void commitEdit() throws ParseException {
861 // If the value in the JFormattedTextField is legal, this will have
862 // the result of pushing the value to the SpinnerModel
863 // by way of the <code>propertyChange</code> method.
864 JFormattedTextField ftf = getTextField();
865
866 ftf.commitEdit();
867 }
868
869 /**
870 * Returns the baseline.
871 *
872 * @throws IllegalArgumentException {@inheritDoc}
873 * @see javax.swing.JComponent#getBaseline(int,int)
874 * @see javax.swing.JComponent#getBaselineResizeBehavior()
875 * @since 1.6
876 */
877 public int getBaseline(int width, int height) {
878 // check size.
879 super.getBaseline(width, height);
880 Insets insets = getInsets();
881 width = width - insets.left - insets.right;
882 height = height - insets.top - insets.bottom;
883 int baseline = getComponent(0).getBaseline(width, height);
884 if (baseline >= 0) {
885 return baseline + insets.top;
886 }
887 return -1;
888 }
889
890 /**
891 * Returns an enum indicating how the baseline of the component
892 * changes as the size changes.
893 *
894 * @throws NullPointerException {@inheritDoc}
895 * @see javax.swing.JComponent#getBaseline(int, int)
896 * @since 1.6
897 */
898 public BaselineResizeBehavior getBaselineResizeBehavior() {
899 return getComponent(0).getBaselineResizeBehavior();
900 }
901 }
902
903
904
905
906 /**
907 * This subclass of javax.swing.DateFormatter maps the minimum/maximum
908 * properties to te start/end properties of a SpinnerDateModel.
909 */
910 private static class DateEditorFormatter extends DateFormatter {
911 private final SpinnerDateModel model;
912
913 DateEditorFormatter(SpinnerDateModel model, DateFormat format) {
914 super(format);
915 this.model = model;
916 }
917
918 public void setMinimum(Comparable min) {
919 model.setStart(min);
920 }
921
922 public Comparable getMinimum() {
923 return model.getStart();
924 }
925
926 public void setMaximum(Comparable max) {
927 model.setEnd(max);
928 }
929
930 public Comparable getMaximum() {
931 return model.getEnd();
932 }
933 }
934
935
936 /**
937 * An editor for a <code>JSpinner</code> whose model is a
938 * <code>SpinnerDateModel</code>. The value of the editor is
939 * displayed with a <code>JFormattedTextField</code> whose format
940 * is defined by a <code>DateFormatter</code> instance whose
941 * <code>minimum</code> and <code>maximum</code> properties
942 * are mapped to the <code>SpinnerDateModel</code>.
943 * @since 1.4
944 */
945 // PENDING(hmuller): more example javadoc
946 public static class DateEditor extends DefaultEditor
947 {
948 // This is here until SimpleDateFormat gets a constructor that
949 // takes a Locale: 4923525
950 private static String getDefaultPattern(Locale loc) {
951 ResourceBundle r = LocaleData.getDateFormatData(loc);
952 String[] dateTimePatterns = r.getStringArray("DateTimePatterns");
953 Object[] dateTimeArgs = {dateTimePatterns[DateFormat.SHORT],
954 dateTimePatterns[DateFormat.SHORT + 4]};
955 return MessageFormat.format(dateTimePatterns[8], dateTimeArgs);
956 }
957
958 /**
959 * Construct a <code>JSpinner</code> editor that supports displaying
960 * and editing the value of a <code>SpinnerDateModel</code>
961 * with a <code>JFormattedTextField</code>. <code>This</code>
962 * <code>DateEditor</code> becomes both a <code>ChangeListener</code>
963 * on the spinners model and a <code>PropertyChangeListener</code>
964 * on the new <code>JFormattedTextField</code>.
965 *
966 * @param spinner the spinner whose model <code>this</code> editor will monitor
967 * @exception IllegalArgumentException if the spinners model is not
968 * an instance of <code>SpinnerDateModel</code>
969 *
970 * @see #getModel
971 * @see #getFormat
972 * @see SpinnerDateModel
973 */
974 public DateEditor(JSpinner spinner) {
975 this(spinner, getDefaultPattern(spinner.getLocale()));
976 }
977
978
979 /**
980 * Construct a <code>JSpinner</code> editor that supports displaying
981 * and editing the value of a <code>SpinnerDateModel</code>
982 * with a <code>JFormattedTextField</code>. <code>This</code>
983 * <code>DateEditor</code> becomes both a <code>ChangeListener</code>
984 * on the spinner and a <code>PropertyChangeListener</code>
985 * on the new <code>JFormattedTextField</code>.
986 *
987 * @param spinner the spinner whose model <code>this</code> editor will monitor
988 * @param dateFormatPattern the initial pattern for the
989 * <code>SimpleDateFormat</code> object that's used to display
990 * and parse the value of the text field.
991 * @exception IllegalArgumentException if the spinners model is not
992 * an instance of <code>SpinnerDateModel</code>
993 *
994 * @see #getModel
995 * @see #getFormat
996 * @see SpinnerDateModel
997 * @see java.text.SimpleDateFormat
998 */
999 public DateEditor(JSpinner spinner, String dateFormatPattern) {
1000 this(spinner, new SimpleDateFormat(dateFormatPattern,
1001 spinner.getLocale()));
1002 }
1003
1004 /**
1005 * Construct a <code>JSpinner</code> editor that supports displaying
1006 * and editing the value of a <code>SpinnerDateModel</code>
1007 * with a <code>JFormattedTextField</code>. <code>This</code>
1008 * <code>DateEditor</code> becomes both a <code>ChangeListener</code>
1009 * on the spinner and a <code>PropertyChangeListener</code>
1010 * on the new <code>JFormattedTextField</code>.
1011 *
1012 * @param spinner the spinner whose model <code>this</code> editor
1013 * will monitor
1014 * @param format <code>DateFormat</code> object that's used to display
1015 * and parse the value of the text field.
1016 * @exception IllegalArgumentException if the spinners model is not
1017 * an instance of <code>SpinnerDateModel</code>
1018 *
1019 * @see #getModel
1020 * @see #getFormat
1021 * @see SpinnerDateModel
1022 * @see java.text.SimpleDateFormat
1023 */
1024 private DateEditor(JSpinner spinner, DateFormat format) {
1025 super(spinner);
1026 if (!(spinner.getModel() instanceof SpinnerDateModel)) {
1027 throw new IllegalArgumentException(
1028 "model not a SpinnerDateModel");
1029 }
1030
1031 SpinnerDateModel model = (SpinnerDateModel)spinner.getModel();
1032 DateFormatter formatter = new DateEditorFormatter(model, format);
1033 DefaultFormatterFactory factory = new DefaultFormatterFactory(
1034 formatter);
1035 JFormattedTextField ftf = getTextField();
1036 ftf.setEditable(true);
1037 ftf.setFormatterFactory(factory);
1038
1039 /* TBD - initializing the column width of the text field
1040 * is imprecise and doing it here is tricky because
1041 * the developer may configure the formatter later.
1042 */
1043 try {
1044 String maxString = formatter.valueToString(model.getStart());
1045 String minString = formatter.valueToString(model.getEnd());
1046 ftf.setColumns(Math.max(maxString.length(),
1047 minString.length()));
1048 }
1049 catch (ParseException e) {
1050 // PENDING: hmuller
1051 }
1052 }
1053
1054 /**
1055 * Returns the <code>java.text.SimpleDateFormat</code> object the
1056 * <code>JFormattedTextField</code> uses to parse and format
1057 * numbers.
1058 *
1059 * @return the value of <code>getTextField().getFormatter().getFormat()</code>.
1060 * @see #getTextField
1061 * @see java.text.SimpleDateFormat
1062 */
1063 public SimpleDateFormat getFormat() {
1064 return (SimpleDateFormat)((DateFormatter)(getTextField().getFormatter())).getFormat();
1065 }
1066
1067
1068 /**
1069 * Return our spinner ancestor's <code>SpinnerDateModel</code>.
1070 *
1071 * @return <code>getSpinner().getModel()</code>
1072 * @see #getSpinner
1073 * @see #getTextField
1074 */
1075 public SpinnerDateModel getModel() {
1076 return (SpinnerDateModel)(getSpinner().getModel());
1077 }
1078 }
1079
1080
1081 /**
1082 * This subclass of javax.swing.NumberFormatter maps the minimum/maximum
1083 * properties to a SpinnerNumberModel and initializes the valueClass
1084 * of the NumberFormatter to match the type of the initial models value.
1085 */
1086 private static class NumberEditorFormatter extends NumberFormatter {
1087 private final SpinnerNumberModel model;
1088
1089 NumberEditorFormatter(SpinnerNumberModel model, NumberFormat format) {
1090 super(format);
1091 this.model = model;
1092 setValueClass(model.getValue().getClass());
1093 }
1094
1095 public void setMinimum(Comparable min) {
1096 model.setMinimum(min);
1097 }
1098
1099 public Comparable getMinimum() {
1100 return model.getMinimum();
1101 }
1102
1103 public void setMaximum(Comparable max) {
1104 model.setMaximum(max);
1105 }
1106
1107 public Comparable getMaximum() {
1108 return model.getMaximum();
1109 }
1110 }
1111
1112
1113
1114 /**
1115 * An editor for a <code>JSpinner</code> whose model is a
1116 * <code>SpinnerNumberModel</code>. The value of the editor is
1117 * displayed with a <code>JFormattedTextField</code> whose format
1118 * is defined by a <code>NumberFormatter</code> instance whose
1119 * <code>minimum</code> and <code>maximum</code> properties
1120 * are mapped to the <code>SpinnerNumberModel</code>.
1121 * @since 1.4
1122 */
1123 // PENDING(hmuller): more example javadoc
1124 public static class NumberEditor extends DefaultEditor
1125 {
1126 // This is here until DecimalFormat gets a constructor that
1127 // takes a Locale: 4923525
1128 private static String getDefaultPattern(Locale locale) {
1129 // Get the pattern for the default locale.
1130 ResourceBundle rb = LocaleData.getNumberFormatData(locale);
1131 String[] all = rb.getStringArray("NumberPatterns");
1132 return all[0];
1133 }
1134
1135 /**
1136 * Construct a <code>JSpinner</code> editor that supports displaying
1137 * and editing the value of a <code>SpinnerNumberModel</code>
1138 * with a <code>JFormattedTextField</code>. <code>This</code>
1139 * <code>NumberEditor</code> becomes both a <code>ChangeListener</code>
1140 * on the spinner and a <code>PropertyChangeListener</code>
1141 * on the new <code>JFormattedTextField</code>.
1142 *
1143 * @param spinner the spinner whose model <code>this</code> editor will monitor
1144 * @exception IllegalArgumentException if the spinners model is not
1145 * an instance of <code>SpinnerNumberModel</code>
1146 *
1147 * @see #getModel
1148 * @see #getFormat
1149 * @see SpinnerNumberModel
1150 */
1151 public NumberEditor(JSpinner spinner) {
1152 this(spinner, getDefaultPattern(spinner.getLocale()));
1153 }
1154
1155 /**
1156 * Construct a <code>JSpinner</code> editor that supports displaying
1157 * and editing the value of a <code>SpinnerNumberModel</code>
1158 * with a <code>JFormattedTextField</code>. <code>This</code>
1159 * <code>NumberEditor</code> becomes both a <code>ChangeListener</code>
1160 * on the spinner and a <code>PropertyChangeListener</code>
1161 * on the new <code>JFormattedTextField</code>.
1162 *
1163 * @param spinner the spinner whose model <code>this</code> editor will monitor
1164 * @param decimalFormatPattern the initial pattern for the
1165 * <code>DecimalFormat</code> object that's used to display
1166 * and parse the value of the text field.
1167 * @exception IllegalArgumentException if the spinners model is not
1168 * an instance of <code>SpinnerNumberModel</code> or if
1169 * <code>decimalFormatPattern</code> is not a legal
1170 * argument to <code>DecimalFormat</code>
1171 *
1172 * @see #getTextField
1173 * @see SpinnerNumberModel
1174 * @see java.text.DecimalFormat
1175 */
1176 public NumberEditor(JSpinner spinner, String decimalFormatPattern) {
1177 this(spinner, new DecimalFormat(decimalFormatPattern));
1178 }
1179
1180
1181 /**
1182 * Construct a <code>JSpinner</code> editor that supports displaying
1183 * and editing the value of a <code>SpinnerNumberModel</code>
1184 * with a <code>JFormattedTextField</code>. <code>This</code>
1185 * <code>NumberEditor</code> becomes both a <code>ChangeListener</code>
1186 * on the spinner and a <code>PropertyChangeListener</code>
1187 * on the new <code>JFormattedTextField</code>.
1188 *
1189 * @param spinner the spinner whose model <code>this</code> editor will monitor
1190 * @param decimalFormatPattern the initial pattern for the
1191 * <code>DecimalFormat</code> object that's used to display
1192 * and parse the value of the text field.
1193 * @exception IllegalArgumentException if the spinners model is not
1194 * an instance of <code>SpinnerNumberModel</code>
1195 *
1196 * @see #getTextField
1197 * @see SpinnerNumberModel
1198 * @see java.text.DecimalFormat
1199 */
1200 private NumberEditor(JSpinner spinner, DecimalFormat format) {
1201 super(spinner);
1202 if (!(spinner.getModel() instanceof SpinnerNumberModel)) {
1203 throw new IllegalArgumentException(
1204 "model not a SpinnerNumberModel");
1205 }
1206
1207 SpinnerNumberModel model = (SpinnerNumberModel)spinner.getModel();
1208 NumberFormatter formatter = new NumberEditorFormatter(model,
1209 format);
1210 DefaultFormatterFactory factory = new DefaultFormatterFactory(
1211 formatter);
1212 JFormattedTextField ftf = getTextField();
1213 ftf.setEditable(true);
1214 ftf.setFormatterFactory(factory);
1215 ftf.setHorizontalAlignment(JTextField.RIGHT);
1216
1217 /* TBD - initializing the column width of the text field
1218 * is imprecise and doing it here is tricky because
1219 * the developer may configure the formatter later.
1220 */
1221 try {
1222 String maxString = formatter.valueToString(model.getMinimum());
1223 String minString = formatter.valueToString(model.getMaximum());
1224 ftf.setColumns(Math.max(maxString.length(),
1225 minString.length()));
1226 }
1227 catch (ParseException e) {
1228 // TBD should throw a chained error here
1229 }
1230
1231 }
1232
1233
1234 /**
1235 * Returns the <code>java.text.DecimalFormat</code> object the
1236 * <code>JFormattedTextField</code> uses to parse and format
1237 * numbers.
1238 *
1239 * @return the value of <code>getTextField().getFormatter().getFormat()</code>.
1240 * @see #getTextField
1241 * @see java.text.DecimalFormat
1242 */
1243 public DecimalFormat getFormat() {
1244 return (DecimalFormat)((NumberFormatter)(getTextField().getFormatter())).getFormat();
1245 }
1246
1247
1248 /**
1249 * Return our spinner ancestor's <code>SpinnerNumberModel</code>.
1250 *
1251 * @return <code>getSpinner().getModel()</code>
1252 * @see #getSpinner
1253 * @see #getTextField
1254 */
1255 public SpinnerNumberModel getModel() {
1256 return (SpinnerNumberModel)(getSpinner().getModel());
1257 }
1258 }
1259
1260
1261 /**
1262 * An editor for a <code>JSpinner</code> whose model is a
1263 * <code>SpinnerListModel</code>.
1264 * @since 1.4
1265 */
1266 public static class ListEditor extends DefaultEditor
1267 {
1268 /**
1269 * Construct a <code>JSpinner</code> editor that supports displaying
1270 * and editing the value of a <code>SpinnerListModel</code>
1271 * with a <code>JFormattedTextField</code>. <code>This</code>
1272 * <code>ListEditor</code> becomes both a <code>ChangeListener</code>
1273 * on the spinner and a <code>PropertyChangeListener</code>
1274 * on the new <code>JFormattedTextField</code>.
1275 *
1276 * @param spinner the spinner whose model <code>this</code> editor will monitor
1277 * @exception IllegalArgumentException if the spinners model is not
1278 * an instance of <code>SpinnerListModel</code>
1279 *
1280 * @see #getModel
1281 * @see SpinnerListModel
1282 */
1283 public ListEditor(JSpinner spinner) {
1284 super(spinner);
1285 if (!(spinner.getModel() instanceof SpinnerListModel)) {
1286 throw new IllegalArgumentException("model not a SpinnerListModel");
1287 }
1288 getTextField().setEditable(true);
1289 getTextField().setFormatterFactory(new
1290 DefaultFormatterFactory(new ListFormatter()));
1291 }
1292
1293 /**
1294 * Return our spinner ancestor's <code>SpinnerNumberModel</code>.
1295 *
1296 * @return <code>getSpinner().getModel()</code>
1297 * @see #getSpinner
1298 * @see #getTextField
1299 */
1300 public SpinnerListModel getModel() {
1301 return (SpinnerListModel)(getSpinner().getModel());
1302 }
1303
1304
1305 /**
1306 * ListFormatter provides completion while text is being input
1307 * into the JFormattedTextField. Completion is only done if the
1308 * user is inserting text at the end of the document. Completion
1309 * is done by way of the SpinnerListModel method findNextMatch.
1310 */
1311 private class ListFormatter extends
1312 JFormattedTextField.AbstractFormatter {
1313 private DocumentFilter filter;
1314
1315 public String valueToString(Object value) throws ParseException {
1316 if (value == null) {
1317 return "";
1318 }
1319 return value.toString();
1320 }
1321
1322 public Object stringToValue(String string) throws ParseException {
1323 return string;
1324 }
1325
1326 protected DocumentFilter getDocumentFilter() {
1327 if (filter == null) {
1328 filter = new Filter();
1329 }
1330 return filter;
1331 }
1332
1333
1334 private class Filter extends DocumentFilter {
1335 public void replace(FilterBypass fb, int offset, int length,
1336 String string, AttributeSet attrs) throws
1337 BadLocationException {
1338 if (string != null && (offset + length) ==
1339 fb.getDocument().getLength()) {
1340 Object next = getModel().findNextMatch(
1341 fb.getDocument().getText(0, offset) +
1342 string);
1343 String value = (next != null) ? next.toString() : null;
1344
1345 if (value != null) {
1346 fb.remove(0, offset + length);
1347 fb.insertString(0, value, null);
1348 getFormattedTextField().select(offset +
1349 string.length(),
1350 value.length());
1351 return;
1352 }
1353 }
1354 super.replace(fb, offset, length, string, attrs);
1355 }
1356
1357 public void insertString(FilterBypass fb, int offset,
1358 String string, AttributeSet attr)
1359 throws BadLocationException {
1360 replace(fb, offset, 0, string, attr);
1361 }
1362 }
1363 }
1364 }
1365
1366
1367 /**
1368 * An Action implementation that is always disabled.
1369 */
1370 private static class DisabledAction implements Action {
1371 public Object getValue(String key) {
1372 return null;
1373 }
1374 public void putValue(String key, Object value) {
1375 }
1376 public void setEnabled(boolean b) {
1377 }
1378 public boolean isEnabled() {
1379 return false;
1380 }
1381 public void addPropertyChangeListener(PropertyChangeListener l) {
1382 }
1383 public void removePropertyChangeListener(PropertyChangeListener l) {
1384 }
1385 public void actionPerformed(ActionEvent ae) {
1386 }
1387 }
1388
1389 /////////////////
1390 // Accessibility support
1391 ////////////////
1392
1393 /**
1394 * Gets the <code>AccessibleContext</code> for the <code>JSpinner</code>
1395 *
1396 * @return the <code>AccessibleContext</code> for the <code>JSpinner</code>
1397 * @since 1.5
1398 */
1399 public AccessibleContext getAccessibleContext() {
1400 if (accessibleContext == null) {
1401 accessibleContext = new AccessibleJSpinner();
1402 }
1403 return accessibleContext;
1404 }
1405
1406 /**
1407 * <code>AccessibleJSpinner</code> implements accessibility
1408 * support for the <code>JSpinner</code> class.
1409 * @since 1.5
1410 */
1411 protected class AccessibleJSpinner extends AccessibleJComponent
1412 implements AccessibleValue, AccessibleAction, AccessibleText,
1413 AccessibleEditableText, ChangeListener {
1414
1415 private Object oldModelValue = null;
1416
1417 /**
1418 * AccessibleJSpinner constructor
1419 */
1420 protected AccessibleJSpinner() {
1421 // model is guaranteed to be non-null
1422 oldModelValue = model.getValue();
1423 JSpinner.this.addChangeListener(this);
1424 }
1425
1426 /**
1427 * Invoked when the target of the listener has changed its state.
1428 *
1429 * @param e a <code>ChangeEvent</code> object. Must not be null.
1430 * @throws NullPointerException if the parameter is null.
1431 */
1432 public void stateChanged(ChangeEvent e) {
1433 if (e == null) {
1434 throw new NullPointerException();
1435 }
1436 Object newModelValue = model.getValue();
1437 firePropertyChange(ACCESSIBLE_VALUE_PROPERTY,
1438 oldModelValue,
1439 newModelValue);
1440 firePropertyChange(ACCESSIBLE_TEXT_PROPERTY,
1441 null,
1442 0); // entire text may have changed
1443
1444 oldModelValue = newModelValue;
1445 }
1446
1447 /* ===== Begin AccessibleContext methods ===== */
1448
1449 /**
1450 * Gets the role of this object. The role of the object is the generic
1451 * purpose or use of the class of this object. For example, the role
1452 * of a push button is AccessibleRole.PUSH_BUTTON. The roles in
1453 * AccessibleRole are provided so component developers can pick from
1454 * a set of predefined roles. This enables assistive technologies to
1455 * provide a consistent interface to various tweaked subclasses of
1456 * components (e.g., use AccessibleRole.PUSH_BUTTON for all components
1457 * that act like a push button) as well as distinguish between sublasses
1458 * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes
1459 * and AccessibleRole.RADIO_BUTTON for radio buttons).
1460 * <p>Note that the AccessibleRole class is also extensible, so
1461 * custom component developers can define their own AccessibleRole's
1462 * if the set of predefined roles is inadequate.
1463 *
1464 * @return an instance of AccessibleRole describing the role of the object
1465 * @see AccessibleRole
1466 */
1467 public AccessibleRole getAccessibleRole() {
1468 return AccessibleRole.SPIN_BOX;
1469 }
1470
1471 /**
1472 * Returns the number of accessible children of the object.
1473 *
1474 * @return the number of accessible children of the object.
1475 */
1476 public int getAccessibleChildrenCount() {
1477 // the JSpinner has one child, the editor
1478 if (editor.getAccessibleContext() != null) {
1479 return 1;
1480 }
1481 return 0;
1482 }
1483
1484 /**
1485 * Returns the specified Accessible child of the object. The Accessible
1486 * children of an Accessible object are zero-based, so the first child
1487 * of an Accessible child is at index 0, the second child is at index 1,
1488 * and so on.
1489 *
1490 * @param i zero-based index of child
1491 * @return the Accessible child of the object
1492 * @see #getAccessibleChildrenCount
1493 */
1494 public Accessible getAccessibleChild(int i) {
1495 // the JSpinner has one child, the editor
1496 if (i != 0) {
1497 return null;
1498 }
1499 if (editor.getAccessibleContext() != null) {
1500 return (Accessible)editor;
1501 }
1502 return null;
1503 }
1504
1505 /* ===== End AccessibleContext methods ===== */
1506
1507 /**
1508 * Gets the AccessibleAction associated with this object that supports
1509 * one or more actions.
1510 *
1511 * @return AccessibleAction if supported by object; else return null
1512 * @see AccessibleAction
1513 */
1514 public AccessibleAction getAccessibleAction() {
1515 return this;
1516 }
1517
1518 /**
1519 * Gets the AccessibleText associated with this object presenting
1520 * text on the display.
1521 *
1522 * @return AccessibleText if supported by object; else return null
1523 * @see AccessibleText
1524 */
1525 public AccessibleText getAccessibleText() {
1526 return this;
1527 }
1528
1529 /*
1530 * Returns the AccessibleContext for the JSpinner editor
1531 */
1532 private AccessibleContext getEditorAccessibleContext() {
1533 if (editor instanceof DefaultEditor) {
1534 JTextField textField = ((DefaultEditor)editor).getTextField();
1535 if (textField != null) {
1536 return textField.getAccessibleContext();
1537 }
1538 } else if (editor instanceof Accessible) {
1539 return ((Accessible)editor).getAccessibleContext();
1540 }
1541 return null;
1542 }
1543
1544 /*
1545 * Returns the AccessibleText for the JSpinner editor
1546 */
1547 private AccessibleText getEditorAccessibleText() {
1548 AccessibleContext ac = getEditorAccessibleContext();
1549 if (ac != null) {
1550 return ac.getAccessibleText();
1551 }
1552 return null;
1553 }
1554
1555 /*
1556 * Returns the AccessibleEditableText for the JSpinner editor
1557 */
1558 private AccessibleEditableText getEditorAccessibleEditableText() {
1559 AccessibleText at = getEditorAccessibleText();
1560 if (at instanceof AccessibleEditableText) {
1561 return (AccessibleEditableText)at;
1562 }
1563 return null;
1564 }
1565
1566 /**
1567 * Gets the AccessibleValue associated with this object.
1568 *
1569 * @return AccessibleValue if supported by object; else return null
1570 * @see AccessibleValue
1571 *
1572 */
1573 public AccessibleValue getAccessibleValue() {
1574 return this;
1575 }
1576
1577 /* ===== Begin AccessibleValue impl ===== */
1578
1579 /**
1580 * Get the value of this object as a Number. If the value has not been
1581 * set, the return value will be null.
1582 *
1583 * @return value of the object
1584 * @see #setCurrentAccessibleValue
1585 */
1586 public Number getCurrentAccessibleValue() {
1587 Object o = model.getValue();
1588 if (o instanceof Number) {
1589 return (Number)o;
1590 }
1591 return null;
1592 }
1593
1594 /**
1595 * Set the value of this object as a Number.
1596 *
1597 * @param n the value to set for this object
1598 * @return true if the value was set; else False
1599 * @see #getCurrentAccessibleValue
1600 */
1601 public boolean setCurrentAccessibleValue(Number n) {
1602 // try to set the new value
1603 try {
1604 model.setValue(n);
1605 return true;
1606 } catch (IllegalArgumentException iae) {
1607 // SpinnerModel didn't like new value
1608 }
1609 return false;
1610 }
1611
1612 /**
1613 * Get the minimum value of this object as a Number.
1614 *
1615 * @return Minimum value of the object; null if this object does not
1616 * have a minimum value
1617 * @see #getMaximumAccessibleValue
1618 */
1619 public Number getMinimumAccessibleValue() {
1620 if (model instanceof SpinnerNumberModel) {
1621 SpinnerNumberModel numberModel = (SpinnerNumberModel)model;
1622 Object o = numberModel.getMinimum();
1623 if (o instanceof Number) {
1624 return (Number)o;
1625 }
1626 }
1627 return null;
1628 }
1629
1630 /**
1631 * Get the maximum value of this object as a Number.
1632 *
1633 * @return Maximum value of the object; null if this object does not
1634 * have a maximum value
1635 * @see #getMinimumAccessibleValue
1636 */
1637 public Number getMaximumAccessibleValue() {
1638 if (model instanceof SpinnerNumberModel) {
1639 SpinnerNumberModel numberModel = (SpinnerNumberModel)model;
1640 Object o = numberModel.getMaximum();
1641 if (o instanceof Number) {
1642 return (Number)o;
1643 }
1644 }
1645 return null;
1646 }
1647
1648 /* ===== End AccessibleValue impl ===== */
1649
1650 /* ===== Begin AccessibleAction impl ===== */
1651
1652 /**
1653 * Returns the number of accessible actions available in this object
1654 * If there are more than one, the first one is considered the "default"
1655 * action of the object.
1656 *
1657 * Two actions are supported: AccessibleAction.INCREMENT which
1658 * increments the spinner value and AccessibleAction.DECREMENT
1659 * which decrements the spinner value
1660 *
1661 * @return the zero-based number of Actions in this object
1662 */
1663 public int getAccessibleActionCount() {
1664 return 2;
1665 }
1666
1667 /**
1668 * Returns a description of the specified action of the object.
1669 *
1670 * @param i zero-based index of the actions
1671 * @return a String description of the action
1672 * @see #getAccessibleActionCount
1673 */
1674 public String getAccessibleActionDescription(int i) {
1675 if (i == 0) {
1676 return AccessibleAction.INCREMENT;
1677 } else if (i == 1) {
1678 return AccessibleAction.DECREMENT;
1679 }
1680 return null;
1681 }
1682
1683 /**
1684 * Performs the specified Action on the object
1685 *
1686 * @param i zero-based index of actions. The first action
1687 * (index 0) is AccessibleAction.INCREMENT and the second
1688 * action (index 1) is AccessibleAction.DECREMENT.
1689 * @return true if the action was performed; otherwise false.
1690 * @see #getAccessibleActionCount
1691 */
1692 public boolean doAccessibleAction(int i) {
1693 if (i < 0 || i > 1) {
1694 return false;
1695 }
1696 Object o = null;
1697 if (i == 0) {
1698 o = getNextValue(); // AccessibleAction.INCREMENT
1699 } else {
1700 o = getPreviousValue(); // AccessibleAction.DECREMENT
1701 }
1702 // try to set the new value
1703 try {
1704 model.setValue(o);
1705 return true;
1706 } catch (IllegalArgumentException iae) {
1707 // SpinnerModel didn't like new value
1708 }
1709 return false;
1710 }
1711
1712 /* ===== End AccessibleAction impl ===== */
1713
1714 /* ===== Begin AccessibleText impl ===== */
1715
1716 /*
1717 * Returns whether source and destination components have the
1718 * same window ancestor
1719 */
1720 private boolean sameWindowAncestor(Component src, Component dest) {
1721 if (src == null || dest == null) {
1722 return false;
1723 }
1724 return SwingUtilities.getWindowAncestor(src) ==
1725 SwingUtilities.getWindowAncestor(dest);
1726 }
1727
1728 /**
1729 * Given a point in local coordinates, return the zero-based index
1730 * of the character under that Point. If the point is invalid,
1731 * this method returns -1.
1732 *
1733 * @param p the Point in local coordinates
1734 * @return the zero-based index of the character under Point p; if
1735 * Point is invalid return -1.
1736 */
1737 public int getIndexAtPoint(Point p) {
1738 AccessibleText at = getEditorAccessibleText();
1739 if (at != null && sameWindowAncestor(JSpinner.this, editor)) {
1740 // convert point from the JSpinner bounds (source) to
1741 // editor bounds (destination)
1742 Point editorPoint = SwingUtilities.convertPoint(JSpinner.this,
1743 p,
1744 editor);
1745 if (editorPoint != null) {
1746 return at.getIndexAtPoint(editorPoint);
1747 }
1748 }
1749 return -1;
1750 }
1751
1752 /**
1753 * Determines the bounding box of the character at the given
1754 * index into the string. The bounds are returned in local
1755 * coordinates. If the index is invalid an empty rectangle is
1756 * returned.
1757 *
1758 * @param i the index into the String
1759 * @return the screen coordinates of the character's bounding box,
1760 * if index is invalid return an empty rectangle.
1761 */
1762 public Rectangle getCharacterBounds(int i) {
1763 AccessibleText at = getEditorAccessibleText();
1764 if (at != null ) {
1765 Rectangle editorRect = at.getCharacterBounds(i);
1766 if (editorRect != null &&
1767 sameWindowAncestor(JSpinner.this, editor)) {
1768 // return rectangle in the the JSpinner bounds
1769 return SwingUtilities.convertRectangle(editor,
1770 editorRect,
1771 JSpinner.this);
1772 }
1773 }
1774 return null;
1775 }
1776
1777 /**
1778 * Returns the number of characters (valid indicies)
1779 *
1780 * @return the number of characters
1781 */
1782 public int getCharCount() {
1783 AccessibleText at = getEditorAccessibleText();
1784 if (at != null) {
1785 return at.getCharCount();
1786 }
1787 return -1;
1788 }
1789
1790 /**
1791 * Returns the zero-based offset of the caret.
1792 *
1793 * Note: That to the right of the caret will have the same index
1794 * value as the offset (the caret is between two characters).
1795 * @return the zero-based offset of the caret.
1796 */
1797 public int getCaretPosition() {
1798 AccessibleText at = getEditorAccessibleText();
1799 if (at != null) {
1800 return at.getCaretPosition();
1801 }
1802 return -1;
1803 }
1804
1805 /**
1806 * Returns the String at a given index.
1807 *
1808 * @param part the CHARACTER, WORD, or SENTENCE to retrieve
1809 * @param index an index within the text
1810 * @return the letter, word, or sentence
1811 */
1812 public String getAtIndex(int part, int index) {
1813 AccessibleText at = getEditorAccessibleText();
1814 if (at != null) {
1815 return at.getAtIndex(part, index);
1816 }
1817 return null;
1818 }
1819
1820 /**
1821 * Returns the String after a given index.
1822 *
1823 * @param part the CHARACTER, WORD, or SENTENCE to retrieve
1824 * @param index an index within the text
1825 * @return the letter, word, or sentence
1826 */
1827 public String getAfterIndex(int part, int index) {
1828 AccessibleText at = getEditorAccessibleText();
1829 if (at != null) {
1830 return at.getAfterIndex(part, index);
1831 }
1832 return null;
1833 }
1834
1835 /**
1836 * Returns the String before a given index.
1837 *
1838 * @param part the CHARACTER, WORD, or SENTENCE to retrieve
1839 * @param index an index within the text
1840 * @return the letter, word, or sentence
1841 */
1842 public String getBeforeIndex(int part, int index) {
1843 AccessibleText at = getEditorAccessibleText();
1844 if (at != null) {
1845 return at.getBeforeIndex(part, index);
1846 }
1847 return null;
1848 }
1849
1850 /**
1851 * Returns the AttributeSet for a given character at a given index
1852 *
1853 * @param i the zero-based index into the text
1854 * @return the AttributeSet of the character
1855 */
1856 public AttributeSet getCharacterAttribute(int i) {
1857 AccessibleText at = getEditorAccessibleText();
1858 if (at != null) {
1859 return at.getCharacterAttribute(i);
1860 }
1861 return null;
1862 }
1863
1864 /**
1865 * Returns the start offset within the selected text.
1866 * If there is no selection, but there is
1867 * a caret, the start and end offsets will be the same.
1868 *
1869 * @return the index into the text of the start of the selection
1870 */
1871 public int getSelectionStart() {
1872 AccessibleText at = getEditorAccessibleText();
1873 if (at != null) {
1874 return at.getSelectionStart();
1875 }
1876 return -1;
1877 }
1878
1879 /**
1880 * Returns the end offset within the selected text.
1881 * If there is no selection, but there is
1882 * a caret, the start and end offsets will be the same.
1883 *
1884 * @return the index into teh text of the end of the selection
1885 */
1886 public int getSelectionEnd() {
1887 AccessibleText at = getEditorAccessibleText();
1888 if (at != null) {
1889 return at.getSelectionEnd();
1890 }
1891 return -1;
1892 }
1893
1894 /**
1895 * Returns the portion of the text that is selected.
1896 *
1897 * @return the String portion of the text that is selected
1898 */
1899 public String getSelectedText() {
1900 AccessibleText at = getEditorAccessibleText();
1901 if (at != null) {
1902 return at.getSelectedText();
1903 }
1904 return null;
1905 }
1906
1907 /* ===== End AccessibleText impl ===== */
1908
1909
1910 /* ===== Begin AccessibleEditableText impl ===== */
1911
1912 /**
1913 * Sets the text contents to the specified string.
1914 *
1915 * @param s the string to set the text contents
1916 */
1917 public void setTextContents(String s) {
1918 AccessibleEditableText at = getEditorAccessibleEditableText();
1919 if (at != null) {
1920 at.setTextContents(s);
1921 }
1922 }
1923
1924 /**
1925 * Inserts the specified string at the given index/
1926 *
1927 * @param index the index in the text where the string will
1928 * be inserted
1929 * @param s the string to insert in the text
1930 */
1931 public void insertTextAtIndex(int index, String s) {
1932 AccessibleEditableText at = getEditorAccessibleEditableText();
1933 if (at != null) {
1934 at.insertTextAtIndex(index, s);
1935 }
1936 }
1937
1938 /**
1939 * Returns the text string between two indices.
1940 *
1941 * @param startIndex the starting index in the text
1942 * @param endIndex the ending index in the text
1943 * @return the text string between the indices
1944 */
1945 public String getTextRange(int startIndex, int endIndex) {
1946 AccessibleEditableText at = getEditorAccessibleEditableText();
1947 if (at != null) {
1948 return at.getTextRange(startIndex, endIndex);
1949 }
1950 return null;
1951 }
1952
1953 /**
1954 * Deletes the text between two indices
1955 *
1956 * @param startIndex the starting index in the text
1957 * @param endIndex the ending index in the text
1958 */
1959 public void delete(int startIndex, int endIndex) {
1960 AccessibleEditableText at = getEditorAccessibleEditableText();
1961 if (at != null) {
1962 at.delete(startIndex, endIndex);
1963 }
1964 }
1965
1966 /**
1967 * Cuts the text between two indices into the system clipboard.
1968 *
1969 * @param startIndex the starting index in the text
1970 * @param endIndex the ending index in the text
1971 */
1972 public void cut(int startIndex, int endIndex) {
1973 AccessibleEditableText at = getEditorAccessibleEditableText();
1974 if (at != null) {
1975 at.cut(startIndex, endIndex);
1976 }
1977 }
1978
1979 /**
1980 * Pastes the text from the system clipboard into the text
1981 * starting at the specified index.
1982 *
1983 * @param startIndex the starting index in the text
1984 */
1985 public void paste(int startIndex) {
1986 AccessibleEditableText at = getEditorAccessibleEditableText();
1987 if (at != null) {
1988 at.paste(startIndex);
1989 }
1990 }
1991
1992 /**
1993 * Replaces the text between two indices with the specified
1994 * string.
1995 *
1996 * @param startIndex the starting index in the text
1997 * @param endIndex the ending index in the text
1998 * @param s the string to replace the text between two indices
1999 */
2000 public void replaceText(int startIndex, int endIndex, String s) {
2001 AccessibleEditableText at = getEditorAccessibleEditableText();
2002 if (at != null) {
2003 at.replaceText(startIndex, endIndex, s);
2004 }
2005 }
2006
2007 /**
2008 * Selects the text between two indices.
2009 *
2010 * @param startIndex the starting index in the text
2011 * @param endIndex the ending index in the text
2012 */
2013 public void selectText(int startIndex, int endIndex) {
2014 AccessibleEditableText at = getEditorAccessibleEditableText();
2015 if (at != null) {
2016 at.selectText(startIndex, endIndex);
2017 }
2018 }
2019
2020 /**
2021 * Sets attributes for the text between two indices.
2022 *
2023 * @param startIndex the starting index in the text
2024 * @param endIndex the ending index in the text
2025 * @param as the attribute set
2026 * @see AttributeSet
2027 */
2028 public void setAttributes(int startIndex, int endIndex, AttributeSet as) {
2029 AccessibleEditableText at = getEditorAccessibleEditableText();
2030 if (at != null) {
2031 at.setAttributes(startIndex, endIndex, as);
2032 }
2033 }
2034 } /* End AccessibleJSpinner */
2035 }