1. /*
  2. * @(#)JSpinner.java 1.29 03/01/23
  3. *
  4. * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package javax.swing;
  8. import java.awt.*;
  9. import java.awt.event.*;
  10. import javax.swing.*;
  11. import javax.swing.event.*;
  12. import javax.swing.text.*;
  13. import javax.swing.plaf.SpinnerUI;
  14. import java.util.*;
  15. import java.beans.*;
  16. import java.text.*;
  17. import java.io.*;
  18. import java.util.HashMap;
  19. /**
  20. * A single line input field that lets the user select a
  21. * number or an object value from an ordered sequence. Spinners typically
  22. * provide a pair of tiny arrow buttons for stepping through the elements
  23. * of the sequence. The keyboard up/down arrow keys also cycle through the
  24. * elements. The user may also be allowed to type a (legal) value directly
  25. * into the spinner. Although combo boxes provide similar functionality,
  26. * spinners are sometimes preferred because they don't require a drop down list
  27. * that can obscure important data.
  28. * <p>
  29. * A <code>JSpinner</code>'s sequence value is defined by its
  30. * <code>SpinnerModel</code>.
  31. * The <code>model</code> can be specified as a constructor argument and
  32. * changed with the <code>model</code> property. <code>SpinnerModel</code>
  33. * classes for some common types are provided: <code>SpinnerListModel</code>,
  34. * <code>SpinnerNumberModel</code>, and <code>SpinnerDateModel</code>.
  35. * <p>
  36. * A <code>JSpinner</code> has a single child component that's
  37. * responsible for displaying
  38. * and potentially changing the current element or <i>value</i> of
  39. * the model, which is called the <code>editor</code>. The editor is created
  40. * by the <code>JSpinner</code>'s constructor and can be changed with the
  41. * <code>editor</code> property. The <code>JSpinner</code>'s editor stays
  42. * in sync with the model by listening for <code>ChangeEvent</code>s.
  43. * <p>
  44. * <strong>Warning:</strong>
  45. * Serialized objects of this class will not be compatible with
  46. * future Swing releases. The current serialization support is
  47. * appropriate for short term storage or RMI between applications running
  48. * the same version of Swing. As of 1.4, support for long term storage
  49. * of all JavaBeans<sup><font size="-2">TM</font></sup>
  50. * has been added to the <code>java.beans</code> package.
  51. * Please see {@link java.beans.XMLEncoder}.
  52. *
  53. * @beaninfo
  54. * attribute: isContainer false
  55. * description: A single line input field that lets the user select a
  56. * number or an object value from an ordered set.
  57. *
  58. * @see SpinnerModel
  59. * @see AbstractSpinnerModel
  60. * @see SpinnerListModel
  61. * @see SpinnerNumberModel
  62. * @see SpinnerDateModel
  63. * @see JFormattedTextField
  64. *
  65. * @version 1.29 01/23/03
  66. * @author Hans Muller
  67. * @since 1.4
  68. */
  69. public class JSpinner extends JComponent
  70. {
  71. /**
  72. * @see #getUIClassID
  73. * @see #readObject
  74. */
  75. private static final String uiClassID = "SpinnerUI";
  76. private static final Action DISABLED_ACTION = new DisabledAction();
  77. private transient SpinnerModel model;
  78. private JComponent editor;
  79. private ChangeListener modelListener;
  80. private transient ChangeEvent changeEvent;
  81. private boolean editorExplicitlySet = false;
  82. /**
  83. * Constructs a complete spinner with pair of next/previous buttons
  84. * and an editor for the <code>SpinnerModel</code>.
  85. */
  86. public JSpinner(SpinnerModel model) {
  87. this.model = model;
  88. this.editor = createEditor(model);
  89. setOpaque(true);
  90. updateUI();
  91. }
  92. /**
  93. * Constructs a spinner with an <code>Integer SpinnerNumberModel</code>
  94. * with initial value 0 and no minimum or maximum limits.
  95. */
  96. public JSpinner() {
  97. this(new SpinnerNumberModel());
  98. }
  99. /**
  100. * Returns the look and feel (L&F) object that renders this component.
  101. *
  102. * @return the <code>SpinnerUI</code> object that renders this component
  103. */
  104. public SpinnerUI getUI() {
  105. return (SpinnerUI)ui;
  106. }
  107. /**
  108. * Sets the look and feel (L&F) object that renders this component.
  109. *
  110. * @param ui the <code>SpinnerUI</code> L&F object
  111. * @see UIDefaults#getUI
  112. */
  113. public void setUI(SpinnerUI ui) {
  114. super.setUI(ui);
  115. }
  116. /**
  117. * Returns the suffix used to construct the name of the look and feel
  118. * (L&F) class used to render this component.
  119. *
  120. * @return the string "SpinnerUI"
  121. * @see JComponent#getUIClassID
  122. * @see UIDefaults#getUI
  123. */
  124. public String getUIClassID() {
  125. return uiClassID;
  126. }
  127. /**
  128. * Resets the UI property with the value from the current look and feel.
  129. *
  130. * @see UIManager#getUI
  131. */
  132. public void updateUI() {
  133. setUI((SpinnerUI)UIManager.getUI(this));
  134. invalidate();
  135. }
  136. /**
  137. * This method is called by the constructors to create the
  138. * <code>JComponent</code>
  139. * that displays the current value of the sequence. The editor may
  140. * also allow the user to enter an element of the sequence directly.
  141. * An editor must listen for <code>ChangeEvents</code> on the
  142. * <code>model</code> and keep the value it displays
  143. * in sync with the value of the model.
  144. * <p>
  145. * Subclasses may override this method to add support for new
  146. * <code>SpinnerModel</code> classes. Alternatively one can just
  147. * replace the editor created here with the <code>setEditor</code>
  148. * method. The default mapping from model type to editor is:
  149. * <ul>
  150. * <li> <code>SpinnerNumberModel => JSpinner.NumberEditor</code>
  151. * <li> <code>SpinnerDateModel => JSpinner.DateEditor</code>
  152. * <li> <code>SpinnerListModel => JSpinner.ListEditor</code>
  153. * <li> <i>all others</i> => <code>JSpinner.DefaultEditor</code>
  154. * </ul>
  155. *
  156. * @return a component that displays the current value of the sequence
  157. * @param model the value of getModel
  158. * @see #getModel
  159. * @see #setEditor
  160. */
  161. protected JComponent createEditor(SpinnerModel model) {
  162. if (model instanceof SpinnerDateModel) {
  163. return new DateEditor(this);
  164. }
  165. else if (model instanceof SpinnerListModel) {
  166. return new ListEditor(this);
  167. }
  168. else if (model instanceof SpinnerNumberModel) {
  169. return new NumberEditor(this);
  170. }
  171. else {
  172. return new DefaultEditor(this);
  173. }
  174. }
  175. /**
  176. * Changes the model that represents the value of this spinner.
  177. * If the editor property has not been explicitly set,
  178. * the editor property is (implicitly) set after the <code>"model"</code>
  179. * <code>PropertyChangeEvent</code> has been fired. The editor
  180. * property is set to the value returned by <code>createEditor</code>,
  181. * as in:
  182. * <pre>
  183. * setEditor(createEditor(model));
  184. * </pre>
  185. *
  186. * @param model the new <code>SpinnerModel</code>
  187. * @see #getModel
  188. * @see #getEditor
  189. * @see #setEditor
  190. * @throws IllegalArgumentException if model is <code>null</code>
  191. *
  192. * @beaninfo
  193. * bound: true
  194. * attribute: visualUpdate true
  195. * description: Model that represents the value of this spinner.
  196. */
  197. public void setModel(SpinnerModel model) {
  198. if (model == null) {
  199. throw new IllegalArgumentException("null model");
  200. }
  201. if (!model.equals(this.model)) {
  202. SpinnerModel oldModel = this.model;
  203. this.model = model;
  204. if (modelListener != null) {
  205. this.model.addChangeListener(modelListener);
  206. }
  207. firePropertyChange("model", oldModel, model);
  208. if (!editorExplicitlySet) {
  209. setEditor(createEditor(model)); // sets editorExplicitlySet true
  210. editorExplicitlySet = false;
  211. }
  212. repaint();
  213. revalidate();
  214. }
  215. }
  216. /**
  217. * Returns the <code>SpinnerModel</code> that defines
  218. * this spinners sequence of values.
  219. *
  220. * @return the value of the model property
  221. * @see #setModel
  222. */
  223. public SpinnerModel getModel() {
  224. return model;
  225. }
  226. /**
  227. * Returns the current value of the model, typically
  228. * this value is displayed by the <code>editor</code>.
  229. * <p>
  230. * This method simply delegates to the <code>model</code>.
  231. * It is equivalent to:
  232. * <pre>
  233. * getModel().getValue()
  234. * </pre>
  235. *
  236. * @see #setValue
  237. * @see SpinnerModel#getValue
  238. */
  239. public Object getValue() {
  240. return getModel().getValue();
  241. }
  242. /**
  243. * Changes current value of the model, typically
  244. * this value is displayed by the <code>editor</code>.
  245. * If the <code>SpinnerModel</code> implementation
  246. * doesn't support the specified value then an
  247. * <code>IllegalArgumentException</code> is thrown.
  248. * <p>
  249. * This method simply delegates to the <code>model</code>.
  250. * It is equivalent to:
  251. * <pre>
  252. * getModel().setValue(value)
  253. * </pre>
  254. *
  255. * @throws IllegalArgumentException if <code>value</code> isn't allowed
  256. * @see #getValue
  257. * @see SpinnerModel#setValue
  258. */
  259. public void setValue(Object value) {
  260. getModel().setValue(value);
  261. }
  262. /**
  263. * Returns the object in the sequence that comes after the object returned
  264. * by <code>getValue()</code>. If the end of the sequence has been reached
  265. * then return <code>null</code>.
  266. * Calling this method does not effect <code>value</code>.
  267. * <p>
  268. * This method simply delegates to the <code>model</code>.
  269. * It is equivalent to:
  270. * <pre>
  271. * getModel().getNextValue()
  272. * </pre>
  273. *
  274. * @return the next legal value or <code>null</code> if one doesn't exist
  275. * @see #getValue
  276. * @see #getPreviousValue
  277. * @see SpinnerModel#getNextValue
  278. */
  279. public Object getNextValue() {
  280. return getModel().getNextValue();
  281. }
  282. /**
  283. * We pass <code>Change</code> events along to the listeners with the
  284. * the slider (instead of the model itself) as the event source.
  285. */
  286. private class ModelListener implements ChangeListener, Serializable {
  287. public void stateChanged(ChangeEvent e) {
  288. fireStateChanged();
  289. }
  290. }
  291. /**
  292. * Adds a listener to the list that is notified each time a change
  293. * to the model occurs. The source of <code>ChangeEvents</code>
  294. * delivered to <code>ChangeListeners</code> will be this
  295. * <code>JSpinner</code>. Note also that replacing the model
  296. * will not affect listeners added directly to JSpinner.
  297. * Applications can add listeners to the model directly. In that
  298. * case is that the source of the event would be the
  299. * <code>SpinnerModel</code>.
  300. *
  301. * @param listener the <code>ChangeListener</code> to add
  302. * @see #removeChangeListener
  303. * @see #getModel
  304. */
  305. public void addChangeListener(ChangeListener listener) {
  306. if (modelListener == null) {
  307. modelListener = new ModelListener();
  308. getModel().addChangeListener(modelListener);
  309. }
  310. listenerList.add(ChangeListener.class, listener);
  311. }
  312. /**
  313. * Removes a <code>ChangeListener</code> from this spinner.
  314. *
  315. * @param listener the <code>ChangeListener</code> to remove
  316. * @see #fireStateChanged
  317. * @see #addChangeListener
  318. */
  319. public void removeChangeListener(ChangeListener listener) {
  320. listenerList.remove(ChangeListener.class, listener);
  321. }
  322. /**
  323. * Returns an array of all the <code>ChangeListener</code>s added
  324. * to this JSpinner with addChangeListener().
  325. *
  326. * @return all of the <code>ChangeListener</code>s added or an empty
  327. * array if no listeners have been added
  328. * @since 1.4
  329. */
  330. public ChangeListener[] getChangeListeners() {
  331. return (ChangeListener[])listenerList.getListeners(
  332. ChangeListener.class);
  333. }
  334. /**
  335. * Sends a <code>ChangeEvent</code>, whose source is this
  336. * <code>JSpinner</code>, to each <code>ChangeListener</code>.
  337. * When a <code>ChangeListener</code> has been added
  338. * to the spinner, this method method is called each time
  339. * a <code>ChangeEvent</code> is received from the model.
  340. *
  341. * @see #addChangeListener
  342. * @see #removeChangeListener
  343. * @see EventListenerList
  344. */
  345. protected void fireStateChanged() {
  346. Object[] listeners = listenerList.getListenerList();
  347. for (int i = listeners.length - 2; i >= 0; i -= 2) {
  348. if (listeners[i] == ChangeListener.class) {
  349. if (changeEvent == null) {
  350. changeEvent = new ChangeEvent(this);
  351. }
  352. ((ChangeListener)listeners[i+1]).stateChanged(changeEvent);
  353. }
  354. }
  355. }
  356. /**
  357. * Returns the object in the sequence that comes
  358. * before the object returned by <code>getValue()</code>.
  359. * If the end of the sequence has been reached then
  360. * return <code>null</code>. Calling this method does
  361. * not effect <code>value</code>.
  362. * <p>
  363. * This method simply delegates to the <code>model</code>.
  364. * It is equivalent to:
  365. * <pre>
  366. * getModel().getPreviousValue()
  367. * </pre>
  368. *
  369. * @return the previous legal value or <code>null</code>
  370. * if one doesn't exist
  371. * @see #getValue
  372. * @see #getNextValue
  373. * @see SpinnerModel#getPreviousValue
  374. */
  375. public Object getPreviousValue() {
  376. return getModel().getPreviousValue();
  377. }
  378. /**
  379. * Changes the <code>JComponent</code> that displays the current value
  380. * of the <code>SpinnerModel</code>. It is the responsibility of this
  381. * method to <i>disconnect</i> the old editor from the model and to
  382. * connect the new editor. This may mean removing the
  383. * old editors <code>ChangeListener</code> from the model or the
  384. * spinner itself and adding one for the new editor.
  385. *
  386. * @param editor the new editor
  387. * @see #getEditor
  388. * @see #createEditor
  389. * @see #getModel
  390. * @throws IllegalArgumentException if editor is <code>null</code>
  391. *
  392. * @beaninfo
  393. * bound: true
  394. * attribute: visualUpdate true
  395. * description: JComponent that displays the current value of the model
  396. */
  397. public void setEditor(JComponent editor) {
  398. if (editor == null) {
  399. throw new IllegalArgumentException("null editor");
  400. }
  401. if (!editor.equals(this.editor)) {
  402. JComponent oldEditor = this.editor;
  403. this.editor = editor;
  404. if (oldEditor instanceof DefaultEditor) {
  405. ((DefaultEditor)oldEditor).dismiss(this);
  406. }
  407. editorExplicitlySet = true;
  408. firePropertyChange("editor", oldEditor, editor);
  409. revalidate();
  410. repaint();
  411. }
  412. }
  413. /**
  414. * Returns the component that displays and potentially
  415. * changes the model's value.
  416. *
  417. * @return the component that displays and potentially
  418. * changes the model's value
  419. * @see #setEditor
  420. * @see #createEditor
  421. */
  422. public JComponent getEditor() {
  423. return editor;
  424. }
  425. /**
  426. * Commits the currently edited value to the <code>SpinnerModel</code>.
  427. * <p>
  428. * If the editor is an instance of <code>DefaultEditor</code>, the
  429. * call if forwarded to the editor, otherwise this does nothing.
  430. *
  431. * @throws ParseException if the currently edited value couldn't
  432. * be commited.
  433. */
  434. public void commitEdit() throws ParseException {
  435. JComponent editor = getEditor();
  436. if (editor instanceof DefaultEditor) {
  437. ((DefaultEditor)editor).commitEdit();
  438. }
  439. }
  440. /*
  441. * See readObject and writeObject in JComponent for more
  442. * information about serialization in Swing.
  443. *
  444. * @param s Stream to write to
  445. */
  446. private void writeObject(ObjectOutputStream s) throws IOException {
  447. s.defaultWriteObject();
  448. HashMap additionalValues = new HashMap(1);
  449. SpinnerModel model = getModel();
  450. if (model instanceof Serializable) {
  451. additionalValues.put("model", model);
  452. }
  453. s.writeObject(additionalValues);
  454. if (getUIClassID().equals(uiClassID)) {
  455. byte count = JComponent.getWriteObjCounter(this);
  456. JComponent.setWriteObjCounter(this, --count);
  457. if (count == 0 && ui != null) {
  458. ui.installUI(this);
  459. }
  460. }
  461. }
  462. private void readObject(ObjectInputStream s)
  463. throws IOException, ClassNotFoundException {
  464. s.defaultReadObject();
  465. Map additionalValues = (Map)s.readObject();
  466. model = (SpinnerModel)additionalValues.get("model");
  467. }
  468. /**
  469. * A simple base class for more specialized editors
  470. * that displays a read-only view of the model's current
  471. * value with a <code>JFormattedTextField<code>. Subclasses
  472. * can configure the <code>JFormattedTextField<code> to create
  473. * an editor that's appropriate for the type of model they
  474. * support and they may want to override
  475. * the <code>stateChanged</code> and <code>propertyChanged</code>
  476. * methods, which keep the model and the text field in sync.
  477. * <p>
  478. * This class defines a <code>dismiss</code> method that removes the
  479. * editors <code>ChangeListener</code> from the <code>JSpinner</code>
  480. * that it's part of. The <code>setEditor</code> method knows about
  481. * <code>DefaultEditor.dismiss</code>, so if the developer
  482. * replaces an editor that's derived from <code>JSpinner.DefaultEditor</code>
  483. * its <code>ChangeListener</code> connection back to the
  484. * <code>JSpinner</code> will be removed. However after that,
  485. * it's up to the developer to manage their editor listeners.
  486. * Similarly, if a subclass overrides <code>createEditor</code>,
  487. * it's up to the subclasser to deal with their editor
  488. * subsequently being replaced (with <code>setEditor</code>).
  489. * We expect that in most cases, and in editor installed
  490. * with <code>setEditor</code> or created by a <code>createEditor</code>
  491. * override, will not be replaced anyway.
  492. * <p>
  493. * This class is the <code>LayoutManager<code> for it's single
  494. * <code>JFormattedTextField</code> child. By default the
  495. * child is just centered with the parents insets.
  496. */
  497. public static class DefaultEditor extends JPanel
  498. implements ChangeListener, PropertyChangeListener, LayoutManager
  499. {
  500. /**
  501. * Constructs an editor component for the specified <code>JSpinner</code>.
  502. * This <code>DefaultEditor</code> is it's own layout manager and
  503. * it is added to the spinner's <code>ChangeListener</code> list.
  504. * The constructor creates a single <code>JFormattedTextField<code> child,
  505. * initializes it's value to be the spinner model's current value
  506. * and adds it to <code>this</code> <code>DefaultEditor</code>.
  507. *
  508. * @param spinner the spinner whose model <code>this</code> editor will monitor
  509. * @see #getTextField
  510. * @see JSpinner#addChangeListener
  511. */
  512. public DefaultEditor(JSpinner spinner) {
  513. super(null);
  514. JFormattedTextField ftf = new JFormattedTextField();
  515. ftf.setValue(spinner.getValue());
  516. ftf.addPropertyChangeListener(this);
  517. ftf.setEditable(false);
  518. add(ftf);
  519. setLayout(this);
  520. spinner.addChangeListener(this);
  521. // We want the spinner's increment/decrement actions to be
  522. // active vs those of the JFormattedTextField. As such we
  523. // put disabled actions in the JFormattedTextField's actionmap.
  524. // A binding to a disabled action is treated as a nonexistant
  525. // binding.
  526. ActionMap ftfMap = ftf.getActionMap();
  527. if (ftfMap != null) {
  528. ftfMap.put("increment", DISABLED_ACTION);
  529. ftfMap.put("decrement", DISABLED_ACTION);
  530. }
  531. }
  532. /**
  533. * Disconnect <code>this</code> editor from the specified
  534. * <code>JSpinner</code>. By default, this method removes
  535. * itself from the spinners <code>ChangeListener</code> list.
  536. *
  537. * @param spinner the <code>JSpinner</code> to disconnect this
  538. * editor from; the same spinner as was passed to the constructor.
  539. */
  540. public void dismiss(JSpinner spinner) {
  541. spinner.removeChangeListener(this);
  542. }
  543. /**
  544. * Returns the <code>JSpinner</code> ancestor of this editor or null.
  545. * Typically the editor's parent is a <code>JSpinner</code> however
  546. * subclasses of <codeJSpinner</code> may override the
  547. * the <code>createEditor</code> method and insert one or more containers
  548. * between the <code>JSpinner</code> and it's editor.
  549. *
  550. * @return <code>JSpinner</code> ancestor
  551. * @see JSpinner#createEditor
  552. */
  553. public JSpinner getSpinner() {
  554. for (Component c = this; c != null; c = c.getParent()) {
  555. if (c instanceof JSpinner) {
  556. return (JSpinner)c;
  557. }
  558. }
  559. return null;
  560. }
  561. /**
  562. * Returns the <code>JFormattedTextField</code> child of this
  563. * editor. By default the text field is the first and only
  564. * child of editor.
  565. *
  566. * @return the <code>JFormattedTextField</code> that gives the user
  567. * access to the <code>SpinnerDateModel's</code> value.
  568. * @see #getSpinner
  569. * @see #getModel
  570. */
  571. public JFormattedTextField getTextField() {
  572. return (JFormattedTextField)getComponent(0);
  573. }
  574. /**
  575. * This method is called when the spinner's model's state changes.
  576. * It sets the <code>value</code> of the text field to the current
  577. * value of the spinners model.
  578. *
  579. * @param e not used
  580. * @see #getTextField
  581. * @see JSpinner#getValue
  582. */
  583. public void stateChanged(ChangeEvent e) {
  584. JSpinner spinner = (JSpinner)(e.getSource());
  585. getTextField().setValue(spinner.getValue());
  586. }
  587. /**
  588. * Called by the <code>JFormattedTextField</code>
  589. * <code>PropertyChangeListener</code>. When the <code>"value"</code>
  590. * property changes, which implies that the user has typed a new
  591. * number, we set the value of the spinners model.
  592. * <p>
  593. * This class ignores <code>PropertyChangeEvents</code> whose
  594. * source is not the <code>JFormattedTextField</code>, so subclasses
  595. * may safely make <code>this</code> <code>DefaultEditor</code> a
  596. * <code>PropertyChangeListener</code> on other objects.
  597. *
  598. * @param e the <code>PropertyChangeEvent</code> whose source is
  599. * the <code>JFormattedTextField</code> created by this class.
  600. * @see #getTextField
  601. */
  602. public void propertyChange(PropertyChangeEvent e)
  603. {
  604. JSpinner spinner = getSpinner();
  605. if (spinner == null) {
  606. // Indicates we aren't installed anywhere.
  607. return;
  608. }
  609. Object source = e.getSource();
  610. String name = e.getPropertyName();
  611. if ((source instanceof JFormattedTextField) && "value".equals(name)) {
  612. Object lastValue = spinner.getValue();
  613. // Try to set the new value
  614. try {
  615. spinner.setValue(getTextField().getValue());
  616. } catch (IllegalArgumentException iae) {
  617. // SpinnerModel didn't like new value, reset
  618. try {
  619. ((JFormattedTextField)source).setValue(lastValue);
  620. } catch (IllegalArgumentException iae2) {
  621. // Still bogus, nothing else we can do, the
  622. // SpinnerModel and JFormattedTextField are now out
  623. // of sync.
  624. }
  625. }
  626. }
  627. }
  628. /**
  629. * This <code>LayoutManager</code> method does nothing. We're
  630. * only managing a single child and there's no support
  631. * for layout constraints.
  632. *
  633. * @param name ignored
  634. * @param child ignored
  635. */
  636. public void addLayoutComponent(String name, Component child) {
  637. }
  638. /**
  639. * This <code>LayoutManager</code> method does nothing. There
  640. * isn't any per-child state.
  641. *
  642. * @param child ignored
  643. */
  644. public void removeLayoutComponent(Component child) {
  645. }
  646. /**
  647. * Returns the size of the parents insets.
  648. */
  649. private Dimension insetSize(Container parent) {
  650. Insets insets = parent.getInsets();
  651. int w = insets.left + insets.right;
  652. int h = insets.top + insets.bottom;
  653. return new Dimension(w, h);
  654. }
  655. /**
  656. * Returns the preferred size of first (and only) child plus the
  657. * size of the parents insets.
  658. *
  659. * @param parent the Container that's managing the layout
  660. * @return the preferred dimensions to lay out the subcomponents
  661. * of the specified container.
  662. */
  663. public Dimension preferredLayoutSize(Container parent) {
  664. Dimension preferredSize = insetSize(parent);
  665. if (parent.getComponentCount() > 0) {
  666. Dimension childSize = getComponent(0).getPreferredSize();
  667. preferredSize.width += childSize.width;
  668. preferredSize.height += childSize.height;
  669. }
  670. return preferredSize;
  671. }
  672. /**
  673. * Returns the minimum size of first (and only) child plus the
  674. * size of the parents insets.
  675. *
  676. * @param parent the Container that's managing the layout
  677. * @return the minimum dimensions needed to lay out the subcomponents
  678. * of the specified container.
  679. */
  680. public Dimension minimumLayoutSize(Container parent) {
  681. Dimension minimumSize = insetSize(parent);
  682. if (parent.getComponentCount() > 0) {
  683. Dimension childSize = getComponent(0).getMinimumSize();
  684. minimumSize.width += childSize.width;
  685. minimumSize.height += childSize.height;
  686. }
  687. return minimumSize;
  688. }
  689. /**
  690. * Resize the one (and only) child to completely fill the area
  691. * within the parents insets.
  692. */
  693. public void layoutContainer(Container parent) {
  694. if (parent.getComponentCount() > 0) {
  695. Insets insets = parent.getInsets();
  696. int w = parent.getWidth() - (insets.left + insets.right);
  697. int h = parent.getHeight() - (insets.top + insets.bottom);
  698. getComponent(0).setBounds(insets.left, insets.top, w, h);
  699. }
  700. }
  701. /**
  702. * Pushes the currently edited value to the <code>SpinnerModel</code>.
  703. * <p>
  704. * The default implementation invokes <code>commitEdit</code> on the
  705. * <code>JFormattedTextField</code>.
  706. *
  707. * @throws ParseException if the edited value is not legal
  708. */
  709. public void commitEdit() throws ParseException {
  710. // If the value in the JFormattedTextField is legal, this will have
  711. // the result of pushing the value to the SpinnerModel
  712. // by way of the <code>propertyChange</code> method.
  713. JFormattedTextField ftf = getTextField();
  714. ftf.commitEdit();
  715. }
  716. }
  717. /**
  718. * This subclass of javax.swing.DateFormatter maps the minimum/maximum
  719. * properties to te start/end properties of a SpinnerDateModel.
  720. */
  721. private static class DateEditorFormatter extends DateFormatter {
  722. private final SpinnerDateModel model;
  723. DateEditorFormatter(SpinnerDateModel model, DateFormat format) {
  724. super(format);
  725. this.model = model;
  726. }
  727. public void setMinimum(Comparable min) {
  728. model.setStart(min);
  729. }
  730. public Comparable getMinimum() {
  731. return model.getStart();
  732. }
  733. public void setMaximum(Comparable max) {
  734. model.setEnd(max);
  735. }
  736. public Comparable getMaximum() {
  737. return model.getEnd();
  738. }
  739. }
  740. /**
  741. * An editor for a <code>JSpinner</code> whose model is a
  742. * <code>SpinnerDateModel</code>. The value of the editor is
  743. * displayed with a <code>JFormattedTextField</code> whose format
  744. * is defined by a <code>DateFormatter</code> instance whose
  745. * <code>minimum</code> and <code>maximum</code> properties
  746. * are mapped to the <code>SpinnerDateModel</code>.
  747. */
  748. // PENDING(hmuller): more example javadoc
  749. public static class DateEditor extends DefaultEditor
  750. {
  751. /**
  752. * Construct a <code>JSpinner</code> editor that supports displaying
  753. * and editing the value of a <code>SpinnerDateModel</code>
  754. * with a <code>JFormattedTextField</code>. <code>This</code>
  755. * <code>DateEditor</code> becomes both a <code>ChangeListener</code>
  756. * on the spinners model and a <code>PropertyChangeListener</code>
  757. * on the new <code>JFormattedTextField</code>.
  758. *
  759. * @param spinner the spinner whose model <code>this</code> editor will monitor
  760. * @exception IllegalArgumentException if the spinners model is not
  761. * an instance of <code>SpinnerDateModel</code>
  762. *
  763. * @see #getModel
  764. * @see #getFormat
  765. * @see SpinnerDateModel
  766. */
  767. public DateEditor(JSpinner spinner) {
  768. this(spinner, new SimpleDateFormat());
  769. }
  770. /**
  771. * Construct a <code>JSpinner</code> editor that supports displaying
  772. * and editing the value of a <code>SpinnerDateModel</code>
  773. * with a <code>JFormattedTextField</code>. <code>This</code>
  774. * <code>DateEditor</code> becomes both a <code>ChangeListener</code>
  775. * on the spinner and a <code>PropertyChangeListener</code>
  776. * on the new <code>JFormattedTextField</code>.
  777. *
  778. * @param spinner the spinner whose model <code>this</code> editor will monitor
  779. * @param dateFormatPattern the initial pattern for the
  780. * <code>SimpleDateFormat</code> object that's used to display
  781. * and parse the value of the text field.
  782. * @exception IllegalArgumentException if the spinners model is not
  783. * an instance of <code>SpinnerDateModel</code>
  784. *
  785. * @see #getModel
  786. * @see #getFormat
  787. * @see SpinnerDateModel
  788. * @see java.text.SimpleDateFormat
  789. */
  790. public DateEditor(JSpinner spinner, String dateFormatPattern) {
  791. this(spinner, new SimpleDateFormat(dateFormatPattern));
  792. }
  793. /**
  794. * Construct a <code>JSpinner</code> editor that supports displaying
  795. * and editing the value of a <code>SpinnerDateModel</code>
  796. * with a <code>JFormattedTextField</code>. <code>This</code>
  797. * <code>DateEditor</code> becomes both a <code>ChangeListener</code>
  798. * on the spinner and a <code>PropertyChangeListener</code>
  799. * on the new <code>JFormattedTextField</code>.
  800. *
  801. * @param spinner the spinner whose model <code>this</code> editor
  802. * will monitor
  803. * @param format <code>DateFormat</code> object that's used to display
  804. * and parse the value of the text field.
  805. * @exception IllegalArgumentException if the spinners model is not
  806. * an instance of <code>SpinnerDateModel</code>
  807. *
  808. * @see #getModel
  809. * @see #getFormat
  810. * @see SpinnerDateModel
  811. * @see java.text.SimpleDateFormat
  812. */
  813. private DateEditor(JSpinner spinner, DateFormat format) {
  814. super(spinner);
  815. if (!(spinner.getModel() instanceof SpinnerDateModel)) {
  816. throw new IllegalArgumentException(
  817. "model not a SpinnerDateModel");
  818. }
  819. SpinnerDateModel model = (SpinnerDateModel)spinner.getModel();
  820. DateFormatter formatter = new DateEditorFormatter(model, format);
  821. DefaultFormatterFactory factory = new DefaultFormatterFactory(
  822. formatter);
  823. JFormattedTextField ftf = getTextField();
  824. ftf.setEditable(true);
  825. ftf.setFormatterFactory(factory);
  826. /* TBD - initializing the column width of the text field
  827. * is imprecise and doing it here is tricky because
  828. * the developer may configure the formatter later.
  829. */
  830. try {
  831. String maxString = formatter.valueToString(model.getStart());
  832. String minString = formatter.valueToString(model.getEnd());
  833. ftf.setColumns(Math.max(maxString.length(),
  834. minString.length()));
  835. }
  836. catch (ParseException e) {
  837. // PENDING: hmuller
  838. }
  839. }
  840. /**
  841. * Returns the <code>java.text.SimpleDateFormat</code> object the
  842. * <code>JFormattedTextField</code> uses to parse and format
  843. * numbers.
  844. *
  845. * @return the value of <code>getTextField().getFormatter().getFormat()</code>.
  846. * @see #getTextField
  847. * @see java.text.SimpleDateFormat
  848. */
  849. public SimpleDateFormat getFormat() {
  850. return (SimpleDateFormat)((DateFormatter)(getTextField().getFormatter())).getFormat();
  851. }
  852. /**
  853. * Return our spinner ancestor's <code>SpinnerDateModel</code>.
  854. *
  855. * @return <code>getSpinner().getModel()</code>
  856. * @see #getSpinner
  857. * @see #getTextField
  858. */
  859. public SpinnerDateModel getModel() {
  860. return (SpinnerDateModel)(getSpinner().getModel());
  861. }
  862. }
  863. /**
  864. * This subclass of javax.swing.NumberFormatter maps the minimum/maximum
  865. * properties to a SpinnerNumberModel and initializes the valueClass
  866. * of the NumberFormatter to match the type of the initial models value.
  867. */
  868. private static class NumberEditorFormatter extends NumberFormatter {
  869. private final SpinnerNumberModel model;
  870. NumberEditorFormatter(SpinnerNumberModel model, NumberFormat format) {
  871. super(format);
  872. this.model = model;
  873. setValueClass(model.getValue().getClass());
  874. }
  875. public void setMinimum(Comparable min) {
  876. model.setMinimum(min);
  877. }
  878. public Comparable getMinimum() {
  879. return model.getMinimum();
  880. }
  881. public void setMaximum(Comparable max) {
  882. model.setMaximum(max);
  883. }
  884. public Comparable getMaximum() {
  885. return model.getMaximum();
  886. }
  887. }
  888. /**
  889. * An editor for a <code>JSpinner</code> whose model is a
  890. * <code>SpinnerNumberModel</code>. The value of the editor is
  891. * displayed with a <code>JFormattedTextField</code> whose format
  892. * is defined by a <code>NumberFormatter</code> instance whose
  893. * <code>minimum</code> and <code>maximum</code> properties
  894. * are mapped to the <code>SpinnerNumberModel</code>.
  895. */
  896. // PENDING(hmuller): more example javadoc
  897. public static class NumberEditor extends DefaultEditor
  898. {
  899. /**
  900. * Construct a <code>JSpinner</code> editor that supports displaying
  901. * and editing the value of a <code>SpinnerNumberModel</code>
  902. * with a <code>JFormattedTextField</code>. <code>This</code>
  903. * <code>NumberEditor</code> becomes both a <code>ChangeListener</code>
  904. * on the spinner and a <code>PropertyChangeListener</code>
  905. * on the new <code>JFormattedTextField</code>.
  906. *
  907. * @param spinner the spinner whose model <code>this</code> editor will monitor
  908. * @exception IllegalArgumentException if the spinners model is not
  909. * an instance of <code>SpinnerNumberModel</code>
  910. *
  911. * @see #getModel
  912. * @see #getFormat
  913. * @see SpinnerNumberModel
  914. */
  915. public NumberEditor(JSpinner spinner) {
  916. this(spinner, new DecimalFormat());
  917. }
  918. /**
  919. * Construct a <code>JSpinner</code> editor that supports displaying
  920. * and editing the value of a <code>SpinnerNumberModel</code>
  921. * with a <code>JFormattedTextField</code>. <code>This</code>
  922. * <code>NumberEditor</code> becomes both a <code>ChangeListener</code>
  923. * on the spinner and a <code>PropertyChangeListener</code>
  924. * on the new <code>JFormattedTextField</code>.
  925. *
  926. * @param spinner the spinner whose model <code>this</code> editor will monitor
  927. * @param decimalFormatPattern the initial pattern for the
  928. * <code>DecimalFormat</code> object that's used to display
  929. * and parse the value of the text field.
  930. * @exception IllegalArgumentException if the spinners model is not
  931. * an instance of <code>SpinnerNumberModel</code> or if
  932. * <code>decimalFormatPattern</code> is not a legal
  933. * argument to <code>DecimalFormat</code>
  934. *
  935. * @see #getTextField
  936. * @see SpinnerNumberModel
  937. * @see java.text.DecimalFormat
  938. */
  939. public NumberEditor(JSpinner spinner, String decimalFormatPattern) {
  940. this(spinner, new DecimalFormat(decimalFormatPattern));
  941. }
  942. /**
  943. * Construct a <code>JSpinner</code> editor that supports displaying
  944. * and editing the value of a <code>SpinnerNumberModel</code>
  945. * with a <code>JFormattedTextField</code>. <code>This</code>
  946. * <code>NumberEditor</code> becomes both a <code>ChangeListener</code>
  947. * on the spinner and a <code>PropertyChangeListener</code>
  948. * on the new <code>JFormattedTextField</code>.
  949. *
  950. * @param spinner the spinner whose model <code>this</code> editor will monitor
  951. * @param decimalFormatPattern the initial pattern for the
  952. * <code>DecimalFormat</code> object that's used to display
  953. * and parse the value of the text field.
  954. * @exception IllegalArgumentException if the spinners model is not
  955. * an instance of <code>SpinnerNumberModel</code>
  956. *
  957. * @see #getTextField
  958. * @see SpinnerNumberModel
  959. * @see java.text.DecimalFormat
  960. */
  961. private NumberEditor(JSpinner spinner, DecimalFormat format) {
  962. super(spinner);
  963. if (!(spinner.getModel() instanceof SpinnerNumberModel)) {
  964. throw new IllegalArgumentException(
  965. "model not a SpinnerNumberModel");
  966. }
  967. SpinnerNumberModel model = (SpinnerNumberModel)spinner.getModel();
  968. NumberFormatter formatter = new NumberEditorFormatter(model,
  969. format);
  970. DefaultFormatterFactory factory = new DefaultFormatterFactory(
  971. formatter);
  972. JFormattedTextField ftf = getTextField();
  973. ftf.setEditable(true);
  974. ftf.setFormatterFactory(factory);
  975. ftf.setHorizontalAlignment(JTextField.RIGHT);
  976. /* TBD - initializing the column width of the text field
  977. * is imprecise and doing it here is tricky because
  978. * the developer may configure the formatter later.
  979. */
  980. try {
  981. String maxString = formatter.valueToString(model.getMinimum());
  982. String minString = formatter.valueToString(model.getMaximum());
  983. ftf.setColumns(Math.max(maxString.length(),
  984. minString.length()));
  985. }
  986. catch (ParseException e) {
  987. // TBD should throw a chained error here
  988. }
  989. }
  990. /**
  991. * Returns the <code>java.text.NumberFormat</code> object the
  992. * <code>JFormattedTextField</code> uses to parse and format
  993. * numbers.
  994. *
  995. * @return the value of <code>getTextField().getFormatter().getFormat()</code>.
  996. * @see #getTextField
  997. * @see java.text.DecimalFormat
  998. */
  999. public DecimalFormat getFormat() {
  1000. return (DecimalFormat)((NumberFormatter)(getTextField().getFormatter())).getFormat();
  1001. }
  1002. /**
  1003. * Return our spinner ancestor's <code>SpinnerNumberModel</code>.
  1004. *
  1005. * @return <code>getSpinner().getModel()</code>
  1006. * @see #getSpinner
  1007. * @see #getTextField
  1008. */
  1009. public SpinnerNumberModel getModel() {
  1010. return (SpinnerNumberModel)(getSpinner().getModel());
  1011. }
  1012. }
  1013. /**
  1014. * An editor for a <code>JSpinner</code> whose model is a
  1015. * <code>SpinnerListModel</code>.
  1016. */
  1017. public static class ListEditor extends DefaultEditor
  1018. {
  1019. /**
  1020. * Construct a <code>JSpinner</code> editor that supports displaying
  1021. * and editing the value of a <code>SpinnerListModel</code>
  1022. * with a <code>JFormattedTextField</code>. <code>This</code>
  1023. * <code>ListEditor</code> becomes both a <code>ChangeListener</code>
  1024. * on the spinner and a <code>PropertyChangeListener</code>
  1025. * on the new <code>JFormattedTextField</code>.
  1026. *
  1027. * @param spinner the spinner whose model <code>this</code> editor will monitor
  1028. * @exception IllegalArgumentException if the spinners model is not
  1029. * an instance of <code>SpinnerListModel</code>
  1030. *
  1031. * @see #getModel
  1032. * @see SpinnerListModel
  1033. */
  1034. public ListEditor(JSpinner spinner) {
  1035. super(spinner);
  1036. if (!(spinner.getModel() instanceof SpinnerListModel)) {
  1037. throw new IllegalArgumentException("model not a SpinnerListModel");
  1038. }
  1039. getTextField().setEditable(true);
  1040. getTextField().setFormatterFactory(new
  1041. DefaultFormatterFactory(new ListFormatter()));
  1042. }
  1043. /**
  1044. * Return our spinner ancestor's <code>SpinnerNumberModel</code>.
  1045. *
  1046. * @return <code>getSpinner().getModel()</code>
  1047. * @see #getSpinner
  1048. * @see #getTextField
  1049. */
  1050. public SpinnerListModel getModel() {
  1051. return (SpinnerListModel)(getSpinner().getModel());
  1052. }
  1053. /**
  1054. * ListFormatter provides completion while text is being input
  1055. * into the JFormattedTextField. Completion is only done if the
  1056. * user is inserting text at the end of the document. Completion
  1057. * is done by way of the SpinnerListModel method findNextMatch.
  1058. */
  1059. private class ListFormatter extends
  1060. JFormattedTextField.AbstractFormatter {
  1061. private DocumentFilter filter;
  1062. public String valueToString(Object value) throws ParseException {
  1063. if (value == null) {
  1064. return "";
  1065. }
  1066. return value.toString();
  1067. }
  1068. public Object stringToValue(String string) throws ParseException {
  1069. return string;
  1070. }
  1071. protected DocumentFilter getDocumentFilter() {
  1072. if (filter == null) {
  1073. filter = new Filter();
  1074. }
  1075. return filter;
  1076. }
  1077. private class Filter extends DocumentFilter {
  1078. public void replace(FilterBypass fb, int offset, int length,
  1079. String string, AttributeSet attrs) throws
  1080. BadLocationException {
  1081. if (string != null && (offset + length) ==
  1082. fb.getDocument().getLength()) {
  1083. Object next = getModel().findNextMatch(
  1084. fb.getDocument().getText(0, offset) +
  1085. string);
  1086. String value = (next != null) ? next.toString() : null;
  1087. if (value != null) {
  1088. fb.remove(0, offset + length);
  1089. fb.insertString(0, value, null);
  1090. getFormattedTextField().select(offset +
  1091. string.length(),
  1092. value.length());
  1093. return;
  1094. }
  1095. }
  1096. super.replace(fb, offset, length, string, attrs);
  1097. }
  1098. public void insertString(FilterBypass fb, int offset,
  1099. String string, AttributeSet attr)
  1100. throws BadLocationException {
  1101. replace(fb, offset, 0, string, attr);
  1102. }
  1103. }
  1104. }
  1105. }
  1106. /**
  1107. * An Action implementation that is always disabled.
  1108. */
  1109. private static class DisabledAction implements Action {
  1110. public Object getValue(String key) {
  1111. return null;
  1112. }
  1113. public void putValue(String key, Object value) {
  1114. }
  1115. public void setEnabled(boolean b) {
  1116. }
  1117. public boolean isEnabled() {
  1118. return false;
  1119. }
  1120. public void addPropertyChangeListener(PropertyChangeListener l) {
  1121. }
  1122. public void removePropertyChangeListener(PropertyChangeListener l) {
  1123. }
  1124. public void actionPerformed(ActionEvent ae) {
  1125. }
  1126. }
  1127. }