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