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