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