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