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