Frames | No Frames |
1: /* JSpinner.java -- 2: Copyright (C) 2004, 2005, 2006 Free Software Foundation, Inc. 3: 4: This file is part of GNU Classpath. 5: 6: GNU Classpath is free software; you can redistribute it and/or modify 7: it under the terms of the GNU General Public License as published by 8: the Free Software Foundation; either version 2, or (at your option) 9: any later version. 10: 11: GNU Classpath is distributed in the hope that it will be useful, but 12: WITHOUT ANY WARRANTY; without even the implied warranty of 13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14: General Public License for more details. 15: 16: You should have received a copy of the GNU General Public License 17: along with GNU Classpath; see the file COPYING. If not, write to the 18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19: 02110-1301 USA. 20: 21: Linking this library statically or dynamically with other modules is 22: making a combined work based on this library. Thus, the terms and 23: conditions of the GNU General Public License cover the whole 24: combination. 25: 26: As a special exception, the copyright holders of this library give you 27: permission to link this library with independent modules to produce an 28: executable, regardless of the license terms of these independent 29: modules, and to copy and distribute the resulting executable under 30: terms of your choice, provided that you also meet, for each linked 31: independent module, the terms and conditions of the license of that 32: module. An independent module is a module which is not derived from 33: or based on this library. If you modify this library, you may extend 34: this exception to your version of the library, but you are not 35: obligated to do so. If you do not wish to do so, delete this 36: exception statement from your version. */ 37: 38: 39: package javax.swing; 40: 41: import java.awt.Component; 42: import java.awt.Container; 43: import java.awt.Dimension; 44: import java.awt.Insets; 45: import java.awt.LayoutManager; 46: import java.beans.PropertyChangeEvent; 47: import java.beans.PropertyChangeListener; 48: import java.text.DateFormat; 49: import java.text.DecimalFormat; 50: import java.text.NumberFormat; 51: import java.text.ParseException; 52: import java.text.SimpleDateFormat; 53: 54: import javax.swing.event.ChangeEvent; 55: import javax.swing.event.ChangeListener; 56: import javax.swing.plaf.SpinnerUI; 57: import javax.swing.text.DateFormatter; 58: import javax.swing.text.DefaultFormatterFactory; 59: import javax.swing.text.NumberFormatter; 60: 61: /** 62: * A <code>JSpinner</code> is a component that displays a single value from 63: * a sequence of values, and provides a convenient means for selecting the 64: * previous and next values in the sequence. Typically the spinner displays 65: * a numeric value, but it is possible to display dates or arbitrary items 66: * from a list. 67: * 68: * @author Ka-Hing Cheung 69: * 70: * @since 1.4 71: */ 72: public class JSpinner extends JComponent 73: { 74: /** 75: * The base class for the editor used by the {@link JSpinner} component. 76: * The editor is in fact a panel containing a {@link JFormattedTextField} 77: * component. 78: */ 79: public static class DefaultEditor 80: extends JPanel 81: implements ChangeListener, PropertyChangeListener, LayoutManager 82: { 83: /** The spinner that the editor is allocated to. */ 84: private JSpinner spinner; 85: 86: /** The JFormattedTextField that backs the editor. */ 87: JFormattedTextField ftf; 88: 89: /** 90: * For compatability with Sun's JDK 1.4.2 rev. 5 91: */ 92: private static final long serialVersionUID = -5317788736173368172L; 93: 94: /** 95: * Creates a new <code>DefaultEditor</code> object. The editor is 96: * registered with the spinner as a {@link ChangeListener} here. 97: * 98: * @param spinner the <code>JSpinner</code> associated with this editor 99: */ 100: public DefaultEditor(JSpinner spinner) 101: { 102: super(); 103: setLayout(this); 104: this.spinner = spinner; 105: ftf = new JFormattedTextField(); 106: add(ftf); 107: ftf.setValue(spinner.getValue()); 108: ftf.addPropertyChangeListener(this); 109: if (getComponentOrientation().isLeftToRight()) 110: ftf.setHorizontalAlignment(JTextField.RIGHT); 111: else 112: ftf.setHorizontalAlignment(JTextField.LEFT); 113: spinner.addChangeListener(this); 114: } 115: 116: /** 117: * Returns the <code>JSpinner</code> component that the editor is assigned 118: * to. 119: * 120: * @return The spinner that the editor is assigned to. 121: */ 122: public JSpinner getSpinner() 123: { 124: return spinner; 125: } 126: 127: /** 128: * DOCUMENT ME! 129: */ 130: public void commitEdit() throws ParseException 131: { 132: // TODO: Implement this properly. 133: } 134: 135: /** 136: * Removes the editor from the {@link ChangeListener} list maintained by 137: * the specified <code>spinner</code>. 138: * 139: * @param spinner the spinner (<code>null</code> not permitted). 140: */ 141: public void dismiss(JSpinner spinner) 142: { 143: spinner.removeChangeListener(this); 144: } 145: 146: /** 147: * Returns the text field used to display and edit the current value in 148: * the spinner. 149: * 150: * @return The text field. 151: */ 152: public JFormattedTextField getTextField() 153: { 154: return ftf; 155: } 156: 157: /** 158: * Sets the bounds for the child components in this container. In this 159: * case, the text field is the only component to be laid out. 160: * 161: * @param parent the parent container. 162: */ 163: public void layoutContainer(Container parent) 164: { 165: Insets insets = getInsets(); 166: Dimension size = getSize(); 167: ftf.setBounds(insets.left, insets.top, 168: size.width - insets.left - insets.right, 169: size.height - insets.top - insets.bottom); 170: } 171: 172: /** 173: * Calculates the minimum size for this component. In this case, the 174: * text field is the only subcomponent, so the return value is the minimum 175: * size of the text field plus the insets of this component. 176: * 177: * @param parent the parent container. 178: * 179: * @return The minimum size. 180: */ 181: public Dimension minimumLayoutSize(Container parent) 182: { 183: Insets insets = getInsets(); 184: Dimension minSize = ftf.getMinimumSize(); 185: return new Dimension(minSize.width + insets.left + insets.right, 186: minSize.height + insets.top + insets.bottom); 187: } 188: 189: /** 190: * Calculates the preferred size for this component. In this case, the 191: * text field is the only subcomponent, so the return value is the 192: * preferred size of the text field plus the insets of this component. 193: * 194: * @param parent the parent container. 195: * 196: * @return The preferred size. 197: */ 198: public Dimension preferredLayoutSize(Container parent) 199: { 200: Insets insets = getInsets(); 201: Dimension prefSize = ftf.getPreferredSize(); 202: return new Dimension(prefSize.width + insets.left + insets.right, 203: prefSize.height + insets.top + insets.bottom); 204: } 205: 206: /** 207: * Receives notification of property changes. If the text field's 'value' 208: * property changes, the spinner's model is updated accordingly. 209: * 210: * @param event the event. 211: */ 212: public void propertyChange(PropertyChangeEvent event) 213: { 214: if (event.getSource() == ftf) 215: { 216: if (event.getPropertyName().equals("value")) 217: spinner.getModel().setValue(event.getNewValue()); 218: } 219: } 220: 221: /** 222: * Receives notification of changes in the state of the {@link JSpinner} 223: * that the editor belongs to - the content of the text field is updated 224: * accordingly. 225: * 226: * @param event the change event. 227: */ 228: public void stateChanged(ChangeEvent event) 229: { 230: ftf.setValue(spinner.getValue()); 231: } 232: 233: /** 234: * This method does nothing. It is required by the {@link LayoutManager} 235: * interface, but since this component has a single child, there is no 236: * need to use this method. 237: * 238: * @param child the child component to remove. 239: */ 240: public void removeLayoutComponent(Component child) 241: { 242: // Nothing to do here. 243: } 244: 245: /** 246: * This method does nothing. It is required by the {@link LayoutManager} 247: * interface, but since this component has a single child, there is no 248: * need to use this method. 249: * 250: * @param name the name. 251: * @param child the child component to add. 252: */ 253: public void addLayoutComponent(String name, Component child) 254: { 255: // Nothing to do here. 256: } 257: } 258: 259: /** 260: * A panel containing a {@link JFormattedTextField} that is configured for 261: * displaying and editing numbers. The panel is used as a subcomponent of 262: * a {@link JSpinner}. 263: * 264: * @see JSpinner#createEditor(SpinnerModel) 265: */ 266: public static class NumberEditor extends DefaultEditor 267: { 268: /** 269: * For compatability with Sun's JDK 270: */ 271: private static final long serialVersionUID = 3791956183098282942L; 272: 273: /** 274: * Creates a new <code>NumberEditor</code> object for the specified 275: * <code>spinner</code>. The editor is registered with the spinner as a 276: * {@link ChangeListener}. 277: * 278: * @param spinner the component the editor will be used with. 279: */ 280: public NumberEditor(JSpinner spinner) 281: { 282: super(spinner); 283: NumberEditorFormatter nef = new NumberEditorFormatter(); 284: nef.setMinimum(getModel().getMinimum()); 285: nef.setMaximum(getModel().getMaximum()); 286: ftf.setFormatterFactory(new DefaultFormatterFactory(nef)); 287: } 288: 289: /** 290: * Creates a new <code>NumberEditor</code> object. 291: * 292: * @param spinner the spinner. 293: * @param decimalFormatPattern the number format pattern. 294: */ 295: public NumberEditor(JSpinner spinner, String decimalFormatPattern) 296: { 297: super(spinner); 298: NumberEditorFormatter nef 299: = new NumberEditorFormatter(decimalFormatPattern); 300: nef.setMinimum(getModel().getMinimum()); 301: nef.setMaximum(getModel().getMaximum()); 302: ftf.setFormatterFactory(new DefaultFormatterFactory(nef)); 303: } 304: 305: /** 306: * Returns the format used by the text field. 307: * 308: * @return The format used by the text field. 309: */ 310: public DecimalFormat getFormat() 311: { 312: NumberFormatter formatter = (NumberFormatter) ftf.getFormatter(); 313: return (DecimalFormat) formatter.getFormat(); 314: } 315: 316: /** 317: * Returns the model used by the editor's {@link JSpinner} component, 318: * cast to a {@link SpinnerNumberModel}. 319: * 320: * @return The model. 321: */ 322: public SpinnerNumberModel getModel() 323: { 324: return (SpinnerNumberModel) getSpinner().getModel(); 325: } 326: } 327: 328: static class NumberEditorFormatter 329: extends NumberFormatter 330: { 331: public NumberEditorFormatter() 332: { 333: super(NumberFormat.getInstance()); 334: } 335: public NumberEditorFormatter(String decimalFormatPattern) 336: { 337: super(new DecimalFormat(decimalFormatPattern)); 338: } 339: } 340: 341: /** 342: * A <code>JSpinner</code> editor used for the {@link SpinnerListModel}. 343: * This editor uses a <code>JFormattedTextField</code> to edit the values 344: * of the spinner. 345: * 346: * @author Roman Kennke (kennke@aicas.com) 347: */ 348: public static class ListEditor extends DefaultEditor 349: { 350: /** 351: * Creates a new instance of <code>ListEditor</code>. 352: * 353: * @param spinner the spinner for which this editor is used 354: */ 355: public ListEditor(JSpinner spinner) 356: { 357: super(spinner); 358: } 359: 360: /** 361: * Returns the spinner's model cast as a {@link SpinnerListModel}. 362: * 363: * @return The spinner's model. 364: */ 365: public SpinnerListModel getModel() 366: { 367: return (SpinnerListModel) getSpinner().getModel(); 368: } 369: } 370: 371: /** 372: * An editor class for a <code>JSpinner</code> that is used 373: * for displaying and editing dates (e.g. that uses 374: * <code>SpinnerDateModel</code> as model). 375: * 376: * The editor uses a {@link JTextField} with the value 377: * displayed by a {@link DateFormatter} instance. 378: */ 379: public static class DateEditor extends DefaultEditor 380: { 381: 382: /** The serialVersionUID. */ 383: private static final long serialVersionUID = -4279356973770397815L; 384: 385: /** 386: * Creates a new instance of DateEditor for the specified 387: * <code>JSpinner</code>. 388: * 389: * @param spinner the <code>JSpinner</code> for which to 390: * create a <code>DateEditor</code> instance 391: */ 392: public DateEditor(JSpinner spinner) 393: { 394: super(spinner); 395: DateEditorFormatter nef = new DateEditorFormatter(); 396: nef.setMinimum(getModel().getStart()); 397: nef.setMaximum(getModel().getEnd()); 398: ftf.setFormatterFactory(new DefaultFormatterFactory(nef)); 399: } 400: 401: /** 402: * Creates a new instance of DateEditor for the specified 403: * <code>JSpinner</code> using the specified date format 404: * pattern. 405: * 406: * @param spinner the <code>JSpinner</code> for which to 407: * create a <code>DateEditor</code> instance 408: * @param dateFormatPattern the date format to use 409: * 410: * @see SimpleDateFormat#SimpleDateFormat(String) 411: */ 412: public DateEditor(JSpinner spinner, String dateFormatPattern) 413: { 414: super(spinner); 415: DateEditorFormatter nef = new DateEditorFormatter(dateFormatPattern); 416: nef.setMinimum(getModel().getStart()); 417: nef.setMaximum(getModel().getEnd()); 418: ftf.setFormatterFactory(new DefaultFormatterFactory(nef)); 419: } 420: 421: /** 422: * Returns the <code>SimpleDateFormat</code> instance that is used to 423: * format the date value. 424: * 425: * @return the <code>SimpleDateFormat</code> instance that is used to 426: * format the date value 427: */ 428: public SimpleDateFormat getFormat() 429: { 430: DateFormatter formatter = (DateFormatter) ftf.getFormatter(); 431: return (SimpleDateFormat) formatter.getFormat(); 432: } 433: 434: /** 435: * Returns the {@link SpinnerDateModel} that is edited by this editor. 436: * 437: * @return the <code>SpinnerDateModel</code> that is edited by this editor 438: */ 439: public SpinnerDateModel getModel() 440: { 441: return (SpinnerDateModel) getSpinner().getModel(); 442: } 443: } 444: 445: static class DateEditorFormatter 446: extends DateFormatter 447: { 448: public DateEditorFormatter() 449: { 450: super(DateFormat.getInstance()); 451: } 452: public DateEditorFormatter(String dateFormatPattern) 453: { 454: super(new SimpleDateFormat(dateFormatPattern)); 455: } 456: } 457: 458: /** 459: * A listener that forwards {@link ChangeEvent} notifications from the model 460: * to the {@link JSpinner}'s listeners. 461: */ 462: class ModelListener implements ChangeListener 463: { 464: /** 465: * Creates a new listener. 466: */ 467: public ModelListener() 468: { 469: // nothing to do here 470: } 471: 472: /** 473: * Receives notification from the model that its state has changed. 474: * 475: * @param event the event (ignored). 476: */ 477: public void stateChanged(ChangeEvent event) 478: { 479: fireStateChanged(); 480: } 481: } 482: 483: /** 484: * The model that defines the current value and permitted values for the 485: * spinner. 486: */ 487: private SpinnerModel model; 488: 489: /** The current editor. */ 490: private JComponent editor; 491: 492: private static final long serialVersionUID = 3412663575706551720L; 493: 494: /** 495: * Creates a new <code>JSpinner</code> with default instance of 496: * {@link SpinnerNumberModel} (that is, a model with value 0, step size 1, 497: * and no upper or lower limit). 498: * 499: * @see javax.swing.SpinnerNumberModel 500: */ 501: public JSpinner() 502: { 503: this(new SpinnerNumberModel()); 504: } 505: 506: /** 507: * Creates a new <code>JSpinner with the specified model. The 508: * {@link #createEditor(SpinnerModel)} method is used to create an editor 509: * that is suitable for the model. 510: * 511: * @param model the model (<code>null</code> not permitted). 512: * 513: * @throws NullPointerException if <code>model</code> is <code>null</code>. 514: */ 515: public JSpinner(SpinnerModel model) 516: { 517: this.model = model; 518: this.editor = createEditor(model); 519: model.addChangeListener(new ModelListener()); 520: updateUI(); 521: } 522: 523: /** 524: * If the editor is <code>JSpinner.DefaultEditor</code>, then forwards the 525: * call to it, otherwise do nothing. 526: * 527: * @throws ParseException DOCUMENT ME! 528: */ 529: public void commitEdit() throws ParseException 530: { 531: if (editor instanceof DefaultEditor) 532: ((DefaultEditor) editor).commitEdit(); 533: } 534: 535: /** 536: * Gets the current editor 537: * 538: * @return the current editor 539: * 540: * @see #setEditor 541: */ 542: public JComponent getEditor() 543: { 544: return editor; 545: } 546: 547: /** 548: * Changes the current editor to the new editor. The old editor is 549: * removed from the spinner's {@link ChangeEvent} list. 550: * 551: * @param editor the new editor (<code>null</code> not permitted. 552: * 553: * @throws IllegalArgumentException if <code>editor</code> is 554: * <code>null</code>. 555: * 556: * @see #getEditor 557: */ 558: public void setEditor(JComponent editor) 559: { 560: if (editor == null) 561: throw new IllegalArgumentException("editor may not be null"); 562: 563: JComponent oldEditor = this.editor; 564: if (oldEditor instanceof DefaultEditor) 565: ((DefaultEditor) oldEditor).dismiss(this); 566: else if (oldEditor instanceof ChangeListener) 567: removeChangeListener((ChangeListener) oldEditor); 568: 569: this.editor = editor; 570: firePropertyChange("editor", oldEditor, editor); 571: } 572: 573: /** 574: * Returns the model used by the {@link JSpinner} component. 575: * 576: * @return The model. 577: * 578: * @see #setModel(SpinnerModel) 579: */ 580: public SpinnerModel getModel() 581: { 582: return model; 583: } 584: 585: /** 586: * Sets a new underlying model. 587: * 588: * @param newModel the new model to set 589: * 590: * @exception IllegalArgumentException if newModel is <code>null</code> 591: */ 592: public void setModel(SpinnerModel newModel) 593: { 594: if (newModel == null) 595: throw new IllegalArgumentException(); 596: 597: if (model == newModel) 598: return; 599: 600: SpinnerModel oldModel = model; 601: model = newModel; 602: firePropertyChange("model", oldModel, newModel); 603: setEditor(createEditor(model)); 604: } 605: 606: /** 607: * Gets the next value without changing the current value. 608: * 609: * @return the next value 610: * 611: * @see javax.swing.SpinnerModel#getNextValue 612: */ 613: public Object getNextValue() 614: { 615: return model.getNextValue(); 616: } 617: 618: /** 619: * Gets the previous value without changing the current value. 620: * 621: * @return the previous value 622: * 623: * @see javax.swing.SpinnerModel#getPreviousValue 624: */ 625: public Object getPreviousValue() 626: { 627: return model.getPreviousValue(); 628: } 629: 630: /** 631: * Gets the <code>SpinnerUI</code> that handles this spinner 632: * 633: * @return the <code>SpinnerUI</code> 634: */ 635: public SpinnerUI getUI() 636: { 637: return (SpinnerUI) ui; 638: } 639: 640: /** 641: * Gets the current value of the spinner, according to the underly model, 642: * not the UI. 643: * 644: * @return the current value 645: * 646: * @see javax.swing.SpinnerModel#getValue 647: */ 648: public Object getValue() 649: { 650: return model.getValue(); 651: } 652: 653: /** 654: * Sets the value in the model. 655: * 656: * @param value the new value. 657: */ 658: public void setValue(Object value) 659: { 660: model.setValue(value); 661: } 662: 663: /** 664: * Returns the ID that identifies which look and feel class will be 665: * the UI delegate for this spinner. 666: * 667: * @return <code>"SpinnerUI"</code>. 668: */ 669: public String getUIClassID() 670: { 671: return "SpinnerUI"; 672: } 673: 674: /** 675: * This method resets the spinner's UI delegate to the default UI for the 676: * current look and feel. 677: */ 678: public void updateUI() 679: { 680: setUI((SpinnerUI) UIManager.getUI(this)); 681: } 682: 683: /** 684: * Sets the UI delegate for the component. 685: * 686: * @param ui The spinner's UI delegate. 687: */ 688: public void setUI(SpinnerUI ui) 689: { 690: super.setUI(ui); 691: } 692: 693: /** 694: * Adds a <code>ChangeListener</code> 695: * 696: * @param listener the listener to add 697: */ 698: public void addChangeListener(ChangeListener listener) 699: { 700: listenerList.add(ChangeListener.class, listener); 701: } 702: 703: /** 704: * Remove a particular listener 705: * 706: * @param listener the listener to remove 707: */ 708: public void removeChangeListener(ChangeListener listener) 709: { 710: listenerList.remove(ChangeListener.class, listener); 711: } 712: 713: /** 714: * Gets all the <code>ChangeListener</code>s 715: * 716: * @return all the <code>ChangeListener</code>s 717: */ 718: public ChangeListener[] getChangeListeners() 719: { 720: return (ChangeListener[]) listenerList.getListeners(ChangeListener.class); 721: } 722: 723: /** 724: * Fires a <code>ChangeEvent</code> to all the <code>ChangeListener</code>s 725: * added to this <code>JSpinner</code> 726: */ 727: protected void fireStateChanged() 728: { 729: ChangeEvent evt = new ChangeEvent(this); 730: ChangeListener[] listeners = getChangeListeners(); 731: 732: for (int i = 0; i < listeners.length; ++i) 733: listeners[i].stateChanged(evt); 734: } 735: 736: /** 737: * Creates an editor that is appropriate for the specified <code>model</code>. 738: * 739: * @param model the model. 740: * 741: * @return The editor. 742: */ 743: protected JComponent createEditor(SpinnerModel model) 744: { 745: if (model instanceof SpinnerDateModel) 746: return new DateEditor(this); 747: else if (model instanceof SpinnerNumberModel) 748: return new NumberEditor(this); 749: else if (model instanceof SpinnerListModel) 750: return new ListEditor(this); 751: else 752: return new DefaultEditor(this); 753: } 754: }