1 /* 2 * Copyright (c) 2000, 2015, 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.text.spi.DateFormatProvider; 40 import java.text.spi.NumberFormatProvider; 41 42 import javax.accessibility.*; 43 import sun.util.locale.provider.LocaleProviderAdapter; 44 import sun.util.locale.provider.LocaleResources; 45 46 /** 47 * A single line input field that lets the user select a 48 * number or an object value from an ordered sequence. Spinners typically 49 * provide a pair of tiny arrow buttons for stepping through the elements 50 * of the sequence. The keyboard up/down arrow keys also cycle through the 51 * elements. The user may also be allowed to type a (legal) value directly 52 * into the spinner. Although combo boxes provide similar functionality, 53 * spinners are sometimes preferred because they don't require a drop down list 54 * that can obscure important data. 55 * <p> 56 * A <code>JSpinner</code>'s sequence value is defined by its 57 * <code>SpinnerModel</code>. 58 * The <code>model</code> can be specified as a constructor argument and 59 * changed with the <code>model</code> property. <code>SpinnerModel</code> 60 * classes for some common types are provided: <code>SpinnerListModel</code>, 61 * <code>SpinnerNumberModel</code>, and <code>SpinnerDateModel</code>. 62 * <p> 63 * A <code>JSpinner</code> has a single child component that's 64 * responsible for displaying 65 * and potentially changing the current element or <i>value</i> of 66 * the model, which is called the <code>editor</code>. The editor is created 67 * by the <code>JSpinner</code>'s constructor and can be changed with the 68 * <code>editor</code> property. The <code>JSpinner</code>'s editor stays 69 * in sync with the model by listening for <code>ChangeEvent</code>s. If the 70 * user has changed the value displayed by the <code>editor</code> it is 71 * possible for the <code>model</code>'s value to differ from that of 72 * the <code>editor</code>. To make sure the <code>model</code> has the same 73 * value as the editor use the <code>commitEdit</code> method, eg: 74 * <pre> 75 * try { 76 * spinner.commitEdit(); 77 * } 78 * catch (ParseException pe) { 79 * // Edited value is invalid, spinner.getValue() will return 80 * // the last valid value, you could revert the spinner to show that: 81 * JComponent editor = spinner.getEditor(); 82 * if (editor instanceof DefaultEditor) { 83 * ((DefaultEditor)editor).getTextField().setValue(spinner.getValue()); 84 * } 85 * // reset the value to some known value: 86 * spinner.setValue(fallbackValue); 87 * // or treat the last valid value as the current, in which 88 * // case you don't need to do anything. 89 * } 90 * return spinner.getValue(); 91 * </pre> 92 * <p> 93 * For information and examples of using spinner see 94 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/components/spinner.html">How to Use Spinners</a>, 95 * a section in <em>The Java Tutorial.</em> 96 * <p> 97 * <strong>Warning:</strong> Swing is not thread safe. For more 98 * information see <a 99 * href="package-summary.html#threading">Swing's Threading 100 * Policy</a>. 101 * <p> 102 * <strong>Warning:</strong> 103 * Serialized objects of this class will not be compatible with 104 * future Swing releases. The current serialization support is 105 * appropriate for short term storage or RMI between applications running 106 * the same version of Swing. As of 1.4, support for long term storage 107 * of all JavaBeans™ 108 * has been added to the <code>java.beans</code> package. 109 * Please see {@link java.beans.XMLEncoder}. 110 * 111 * @see SpinnerModel 112 * @see AbstractSpinnerModel 113 * @see SpinnerListModel 114 * @see SpinnerNumberModel 115 * @see SpinnerDateModel 116 * @see JFormattedTextField 117 * 118 * @author Hans Muller 119 * @author Lynn Monsanto (accessibility) 120 * @since 1.4 121 */ 122 @JavaBean(defaultProperty = "UI", description = "A single line input field that lets the user select a number or an object value from an ordered set.") 123 @SwingContainer(false) 124 @SuppressWarnings("serial") // Same-version serialization only 125 public class JSpinner extends JComponent implements Accessible 126 { 127 /** 128 * @see #getUIClassID 129 * @see #readObject 130 */ 131 private static final String uiClassID = "SpinnerUI"; 132 133 private static final Action DISABLED_ACTION = new DisabledAction(); 134 135 private SpinnerModel model; 136 private JComponent editor; 137 private ChangeListener modelListener; 138 private transient ChangeEvent changeEvent; 139 private boolean editorExplicitlySet = false; 140 141 142 /** 143 * Constructs a spinner for the given model. The spinner has 144 * a set of previous/next buttons, and an editor appropriate 145 * for the model. 146 * 147 * @param model a model for the new spinner 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 @BeanProperty(bound = false) 200 public String getUIClassID() { 201 return uiClassID; 202 } 203 204 205 206 /** 207 * Resets the UI property with the value from the current look and feel. 208 * 209 * @see UIManager#getUI 210 */ 211 public void updateUI() { 212 setUI((SpinnerUI)UIManager.getUI(this)); 213 invalidate(); 214 } 215 216 217 /** 218 * This method is called by the constructors to create the 219 * <code>JComponent</code> 220 * that displays the current value of the sequence. The editor may 221 * also allow the user to enter an element of the sequence directly. 222 * An editor must listen for <code>ChangeEvents</code> on the 223 * <code>model</code> and keep the value it displays 224 * in sync with the value of the model. 225 * <p> 226 * Subclasses may override this method to add support for new 227 * <code>SpinnerModel</code> classes. Alternatively one can just 228 * replace the editor created here with the <code>setEditor</code> 229 * method. The default mapping from model type to editor is: 230 * <ul> 231 * <li> <code>SpinnerNumberModel => JSpinner.NumberEditor</code> 232 * <li> <code>SpinnerDateModel => JSpinner.DateEditor</code> 233 * <li> <code>SpinnerListModel => JSpinner.ListEditor</code> 234 * <li> <i>all others</i> => <code>JSpinner.DefaultEditor</code> 235 * </ul> 236 * 237 * @return a component that displays the current value of the sequence 238 * @param model the value of getModel 239 * @see #getModel 240 * @see #setEditor 241 */ 242 protected JComponent createEditor(SpinnerModel model) { 243 if (model instanceof SpinnerDateModel) { 244 return new DateEditor(this); 245 } 246 else if (model instanceof SpinnerListModel) { 247 return new ListEditor(this); 248 } 249 else if (model instanceof SpinnerNumberModel) { 250 return new NumberEditor(this); 251 } 252 else { 253 return new DefaultEditor(this); 254 } 255 } 256 257 258 /** 259 * Changes the model that represents the value of this spinner. 260 * If the editor property has not been explicitly set, 261 * the editor property is (implicitly) set after the <code>"model"</code> 262 * <code>PropertyChangeEvent</code> has been fired. The editor 263 * property is set to the value returned by <code>createEditor</code>, 264 * as in: 265 * <pre> 266 * setEditor(createEditor(model)); 267 * </pre> 268 * 269 * @param model the new <code>SpinnerModel</code> 270 * @see #getModel 271 * @see #getEditor 272 * @see #setEditor 273 * @throws IllegalArgumentException if model is <code>null</code> 274 */ 275 @BeanProperty(visualUpdate = true, description 276 = "Model that represents the value of this spinner.") 277 public void setModel(SpinnerModel model) { 278 if (model == null) { 279 throw new IllegalArgumentException("null model"); 280 } 281 if (!model.equals(this.model)) { 282 SpinnerModel oldModel = this.model; 283 this.model = model; 284 if (modelListener != null) { 285 oldModel.removeChangeListener(modelListener); 286 this.model.addChangeListener(modelListener); 287 } 288 firePropertyChange("model", oldModel, model); 289 if (!editorExplicitlySet) { 290 setEditor(createEditor(model)); // sets editorExplicitlySet true 291 editorExplicitlySet = false; 292 } 293 repaint(); 294 revalidate(); 295 } 296 } 297 298 299 /** 300 * Returns the <code>SpinnerModel</code> that defines 301 * this spinners sequence of values. 302 * 303 * @return the value of the model property 304 * @see #setModel 305 */ 306 public SpinnerModel getModel() { 307 return model; 308 } 309 310 311 /** 312 * Returns the current value of the model, typically 313 * this value is displayed by the <code>editor</code>. If the 314 * user has changed the value displayed by the <code>editor</code> it is 315 * possible for the <code>model</code>'s value to differ from that of 316 * the <code>editor</code>, refer to the class level javadoc for examples 317 * of how to deal with this. 318 * <p> 319 * This method simply delegates to the <code>model</code>. 320 * It is equivalent to: 321 * <pre> 322 * getModel().getValue() 323 * </pre> 324 * 325 * @return the current value of the model 326 * @see #setValue 327 * @see SpinnerModel#getValue 328 */ 329 public Object getValue() { 330 return getModel().getValue(); 331 } 332 333 334 /** 335 * Changes current value of the model, typically 336 * this value is displayed by the <code>editor</code>. 337 * If the <code>SpinnerModel</code> implementation 338 * doesn't support the specified value then an 339 * <code>IllegalArgumentException</code> is thrown. 340 * <p> 341 * This method simply delegates to the <code>model</code>. 342 * It is equivalent to: 343 * <pre> 344 * getModel().setValue(value) 345 * </pre> 346 * 347 * @param value new value for the spinner 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 @BeanProperty(bound = false) 375 public Object getNextValue() { 376 return getModel().getNextValue(); 377 } 378 379 380 /** 381 * We pass <code>Change</code> events along to the listeners with the 382 * the slider (instead of the model itself) as the event source. 383 */ 384 private class ModelListener implements ChangeListener, Serializable { 385 public void stateChanged(ChangeEvent e) { 386 fireStateChanged(); 387 } 388 } 389 390 391 /** 392 * Adds a listener to the list that is notified each time a change 393 * to the model occurs. The source of <code>ChangeEvents</code> 394 * delivered to <code>ChangeListeners</code> will be this 395 * <code>JSpinner</code>. Note also that replacing the model 396 * will not affect listeners added directly to JSpinner. 397 * Applications can add listeners to the model directly. In that 398 * case is that the source of the event would be the 399 * <code>SpinnerModel</code>. 400 * 401 * @param listener the <code>ChangeListener</code> to add 402 * @see #removeChangeListener 403 * @see #getModel 404 */ 405 public void addChangeListener(ChangeListener listener) { 406 if (modelListener == null) { 407 modelListener = new ModelListener(); 408 getModel().addChangeListener(modelListener); 409 } 410 listenerList.add(ChangeListener.class, listener); 411 } 412 413 414 415 /** 416 * Removes a <code>ChangeListener</code> from this spinner. 417 * 418 * @param listener the <code>ChangeListener</code> to remove 419 * @see #fireStateChanged 420 * @see #addChangeListener 421 */ 422 public void removeChangeListener(ChangeListener listener) { 423 listenerList.remove(ChangeListener.class, listener); 424 } 425 426 427 /** 428 * Returns an array of all the <code>ChangeListener</code>s added 429 * to this JSpinner with addChangeListener(). 430 * 431 * @return all of the <code>ChangeListener</code>s added or an empty 432 * array if no listeners have been added 433 * @since 1.4 434 */ 435 @BeanProperty(bound = false) 436 public ChangeListener[] getChangeListeners() { 437 return listenerList.getListeners(ChangeListener.class); 438 } 439 440 441 /** 442 * Sends a <code>ChangeEvent</code>, whose source is this 443 * <code>JSpinner</code>, to each <code>ChangeListener</code>. 444 * When a <code>ChangeListener</code> has been added 445 * to the spinner, this method is called each time 446 * a <code>ChangeEvent</code> is received from the model. 447 * 448 * @see #addChangeListener 449 * @see #removeChangeListener 450 * @see EventListenerList 451 */ 452 protected void fireStateChanged() { 453 Object[] listeners = listenerList.getListenerList(); 454 for (int i = listeners.length - 2; i >= 0; i -= 2) { 455 if (listeners[i] == ChangeListener.class) { 456 if (changeEvent == null) { 457 changeEvent = new ChangeEvent(this); 458 } 459 ((ChangeListener)listeners[i+1]).stateChanged(changeEvent); 460 } 461 } 462 } 463 464 465 /** 466 * Returns the object in the sequence that comes 467 * before the object returned by <code>getValue()</code>. 468 * If the end of the sequence has been reached then 469 * return <code>null</code>. Calling this method does 470 * not effect <code>value</code>. 471 * <p> 472 * This method simply delegates to the <code>model</code>. 473 * It is equivalent to: 474 * <pre> 475 * getModel().getPreviousValue() 476 * </pre> 477 * 478 * @return the previous legal value or <code>null</code> 479 * if one doesn't exist 480 * @see #getValue 481 * @see #getNextValue 482 * @see SpinnerModel#getPreviousValue 483 */ 484 @BeanProperty(bound = false) 485 public Object getPreviousValue() { 486 return getModel().getPreviousValue(); 487 } 488 489 490 /** 491 * Changes the <code>JComponent</code> that displays the current value 492 * of the <code>SpinnerModel</code>. It is the responsibility of this 493 * method to <i>disconnect</i> the old editor from the model and to 494 * connect the new editor. This may mean removing the 495 * old editors <code>ChangeListener</code> from the model or the 496 * spinner itself and adding one for the new editor. 497 * 498 * @param editor the new editor 499 * @see #getEditor 500 * @see #createEditor 501 * @see #getModel 502 * @throws IllegalArgumentException if editor is <code>null</code> 503 */ 504 @BeanProperty(visualUpdate = true, description 505 = "JComponent that displays the current value of the model") 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 committed. 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 the 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 @Override 917 @SuppressWarnings("unchecked") 918 public void setMinimum(Comparable<?> min) { 919 model.setStart((Comparable<Date>)min); 920 } 921 922 @Override 923 public Comparable<Date> getMinimum() { 924 return model.getStart(); 925 } 926 927 @Override 928 @SuppressWarnings("unchecked") 929 public void setMaximum(Comparable<?> max) { 930 model.setEnd((Comparable<Date>)max); 931 } 932 933 @Override 934 public Comparable<Date> getMaximum() { 935 return model.getEnd(); 936 } 937 } 938 939 940 /** 941 * An editor for a <code>JSpinner</code> whose model is a 942 * <code>SpinnerDateModel</code>. The value of the editor is 943 * displayed with a <code>JFormattedTextField</code> whose format 944 * is defined by a <code>DateFormatter</code> instance whose 945 * <code>minimum</code> and <code>maximum</code> properties 946 * are mapped to the <code>SpinnerDateModel</code>. 947 * @since 1.4 948 */ 949 // PENDING(hmuller): more example javadoc 950 public static class DateEditor extends DefaultEditor 951 { 952 // This is here until SimpleDateFormat gets a constructor that 953 // takes a Locale: 4923525 954 private static String getDefaultPattern(Locale loc) { 955 LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(DateFormatProvider.class, loc); 956 LocaleResources lr = adapter.getLocaleResources(loc); 957 if (lr == null) { 958 lr = LocaleProviderAdapter.forJRE().getLocaleResources(loc); 959 } 960 return lr.getDateTimePattern(DateFormat.SHORT, DateFormat.SHORT, null); 961 } 962 963 /** 964 * Construct a <code>JSpinner</code> editor that supports displaying 965 * and editing the value of a <code>SpinnerDateModel</code> 966 * with a <code>JFormattedTextField</code>. <code>This</code> 967 * <code>DateEditor</code> becomes both a <code>ChangeListener</code> 968 * on the spinner and a <code>PropertyChangeListener</code> 969 * on the new <code>JFormattedTextField</code>. 970 * 971 * @param spinner the spinner whose model <code>this</code> editor will monitor 972 * @exception IllegalArgumentException if the spinners model is not 973 * an instance of <code>SpinnerDateModel</code> 974 * 975 * @see #getModel 976 * @see #getFormat 977 * @see SpinnerDateModel 978 */ 979 public DateEditor(JSpinner spinner) { 980 this(spinner, getDefaultPattern(spinner.getLocale())); 981 } 982 983 984 /** 985 * Construct a <code>JSpinner</code> editor that supports displaying 986 * and editing the value of a <code>SpinnerDateModel</code> 987 * with a <code>JFormattedTextField</code>. <code>This</code> 988 * <code>DateEditor</code> becomes both a <code>ChangeListener</code> 989 * on the spinner and a <code>PropertyChangeListener</code> 990 * on the new <code>JFormattedTextField</code>. 991 * 992 * @param spinner the spinner whose model <code>this</code> editor will monitor 993 * @param dateFormatPattern the initial pattern for the 994 * <code>SimpleDateFormat</code> object that's used to display 995 * and parse the value of the text field. 996 * @exception IllegalArgumentException if the spinners model is not 997 * an instance of <code>SpinnerDateModel</code> 998 * 999 * @see #getModel 1000 * @see #getFormat 1001 * @see SpinnerDateModel 1002 * @see java.text.SimpleDateFormat 1003 */ 1004 public DateEditor(JSpinner spinner, String dateFormatPattern) { 1005 this(spinner, new SimpleDateFormat(dateFormatPattern, 1006 spinner.getLocale())); 1007 } 1008 1009 /** 1010 * Construct a <code>JSpinner</code> editor that supports displaying 1011 * and editing the value of a <code>SpinnerDateModel</code> 1012 * with a <code>JFormattedTextField</code>. <code>This</code> 1013 * <code>DateEditor</code> becomes both a <code>ChangeListener</code> 1014 * on the spinner and a <code>PropertyChangeListener</code> 1015 * on the new <code>JFormattedTextField</code>. 1016 * 1017 * @param spinner the spinner whose model <code>this</code> editor 1018 * will monitor 1019 * @param format <code>DateFormat</code> object that's used to display 1020 * and parse the value of the text field. 1021 * @exception IllegalArgumentException if the spinners model is not 1022 * an instance of <code>SpinnerDateModel</code> 1023 * 1024 * @see #getModel 1025 * @see #getFormat 1026 * @see SpinnerDateModel 1027 * @see java.text.SimpleDateFormat 1028 */ 1029 private DateEditor(JSpinner spinner, DateFormat format) { 1030 super(spinner); 1031 if (!(spinner.getModel() instanceof SpinnerDateModel)) { 1032 throw new IllegalArgumentException( 1033 "model not a SpinnerDateModel"); 1034 } 1035 1036 SpinnerDateModel model = (SpinnerDateModel)spinner.getModel(); 1037 DateFormatter formatter = new DateEditorFormatter(model, format); 1038 DefaultFormatterFactory factory = new DefaultFormatterFactory( 1039 formatter); 1040 JFormattedTextField ftf = getTextField(); 1041 ftf.setEditable(true); 1042 ftf.setFormatterFactory(factory); 1043 1044 /* TBD - initializing the column width of the text field 1045 * is imprecise and doing it here is tricky because 1046 * the developer may configure the formatter later. 1047 */ 1048 try { 1049 String maxString = formatter.valueToString(model.getStart()); 1050 String minString = formatter.valueToString(model.getEnd()); 1051 ftf.setColumns(Math.max(maxString.length(), 1052 minString.length())); 1053 } 1054 catch (ParseException e) { 1055 // PENDING: hmuller 1056 } 1057 } 1058 1059 /** 1060 * Returns the <code>java.text.SimpleDateFormat</code> object the 1061 * <code>JFormattedTextField</code> uses to parse and format 1062 * numbers. 1063 * 1064 * @return the value of <code>getTextField().getFormatter().getFormat()</code>. 1065 * @see #getTextField 1066 * @see java.text.SimpleDateFormat 1067 */ 1068 public SimpleDateFormat getFormat() { 1069 return (SimpleDateFormat)((DateFormatter)(getTextField().getFormatter())).getFormat(); 1070 } 1071 1072 1073 /** 1074 * Return our spinner ancestor's <code>SpinnerDateModel</code>. 1075 * 1076 * @return <code>getSpinner().getModel()</code> 1077 * @see #getSpinner 1078 * @see #getTextField 1079 */ 1080 public SpinnerDateModel getModel() { 1081 return (SpinnerDateModel)(getSpinner().getModel()); 1082 } 1083 } 1084 1085 1086 /** 1087 * This subclass of javax.swing.NumberFormatter maps the minimum/maximum 1088 * properties to a SpinnerNumberModel and initializes the valueClass 1089 * of the NumberFormatter to match the type of the initial models value. 1090 */ 1091 private static class NumberEditorFormatter extends NumberFormatter { 1092 private final SpinnerNumberModel model; 1093 1094 NumberEditorFormatter(SpinnerNumberModel model, NumberFormat format) { 1095 super(format); 1096 this.model = model; 1097 setValueClass(model.getValue().getClass()); 1098 } 1099 1100 @Override 1101 public void setMinimum(Comparable<?> min) { 1102 model.setMinimum(min); 1103 } 1104 1105 @Override 1106 public Comparable<?> getMinimum() { 1107 return model.getMinimum(); 1108 } 1109 1110 @Override 1111 public void setMaximum(Comparable<?> max) { 1112 model.setMaximum(max); 1113 } 1114 1115 @Override 1116 public Comparable<?> getMaximum() { 1117 return model.getMaximum(); 1118 } 1119 } 1120 1121 1122 1123 /** 1124 * An editor for a <code>JSpinner</code> whose model is a 1125 * <code>SpinnerNumberModel</code>. The value of the editor is 1126 * displayed with a <code>JFormattedTextField</code> whose format 1127 * is defined by a <code>NumberFormatter</code> instance whose 1128 * <code>minimum</code> and <code>maximum</code> properties 1129 * are mapped to the <code>SpinnerNumberModel</code>. 1130 * @since 1.4 1131 */ 1132 // PENDING(hmuller): more example javadoc 1133 public static class NumberEditor extends DefaultEditor 1134 { 1135 // This is here until DecimalFormat gets a constructor that 1136 // takes a Locale: 4923525 1137 private static String getDefaultPattern(Locale locale) { 1138 // Get the pattern for the default locale. 1139 LocaleProviderAdapter adapter; 1140 adapter = LocaleProviderAdapter.getAdapter(NumberFormatProvider.class, 1141 locale); 1142 LocaleResources lr = adapter.getLocaleResources(locale); 1143 if (lr == null) { 1144 lr = LocaleProviderAdapter.forJRE().getLocaleResources(locale); 1145 } 1146 String[] all = lr.getNumberPatterns(); 1147 return all[0]; 1148 } 1149 1150 /** 1151 * Construct a <code>JSpinner</code> editor that supports displaying 1152 * and editing the value of a <code>SpinnerNumberModel</code> 1153 * with a <code>JFormattedTextField</code>. <code>This</code> 1154 * <code>NumberEditor</code> becomes both a <code>ChangeListener</code> 1155 * on the spinner and a <code>PropertyChangeListener</code> 1156 * on the new <code>JFormattedTextField</code>. 1157 * 1158 * @param spinner the spinner whose model <code>this</code> editor will monitor 1159 * @exception IllegalArgumentException if the spinners model is not 1160 * an instance of <code>SpinnerNumberModel</code> 1161 * 1162 * @see #getModel 1163 * @see #getFormat 1164 * @see SpinnerNumberModel 1165 */ 1166 public NumberEditor(JSpinner spinner) { 1167 this(spinner, getDefaultPattern(spinner.getLocale())); 1168 } 1169 1170 /** 1171 * Construct a <code>JSpinner</code> editor that supports displaying 1172 * and editing the value of a <code>SpinnerNumberModel</code> 1173 * with a <code>JFormattedTextField</code>. <code>This</code> 1174 * <code>NumberEditor</code> becomes both a <code>ChangeListener</code> 1175 * on the spinner and a <code>PropertyChangeListener</code> 1176 * on the new <code>JFormattedTextField</code>. 1177 * 1178 * @param spinner the spinner whose model <code>this</code> editor will monitor 1179 * @param decimalFormatPattern the initial pattern for the 1180 * <code>DecimalFormat</code> object that's used to display 1181 * and parse the value of the text field. 1182 * @exception IllegalArgumentException if the spinners model is not 1183 * an instance of <code>SpinnerNumberModel</code> or if 1184 * <code>decimalFormatPattern</code> is not a legal 1185 * argument to <code>DecimalFormat</code> 1186 * 1187 * @see #getTextField 1188 * @see SpinnerNumberModel 1189 * @see java.text.DecimalFormat 1190 */ 1191 public NumberEditor(JSpinner spinner, String decimalFormatPattern) { 1192 this(spinner, new DecimalFormat(decimalFormatPattern)); 1193 } 1194 1195 1196 /** 1197 * Construct a <code>JSpinner</code> editor that supports displaying 1198 * and editing the value of a <code>SpinnerNumberModel</code> 1199 * with a <code>JFormattedTextField</code>. <code>This</code> 1200 * <code>NumberEditor</code> becomes both a <code>ChangeListener</code> 1201 * on the spinner and a <code>PropertyChangeListener</code> 1202 * on the new <code>JFormattedTextField</code>. 1203 * 1204 * @param spinner the spinner whose model <code>this</code> editor will monitor 1205 * @param decimalFormatPattern the initial pattern for the 1206 * <code>DecimalFormat</code> object that's used to display 1207 * and parse the value of the text field. 1208 * @exception IllegalArgumentException if the spinners model is not 1209 * an instance of <code>SpinnerNumberModel</code> 1210 * 1211 * @see #getTextField 1212 * @see SpinnerNumberModel 1213 * @see java.text.DecimalFormat 1214 */ 1215 private NumberEditor(JSpinner spinner, DecimalFormat format) { 1216 super(spinner); 1217 if (!(spinner.getModel() instanceof SpinnerNumberModel)) { 1218 throw new IllegalArgumentException( 1219 "model not a SpinnerNumberModel"); 1220 } 1221 1222 SpinnerNumberModel model = (SpinnerNumberModel)spinner.getModel(); 1223 NumberFormatter formatter = new NumberEditorFormatter(model, 1224 format); 1225 DefaultFormatterFactory factory = new DefaultFormatterFactory( 1226 formatter); 1227 JFormattedTextField ftf = getTextField(); 1228 ftf.setEditable(true); 1229 ftf.setFormatterFactory(factory); 1230 // Change the text orientation for the NumberEditor 1231 ftf.setHorizontalAlignment(JTextField.RIGHT); 1232 1233 /* TBD - initializing the column width of the text field 1234 * is imprecise and doing it here is tricky because 1235 * the developer may configure the formatter later. 1236 */ 1237 try { 1238 String maxString = formatter.valueToString(model.getMinimum()); 1239 String minString = formatter.valueToString(model.getMaximum()); 1240 ftf.setColumns(Math.max(maxString.length(), 1241 minString.length())); 1242 } 1243 catch (ParseException e) { 1244 // TBD should throw a chained error here 1245 } 1246 1247 } 1248 1249 1250 /** 1251 * Returns the <code>java.text.DecimalFormat</code> object the 1252 * <code>JFormattedTextField</code> uses to parse and format 1253 * numbers. 1254 * 1255 * @return the value of <code>getTextField().getFormatter().getFormat()</code>. 1256 * @see #getTextField 1257 * @see java.text.DecimalFormat 1258 */ 1259 public DecimalFormat getFormat() { 1260 return (DecimalFormat)((NumberFormatter)(getTextField().getFormatter())).getFormat(); 1261 } 1262 1263 1264 /** 1265 * Return our spinner ancestor's <code>SpinnerNumberModel</code>. 1266 * 1267 * @return <code>getSpinner().getModel()</code> 1268 * @see #getSpinner 1269 * @see #getTextField 1270 */ 1271 public SpinnerNumberModel getModel() { 1272 return (SpinnerNumberModel)(getSpinner().getModel()); 1273 } 1274 1275 /** 1276 * {@inheritDoc} 1277 */ 1278 @Override 1279 public void setComponentOrientation(ComponentOrientation o) { 1280 super.setComponentOrientation(o); 1281 getTextField().setHorizontalAlignment( 1282 o.isLeftToRight() ? JTextField.RIGHT : JTextField.LEFT); 1283 } 1284 } 1285 1286 1287 /** 1288 * An editor for a <code>JSpinner</code> whose model is a 1289 * <code>SpinnerListModel</code>. 1290 * @since 1.4 1291 */ 1292 public static class ListEditor extends DefaultEditor 1293 { 1294 /** 1295 * Construct a <code>JSpinner</code> editor that supports displaying 1296 * and editing the value of a <code>SpinnerListModel</code> 1297 * with a <code>JFormattedTextField</code>. <code>This</code> 1298 * <code>ListEditor</code> becomes both a <code>ChangeListener</code> 1299 * on the spinner and a <code>PropertyChangeListener</code> 1300 * on the new <code>JFormattedTextField</code>. 1301 * 1302 * @param spinner the spinner whose model <code>this</code> editor will monitor 1303 * @exception IllegalArgumentException if the spinners model is not 1304 * an instance of <code>SpinnerListModel</code> 1305 * 1306 * @see #getModel 1307 * @see SpinnerListModel 1308 */ 1309 public ListEditor(JSpinner spinner) { 1310 super(spinner); 1311 if (!(spinner.getModel() instanceof SpinnerListModel)) { 1312 throw new IllegalArgumentException("model not a SpinnerListModel"); 1313 } 1314 getTextField().setEditable(true); 1315 getTextField().setFormatterFactory(new 1316 DefaultFormatterFactory(new ListFormatter())); 1317 } 1318 1319 /** 1320 * Return our spinner ancestor's <code>SpinnerNumberModel</code>. 1321 * 1322 * @return <code>getSpinner().getModel()</code> 1323 * @see #getSpinner 1324 * @see #getTextField 1325 */ 1326 public SpinnerListModel getModel() { 1327 return (SpinnerListModel)(getSpinner().getModel()); 1328 } 1329 1330 1331 /** 1332 * ListFormatter provides completion while text is being input 1333 * into the JFormattedTextField. Completion is only done if the 1334 * user is inserting text at the end of the document. Completion 1335 * is done by way of the SpinnerListModel method findNextMatch. 1336 */ 1337 private class ListFormatter extends 1338 JFormattedTextField.AbstractFormatter { 1339 private DocumentFilter filter; 1340 1341 public String valueToString(Object value) throws ParseException { 1342 if (value == null) { 1343 return ""; 1344 } 1345 return value.toString(); 1346 } 1347 1348 public Object stringToValue(String string) throws ParseException { 1349 return string; 1350 } 1351 1352 protected DocumentFilter getDocumentFilter() { 1353 if (filter == null) { 1354 filter = new Filter(); 1355 } 1356 return filter; 1357 } 1358 1359 1360 private class Filter extends DocumentFilter { 1361 public void replace(FilterBypass fb, int offset, int length, 1362 String string, AttributeSet attrs) throws 1363 BadLocationException { 1364 if (string != null && (offset + length) == 1365 fb.getDocument().getLength()) { 1366 Object next = getModel().findNextMatch( 1367 fb.getDocument().getText(0, offset) + 1368 string); 1369 String value = (next != null) ? next.toString() : null; 1370 1371 if (value != null) { 1372 fb.remove(0, offset + length); 1373 fb.insertString(0, value, null); 1374 getFormattedTextField().select(offset + 1375 string.length(), 1376 value.length()); 1377 return; 1378 } 1379 } 1380 super.replace(fb, offset, length, string, attrs); 1381 } 1382 1383 public void insertString(FilterBypass fb, int offset, 1384 String string, AttributeSet attr) 1385 throws BadLocationException { 1386 replace(fb, offset, 0, string, attr); 1387 } 1388 } 1389 } 1390 } 1391 1392 1393 /** 1394 * An Action implementation that is always disabled. 1395 */ 1396 private static class DisabledAction implements Action { 1397 public Object getValue(String key) { 1398 return null; 1399 } 1400 public void putValue(String key, Object value) { 1401 } 1402 public void setEnabled(boolean b) { 1403 } 1404 public boolean isEnabled() { 1405 return false; 1406 } 1407 public void addPropertyChangeListener(PropertyChangeListener l) { 1408 } 1409 public void removePropertyChangeListener(PropertyChangeListener l) { 1410 } 1411 public void actionPerformed(ActionEvent ae) { 1412 } 1413 } 1414 1415 ///////////////// 1416 // Accessibility support 1417 //////////////// 1418 1419 /** 1420 * Gets the <code>AccessibleContext</code> for the <code>JSpinner</code> 1421 * 1422 * @return the <code>AccessibleContext</code> for the <code>JSpinner</code> 1423 * @since 1.5 1424 */ 1425 @BeanProperty(bound = false) 1426 public AccessibleContext getAccessibleContext() { 1427 if (accessibleContext == null) { 1428 accessibleContext = new AccessibleJSpinner(); 1429 } 1430 return accessibleContext; 1431 } 1432 1433 /** 1434 * <code>AccessibleJSpinner</code> implements accessibility 1435 * support for the <code>JSpinner</code> class. 1436 * @since 1.5 1437 */ 1438 protected class AccessibleJSpinner extends AccessibleJComponent 1439 implements AccessibleValue, AccessibleAction, AccessibleText, 1440 AccessibleEditableText, ChangeListener { 1441 1442 private Object oldModelValue = null; 1443 1444 /** 1445 * AccessibleJSpinner constructor 1446 */ 1447 protected AccessibleJSpinner() { 1448 // model is guaranteed to be non-null 1449 oldModelValue = model.getValue(); 1450 JSpinner.this.addChangeListener(this); 1451 } 1452 1453 /** 1454 * Invoked when the target of the listener has changed its state. 1455 * 1456 * @param e a <code>ChangeEvent</code> object. Must not be null. 1457 * @throws NullPointerException if the parameter is null. 1458 */ 1459 public void stateChanged(ChangeEvent e) { 1460 if (e == null) { 1461 throw new NullPointerException(); 1462 } 1463 Object newModelValue = model.getValue(); 1464 firePropertyChange(ACCESSIBLE_VALUE_PROPERTY, 1465 oldModelValue, 1466 newModelValue); 1467 firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, 1468 null, 1469 0); // entire text may have changed 1470 1471 oldModelValue = newModelValue; 1472 } 1473 1474 /* ===== Begin AccessibleContext methods ===== */ 1475 1476 /** 1477 * Gets the role of this object. The role of the object is the generic 1478 * purpose or use of the class of this object. For example, the role 1479 * of a push button is AccessibleRole.PUSH_BUTTON. The roles in 1480 * AccessibleRole are provided so component developers can pick from 1481 * a set of predefined roles. This enables assistive technologies to 1482 * provide a consistent interface to various tweaked subclasses of 1483 * components (e.g., use AccessibleRole.PUSH_BUTTON for all components 1484 * that act like a push button) as well as distinguish between subclasses 1485 * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes 1486 * and AccessibleRole.RADIO_BUTTON for radio buttons). 1487 * <p>Note that the AccessibleRole class is also extensible, so 1488 * custom component developers can define their own AccessibleRole's 1489 * if the set of predefined roles is inadequate. 1490 * 1491 * @return an instance of AccessibleRole describing the role of the object 1492 * @see AccessibleRole 1493 */ 1494 public AccessibleRole getAccessibleRole() { 1495 return AccessibleRole.SPIN_BOX; 1496 } 1497 1498 /** 1499 * Returns the number of accessible children of the object. 1500 * 1501 * @return the number of accessible children of the object. 1502 */ 1503 public int getAccessibleChildrenCount() { 1504 // the JSpinner has one child, the editor 1505 if (editor.getAccessibleContext() != null) { 1506 return 1; 1507 } 1508 return 0; 1509 } 1510 1511 /** 1512 * Returns the specified Accessible child of the object. The Accessible 1513 * children of an Accessible object are zero-based, so the first child 1514 * of an Accessible child is at index 0, the second child is at index 1, 1515 * and so on. 1516 * 1517 * @param i zero-based index of child 1518 * @return the Accessible child of the object 1519 * @see #getAccessibleChildrenCount 1520 */ 1521 public Accessible getAccessibleChild(int i) { 1522 // the JSpinner has one child, the editor 1523 if (i != 0) { 1524 return null; 1525 } 1526 if (editor.getAccessibleContext() != null) { 1527 return (Accessible)editor; 1528 } 1529 return null; 1530 } 1531 1532 /* ===== End AccessibleContext methods ===== */ 1533 1534 /** 1535 * Gets the AccessibleAction associated with this object that supports 1536 * one or more actions. 1537 * 1538 * @return AccessibleAction if supported by object; else return null 1539 * @see AccessibleAction 1540 */ 1541 public AccessibleAction getAccessibleAction() { 1542 return this; 1543 } 1544 1545 /** 1546 * Gets the AccessibleText associated with this object presenting 1547 * text on the display. 1548 * 1549 * @return AccessibleText if supported by object; else return null 1550 * @see AccessibleText 1551 */ 1552 public AccessibleText getAccessibleText() { 1553 return this; 1554 } 1555 1556 /* 1557 * Returns the AccessibleContext for the JSpinner editor 1558 */ 1559 private AccessibleContext getEditorAccessibleContext() { 1560 if (editor instanceof DefaultEditor) { 1561 JTextField textField = ((DefaultEditor)editor).getTextField(); 1562 if (textField != null) { 1563 return textField.getAccessibleContext(); 1564 } 1565 } else if (editor instanceof Accessible) { 1566 return editor.getAccessibleContext(); 1567 } 1568 return null; 1569 } 1570 1571 /* 1572 * Returns the AccessibleText for the JSpinner editor 1573 */ 1574 private AccessibleText getEditorAccessibleText() { 1575 AccessibleContext ac = getEditorAccessibleContext(); 1576 if (ac != null) { 1577 return ac.getAccessibleText(); 1578 } 1579 return null; 1580 } 1581 1582 /* 1583 * Returns the AccessibleEditableText for the JSpinner editor 1584 */ 1585 private AccessibleEditableText getEditorAccessibleEditableText() { 1586 AccessibleText at = getEditorAccessibleText(); 1587 if (at instanceof AccessibleEditableText) { 1588 return (AccessibleEditableText)at; 1589 } 1590 return null; 1591 } 1592 1593 /** 1594 * Gets the AccessibleValue associated with this object. 1595 * 1596 * @return AccessibleValue if supported by object; else return null 1597 * @see AccessibleValue 1598 * 1599 */ 1600 public AccessibleValue getAccessibleValue() { 1601 return this; 1602 } 1603 1604 /* ===== Begin AccessibleValue impl ===== */ 1605 1606 /** 1607 * Get the value of this object as a Number. If the value has not been 1608 * set, the return value will be null. 1609 * 1610 * @return value of the object 1611 * @see #setCurrentAccessibleValue 1612 */ 1613 public Number getCurrentAccessibleValue() { 1614 Object o = model.getValue(); 1615 if (o instanceof Number) { 1616 return (Number)o; 1617 } 1618 return null; 1619 } 1620 1621 /** 1622 * Set the value of this object as a Number. 1623 * 1624 * @param n the value to set for this object 1625 * @return true if the value was set; else False 1626 * @see #getCurrentAccessibleValue 1627 */ 1628 public boolean setCurrentAccessibleValue(Number n) { 1629 // try to set the new value 1630 try { 1631 model.setValue(n); 1632 return true; 1633 } catch (IllegalArgumentException iae) { 1634 // SpinnerModel didn't like new value 1635 } 1636 return false; 1637 } 1638 1639 /** 1640 * Get the minimum value of this object as a Number. 1641 * 1642 * @return Minimum value of the object; null if this object does not 1643 * have a minimum value 1644 * @see #getMaximumAccessibleValue 1645 */ 1646 public Number getMinimumAccessibleValue() { 1647 if (model instanceof SpinnerNumberModel) { 1648 SpinnerNumberModel numberModel = (SpinnerNumberModel)model; 1649 Object o = numberModel.getMinimum(); 1650 if (o instanceof Number) { 1651 return (Number)o; 1652 } 1653 } 1654 return null; 1655 } 1656 1657 /** 1658 * Get the maximum value of this object as a Number. 1659 * 1660 * @return Maximum value of the object; null if this object does not 1661 * have a maximum value 1662 * @see #getMinimumAccessibleValue 1663 */ 1664 public Number getMaximumAccessibleValue() { 1665 if (model instanceof SpinnerNumberModel) { 1666 SpinnerNumberModel numberModel = (SpinnerNumberModel)model; 1667 Object o = numberModel.getMaximum(); 1668 if (o instanceof Number) { 1669 return (Number)o; 1670 } 1671 } 1672 return null; 1673 } 1674 1675 /* ===== End AccessibleValue impl ===== */ 1676 1677 /* ===== Begin AccessibleAction impl ===== */ 1678 1679 /** 1680 * Returns the number of accessible actions available in this object 1681 * If there are more than one, the first one is considered the "default" 1682 * action of the object. 1683 * 1684 * Two actions are supported: AccessibleAction.INCREMENT which 1685 * increments the spinner value and AccessibleAction.DECREMENT 1686 * which decrements the spinner value 1687 * 1688 * @return the zero-based number of Actions in this object 1689 */ 1690 public int getAccessibleActionCount() { 1691 return 2; 1692 } 1693 1694 /** 1695 * Returns a description of the specified action of the object. 1696 * 1697 * @param i zero-based index of the actions 1698 * @return a String description of the action 1699 * @see #getAccessibleActionCount 1700 */ 1701 public String getAccessibleActionDescription(int i) { 1702 if (i == 0) { 1703 return AccessibleAction.INCREMENT; 1704 } else if (i == 1) { 1705 return AccessibleAction.DECREMENT; 1706 } 1707 return null; 1708 } 1709 1710 /** 1711 * Performs the specified Action on the object 1712 * 1713 * @param i zero-based index of actions. The first action 1714 * (index 0) is AccessibleAction.INCREMENT and the second 1715 * action (index 1) is AccessibleAction.DECREMENT. 1716 * @return true if the action was performed; otherwise false. 1717 * @see #getAccessibleActionCount 1718 */ 1719 public boolean doAccessibleAction(int i) { 1720 if (i < 0 || i > 1) { 1721 return false; 1722 } 1723 Object o; 1724 if (i == 0) { 1725 o = getNextValue(); // AccessibleAction.INCREMENT 1726 } else { 1727 o = getPreviousValue(); // AccessibleAction.DECREMENT 1728 } 1729 // try to set the new value 1730 try { 1731 model.setValue(o); 1732 return true; 1733 } catch (IllegalArgumentException iae) { 1734 // SpinnerModel didn't like new value 1735 } 1736 return false; 1737 } 1738 1739 /* ===== End AccessibleAction impl ===== */ 1740 1741 /* ===== Begin AccessibleText impl ===== */ 1742 1743 /* 1744 * Returns whether source and destination components have the 1745 * same window ancestor 1746 */ 1747 private boolean sameWindowAncestor(Component src, Component dest) { 1748 if (src == null || dest == null) { 1749 return false; 1750 } 1751 return SwingUtilities.getWindowAncestor(src) == 1752 SwingUtilities.getWindowAncestor(dest); 1753 } 1754 1755 /** 1756 * Given a point in local coordinates, return the zero-based index 1757 * of the character under that Point. If the point is invalid, 1758 * this method returns -1. 1759 * 1760 * @param p the Point in local coordinates 1761 * @return the zero-based index of the character under Point p; if 1762 * Point is invalid return -1. 1763 */ 1764 public int getIndexAtPoint(Point p) { 1765 AccessibleText at = getEditorAccessibleText(); 1766 if (at != null && sameWindowAncestor(JSpinner.this, editor)) { 1767 // convert point from the JSpinner bounds (source) to 1768 // editor bounds (destination) 1769 Point editorPoint = SwingUtilities.convertPoint(JSpinner.this, 1770 p, 1771 editor); 1772 if (editorPoint != null) { 1773 return at.getIndexAtPoint(editorPoint); 1774 } 1775 } 1776 return -1; 1777 } 1778 1779 /** 1780 * Determines the bounding box of the character at the given 1781 * index into the string. The bounds are returned in local 1782 * coordinates. If the index is invalid an empty rectangle is 1783 * returned. 1784 * 1785 * @param i the index into the String 1786 * @return the screen coordinates of the character's bounding box, 1787 * if index is invalid return an empty rectangle. 1788 */ 1789 public Rectangle getCharacterBounds(int i) { 1790 AccessibleText at = getEditorAccessibleText(); 1791 if (at != null ) { 1792 Rectangle editorRect = at.getCharacterBounds(i); 1793 if (editorRect != null && 1794 sameWindowAncestor(JSpinner.this, editor)) { 1795 // return rectangle in the JSpinner bounds 1796 return SwingUtilities.convertRectangle(editor, 1797 editorRect, 1798 JSpinner.this); 1799 } 1800 } 1801 return null; 1802 } 1803 1804 /** 1805 * Returns the number of characters (valid indicies) 1806 * 1807 * @return the number of characters 1808 */ 1809 public int getCharCount() { 1810 AccessibleText at = getEditorAccessibleText(); 1811 if (at != null) { 1812 return at.getCharCount(); 1813 } 1814 return -1; 1815 } 1816 1817 /** 1818 * Returns the zero-based offset of the caret. 1819 * 1820 * Note: That to the right of the caret will have the same index 1821 * value as the offset (the caret is between two characters). 1822 * @return the zero-based offset of the caret. 1823 */ 1824 public int getCaretPosition() { 1825 AccessibleText at = getEditorAccessibleText(); 1826 if (at != null) { 1827 return at.getCaretPosition(); 1828 } 1829 return -1; 1830 } 1831 1832 /** 1833 * Returns the String at a given index. 1834 * 1835 * @param part the CHARACTER, WORD, or SENTENCE to retrieve 1836 * @param index an index within the text 1837 * @return the letter, word, or sentence 1838 */ 1839 public String getAtIndex(int part, int index) { 1840 AccessibleText at = getEditorAccessibleText(); 1841 if (at != null) { 1842 return at.getAtIndex(part, index); 1843 } 1844 return null; 1845 } 1846 1847 /** 1848 * Returns the String after a given index. 1849 * 1850 * @param part the CHARACTER, WORD, or SENTENCE to retrieve 1851 * @param index an index within the text 1852 * @return the letter, word, or sentence 1853 */ 1854 public String getAfterIndex(int part, int index) { 1855 AccessibleText at = getEditorAccessibleText(); 1856 if (at != null) { 1857 return at.getAfterIndex(part, index); 1858 } 1859 return null; 1860 } 1861 1862 /** 1863 * Returns the String before a given index. 1864 * 1865 * @param part the CHARACTER, WORD, or SENTENCE to retrieve 1866 * @param index an index within the text 1867 * @return the letter, word, or sentence 1868 */ 1869 public String getBeforeIndex(int part, int index) { 1870 AccessibleText at = getEditorAccessibleText(); 1871 if (at != null) { 1872 return at.getBeforeIndex(part, index); 1873 } 1874 return null; 1875 } 1876 1877 /** 1878 * Returns the AttributeSet for a given character at a given index 1879 * 1880 * @param i the zero-based index into the text 1881 * @return the AttributeSet of the character 1882 */ 1883 public AttributeSet getCharacterAttribute(int i) { 1884 AccessibleText at = getEditorAccessibleText(); 1885 if (at != null) { 1886 return at.getCharacterAttribute(i); 1887 } 1888 return null; 1889 } 1890 1891 /** 1892 * Returns the start offset within the selected text. 1893 * If there is no selection, but there is 1894 * a caret, the start and end offsets will be the same. 1895 * 1896 * @return the index into the text of the start of the selection 1897 */ 1898 public int getSelectionStart() { 1899 AccessibleText at = getEditorAccessibleText(); 1900 if (at != null) { 1901 return at.getSelectionStart(); 1902 } 1903 return -1; 1904 } 1905 1906 /** 1907 * Returns the end offset within the selected text. 1908 * If there is no selection, but there is 1909 * a caret, the start and end offsets will be the same. 1910 * 1911 * @return the index into the text of the end of the selection 1912 */ 1913 public int getSelectionEnd() { 1914 AccessibleText at = getEditorAccessibleText(); 1915 if (at != null) { 1916 return at.getSelectionEnd(); 1917 } 1918 return -1; 1919 } 1920 1921 /** 1922 * Returns the portion of the text that is selected. 1923 * 1924 * @return the String portion of the text that is selected 1925 */ 1926 public String getSelectedText() { 1927 AccessibleText at = getEditorAccessibleText(); 1928 if (at != null) { 1929 return at.getSelectedText(); 1930 } 1931 return null; 1932 } 1933 1934 /* ===== End AccessibleText impl ===== */ 1935 1936 1937 /* ===== Begin AccessibleEditableText impl ===== */ 1938 1939 /** 1940 * Sets the text contents to the specified string. 1941 * 1942 * @param s the string to set the text contents 1943 */ 1944 public void setTextContents(String s) { 1945 AccessibleEditableText at = getEditorAccessibleEditableText(); 1946 if (at != null) { 1947 at.setTextContents(s); 1948 } 1949 } 1950 1951 /** 1952 * Inserts the specified string at the given index/ 1953 * 1954 * @param index the index in the text where the string will 1955 * be inserted 1956 * @param s the string to insert in the text 1957 */ 1958 public void insertTextAtIndex(int index, String s) { 1959 AccessibleEditableText at = getEditorAccessibleEditableText(); 1960 if (at != null) { 1961 at.insertTextAtIndex(index, s); 1962 } 1963 } 1964 1965 /** 1966 * Returns the text string between two indices. 1967 * 1968 * @param startIndex the starting index in the text 1969 * @param endIndex the ending index in the text 1970 * @return the text string between the indices 1971 */ 1972 public String getTextRange(int startIndex, int endIndex) { 1973 AccessibleEditableText at = getEditorAccessibleEditableText(); 1974 if (at != null) { 1975 return at.getTextRange(startIndex, endIndex); 1976 } 1977 return null; 1978 } 1979 1980 /** 1981 * Deletes the text between two indices 1982 * 1983 * @param startIndex the starting index in the text 1984 * @param endIndex the ending index in the text 1985 */ 1986 public void delete(int startIndex, int endIndex) { 1987 AccessibleEditableText at = getEditorAccessibleEditableText(); 1988 if (at != null) { 1989 at.delete(startIndex, endIndex); 1990 } 1991 } 1992 1993 /** 1994 * Cuts the text between two indices into the system clipboard. 1995 * 1996 * @param startIndex the starting index in the text 1997 * @param endIndex the ending index in the text 1998 */ 1999 public void cut(int startIndex, int endIndex) { 2000 AccessibleEditableText at = getEditorAccessibleEditableText(); 2001 if (at != null) { 2002 at.cut(startIndex, endIndex); 2003 } 2004 } 2005 2006 /** 2007 * Pastes the text from the system clipboard into the text 2008 * starting at the specified index. 2009 * 2010 * @param startIndex the starting index in the text 2011 */ 2012 public void paste(int startIndex) { 2013 AccessibleEditableText at = getEditorAccessibleEditableText(); 2014 if (at != null) { 2015 at.paste(startIndex); 2016 } 2017 } 2018 2019 /** 2020 * Replaces the text between two indices with the specified 2021 * string. 2022 * 2023 * @param startIndex the starting index in the text 2024 * @param endIndex the ending index in the text 2025 * @param s the string to replace the text between two indices 2026 */ 2027 public void replaceText(int startIndex, int endIndex, String s) { 2028 AccessibleEditableText at = getEditorAccessibleEditableText(); 2029 if (at != null) { 2030 at.replaceText(startIndex, endIndex, s); 2031 } 2032 } 2033 2034 /** 2035 * Selects the text between two indices. 2036 * 2037 * @param startIndex the starting index in the text 2038 * @param endIndex the ending index in the text 2039 */ 2040 public void selectText(int startIndex, int endIndex) { 2041 AccessibleEditableText at = getEditorAccessibleEditableText(); 2042 if (at != null) { 2043 at.selectText(startIndex, endIndex); 2044 } 2045 } 2046 2047 /** 2048 * Sets attributes for the text between two indices. 2049 * 2050 * @param startIndex the starting index in the text 2051 * @param endIndex the ending index in the text 2052 * @param as the attribute set 2053 * @see AttributeSet 2054 */ 2055 public void setAttributes(int startIndex, int endIndex, AttributeSet as) { 2056 AccessibleEditableText at = getEditorAccessibleEditableText(); 2057 if (at != null) { 2058 at.setAttributes(startIndex, endIndex, as); 2059 } 2060 } 2061 } /* End AccessibleJSpinner */ 2062 }