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