1 /*
   2  * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javax.swing.plaf.basic;
  27 
  28 import java.awt.*;
  29 import java.awt.event.*;
  30 import java.text.ParseException;
  31 
  32 import javax.swing.*;
  33 import javax.swing.border.*;
  34 import javax.swing.event.*;
  35 import javax.swing.plaf.*;
  36 import javax.swing.text.*;
  37 
  38 import java.beans.*;
  39 import java.text.*;
  40 import java.util.*;
  41 import sun.swing.DefaultLookup;
  42 
  43 
  44 /**
  45  * The default Spinner UI delegate.
  46  *
  47  * @author Hans Muller
  48  * @since 1.4
  49  */
  50 public class BasicSpinnerUI extends SpinnerUI
  51 {
  52     /**
  53      * The spinner that we're a UI delegate for.  Initialized by
  54      * the <code>installUI</code> method, and reset to null
  55      * by <code>uninstallUI</code>.
  56      *
  57      * @see #installUI
  58      * @see #uninstallUI
  59      */
  60     protected JSpinner spinner;
  61     private Handler handler;
  62 
  63 
  64     /**
  65      * The mouse/action listeners that are added to the spinner's
  66      * arrow buttons.  These listeners are shared by all
  67      * spinner arrow buttons.
  68      *
  69      * @see #createNextButton
  70      * @see #createPreviousButton
  71      */
  72     private static final ArrowButtonHandler nextButtonHandler = new ArrowButtonHandler("increment", true);
  73     private static final ArrowButtonHandler previousButtonHandler = new ArrowButtonHandler("decrement", false);
  74     private PropertyChangeListener propertyChangeListener;
  75 
  76 
  77     /**
  78      * Used by the default LayoutManager class - SpinnerLayout for
  79      * missing (null) editor/nextButton/previousButton children.
  80      */
  81     private static final Dimension zeroSize = new Dimension(0, 0);
  82 
  83 
  84     /**
  85      * Returns a new instance of BasicSpinnerUI.  SpinnerListUI
  86      * delegates are allocated one per JSpinner.
  87      *
  88      * @param c the JSpinner (not used)
  89      * @see ComponentUI#createUI
  90      * @return a new BasicSpinnerUI object
  91      */
  92     public static ComponentUI createUI(JComponent c) {
  93         return new BasicSpinnerUI();
  94     }
  95 
  96 
  97     private void maybeAdd(Component c, String s) {
  98         if (c != null) {
  99             spinner.add(c, s);
 100         }
 101     }
 102 
 103 
 104     /**
 105      * Calls <code>installDefaults</code>, <code>installListeners</code>,
 106      * and then adds the components returned by <code>createNextButton</code>,
 107      * <code>createPreviousButton</code>, and <code>createEditor</code>.
 108      *
 109      * @param c the JSpinner
 110      * @see #installDefaults
 111      * @see #installListeners
 112      * @see #createNextButton
 113      * @see #createPreviousButton
 114      * @see #createEditor
 115      */
 116     public void installUI(JComponent c) {
 117         this.spinner = (JSpinner)c;
 118         installDefaults();
 119         installListeners();
 120         maybeAdd(createNextButton(), "Next");
 121         maybeAdd(createPreviousButton(), "Previous");
 122         maybeAdd(createEditor(), "Editor");
 123         updateEnabledState();
 124         installKeyboardActions();
 125     }
 126 
 127 
 128     /**
 129      * Calls <code>uninstallDefaults</code>, <code>uninstallListeners</code>,
 130      * and then removes all of the spinners children.
 131      *
 132      * @param c the JSpinner (not used)
 133      */
 134     public void uninstallUI(JComponent c) {
 135         uninstallDefaults();
 136         uninstallListeners();
 137         this.spinner = null;
 138         c.removeAll();
 139     }
 140 
 141 
 142     /**
 143      * Initializes <code>PropertyChangeListener</code> with
 144      * a shared object that delegates interesting PropertyChangeEvents
 145      * to protected methods.
 146      * <p>
 147      * This method is called by <code>installUI</code>.
 148      *
 149      * @see #replaceEditor
 150      * @see #uninstallListeners
 151      */
 152     protected void installListeners() {
 153         propertyChangeListener = createPropertyChangeListener();
 154         spinner.addPropertyChangeListener(propertyChangeListener);
 155         if (DefaultLookup.getBoolean(spinner, this,
 156             "Spinner.disableOnBoundaryValues", false)) {
 157             spinner.addChangeListener(getHandler());
 158         }
 159         JComponent editor = spinner.getEditor();
 160         if (editor != null && editor instanceof JSpinner.DefaultEditor) {
 161             JTextField tf = ((JSpinner.DefaultEditor)editor).getTextField();
 162             if (tf != null) {
 163                 tf.addFocusListener(nextButtonHandler);
 164                 tf.addFocusListener(previousButtonHandler);
 165             }
 166         }
 167     }
 168 
 169 
 170     /**
 171      * Removes the <code>PropertyChangeListener</code> added
 172      * by installListeners.
 173      * <p>
 174      * This method is called by <code>uninstallUI</code>.
 175      *
 176      * @see #installListeners
 177      */
 178     protected void uninstallListeners() {
 179         spinner.removePropertyChangeListener(propertyChangeListener);
 180         spinner.removeChangeListener(handler);
 181         JComponent editor = spinner.getEditor();
 182         removeEditorBorderListener(editor);
 183         if (editor instanceof JSpinner.DefaultEditor) {
 184             JTextField tf = ((JSpinner.DefaultEditor)editor).getTextField();
 185             if (tf != null) {
 186                 tf.removeFocusListener(nextButtonHandler);
 187                 tf.removeFocusListener(previousButtonHandler);
 188             }
 189         }
 190         propertyChangeListener = null;
 191         handler = null;
 192     }
 193 
 194 
 195     /**
 196      * Initialize the <code>JSpinner</code> <code>border</code>,
 197      * <code>foreground</code>, and <code>background</code>, properties
 198      * based on the corresponding "Spinner.*" properties from defaults table.
 199      * The <code>JSpinners</code> layout is set to the value returned by
 200      * <code>createLayout</code>.  This method is called by <code>installUI</code>.
 201      *
 202      * @see #uninstallDefaults
 203      * @see #installUI
 204      * @see #createLayout
 205      * @see LookAndFeel#installBorder
 206      * @see LookAndFeel#installColors
 207      */
 208     protected void installDefaults() {
 209         spinner.setLayout(createLayout());
 210         LookAndFeel.installBorder(spinner, "Spinner.border");
 211         LookAndFeel.installColorsAndFont(spinner, "Spinner.background", "Spinner.foreground", "Spinner.font");
 212         LookAndFeel.installProperty(spinner, "opaque", Boolean.TRUE);
 213 
 214         JComponent editor = spinner.getEditor();
 215         if (editor instanceof JSpinner.DefaultEditor) {
 216             JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField();
 217             if (tf != null) {
 218                 if (tf.getFont() instanceof UIResource) {
 219                     tf.setFont(new FontUIResource(spinner.getFont()));
 220                 }
 221             }
 222         }
 223     }
 224 
 225 
 226     /**
 227      * Sets the <code>JSpinner's</code> layout manager to null.  This
 228      * method is called by <code>uninstallUI</code>.
 229      *
 230      * @see #installDefaults
 231      * @see #uninstallUI
 232      */
 233     protected void uninstallDefaults() {
 234         spinner.setLayout(null);
 235     }
 236 
 237 
 238     private Handler getHandler() {
 239         if (handler == null) {
 240             handler = new Handler();
 241         }
 242         return handler;
 243     }
 244 
 245 
 246     /**
 247      * Installs the necessary listeners on the next button, <code>c</code>,
 248      * to update the <code>JSpinner</code> in response to a user gesture.
 249      *
 250      * @param c Component to install the listeners on
 251      * @throws NullPointerException if <code>c</code> is null.
 252      * @see #createNextButton
 253      * @since 1.5
 254      */
 255     protected void installNextButtonListeners(Component c) {
 256         installButtonListeners(c, nextButtonHandler);
 257     }
 258 
 259     /**
 260      * Installs the necessary listeners on the previous button, <code>c</code>,
 261      * to update the <code>JSpinner</code> in response to a user gesture.
 262      *
 263      * @param c Component to install the listeners on.
 264      * @throws NullPointerException if <code>c</code> is null.
 265      * @see #createPreviousButton
 266      * @since 1.5
 267      */
 268     protected void installPreviousButtonListeners(Component c) {
 269         installButtonListeners(c, previousButtonHandler);
 270     }
 271 
 272     private void installButtonListeners(Component c,
 273                                         ArrowButtonHandler handler) {
 274         if (c instanceof JButton) {
 275             ((JButton)c).addActionListener(handler);
 276         }
 277         c.addMouseListener(handler);
 278     }
 279 
 280     /**
 281      * Creates a <code>LayoutManager</code> that manages the <code>editor</code>,
 282      * <code>nextButton</code>, and <code>previousButton</code>
 283      * children of the JSpinner.  These three children must be
 284      * added with a constraint that identifies their role:
 285      * "Editor", "Next", and "Previous". The default layout manager
 286      * can handle the absence of any of these children.
 287      *
 288      * @return a LayoutManager for the editor, next button, and previous button.
 289      * @see #createNextButton
 290      * @see #createPreviousButton
 291      * @see #createEditor
 292      */
 293     protected LayoutManager createLayout() {
 294         return getHandler();
 295     }
 296 
 297 
 298     /**
 299      * Creates a <code>PropertyChangeListener</code> that can be
 300      * added to the JSpinner itself.  Typically, this listener
 301      * will call replaceEditor when the "editor" property changes,
 302      * since it's the <code>SpinnerUI's</code> responsibility to
 303      * add the editor to the JSpinner (and remove the old one).
 304      * This method is called by <code>installListeners</code>.
 305      *
 306      * @return A PropertyChangeListener for the JSpinner itself
 307      * @see #installListeners
 308      */
 309     protected PropertyChangeListener createPropertyChangeListener() {
 310         return getHandler();
 311     }
 312 
 313 
 314     /**
 315      * Creates a decrement button, i.e. component that replaces the spinner
 316      * value with the object returned by <code>spinner.getPreviousValue</code>.
 317      * By default the <code>previousButton</code> is a {@code JButton}. If the
 318      * decrement button is not needed this method should return {@code null}.
 319      *
 320      * @return a component that will replace the spinner's value with the
 321      *     previous value in the sequence, or {@code null}
 322      * @see #installUI
 323      * @see #createNextButton
 324      * @see #installPreviousButtonListeners
 325      */
 326     protected Component createPreviousButton() {
 327         Component c = createArrowButton(SwingConstants.SOUTH);
 328         c.setName("Spinner.previousButton");
 329         installPreviousButtonListeners(c);
 330         return c;
 331     }
 332 
 333 
 334     /**
 335      * Creates an increment button, i.e. component that replaces the spinner
 336      * value with the object returned by <code>spinner.getNextValue</code>.
 337      * By default the <code>nextButton</code> is a {@code JButton}. If the
 338      * increment button is not needed this method should return {@code null}.
 339      *
 340      * @return a component that will replace the spinner's value with the
 341      *     next value in the sequence, or {@code null}
 342      * @see #installUI
 343      * @see #createPreviousButton
 344      * @see #installNextButtonListeners
 345      */
 346     protected Component createNextButton() {
 347         Component c = createArrowButton(SwingConstants.NORTH);
 348         c.setName("Spinner.nextButton");
 349         installNextButtonListeners(c);
 350         return c;
 351     }
 352 
 353     private Component createArrowButton(int direction) {
 354         JButton b = new BasicArrowButton(direction);
 355         Border buttonBorder = UIManager.getBorder("Spinner.arrowButtonBorder");
 356         if (buttonBorder instanceof UIResource) {
 357             // Wrap the border to avoid having the UIResource be replaced by
 358             // the ButtonUI. This is the opposite of using BorderUIResource.
 359             b.setBorder(new CompoundBorder(buttonBorder, null));
 360         } else {
 361             b.setBorder(buttonBorder);
 362         }
 363         b.setInheritsPopupMenu(true);
 364         return b;
 365     }
 366 
 367 
 368     /**
 369      * This method is called by installUI to get the editor component
 370      * of the <code>JSpinner</code>.  By default it just returns
 371      * <code>JSpinner.getEditor()</code>.  Subclasses can override
 372      * <code>createEditor</code> to return a component that contains
 373      * the spinner's editor or null, if they're going to handle adding
 374      * the editor to the <code>JSpinner</code> in an
 375      * <code>installUI</code> override.
 376      * <p>
 377      * Typically this method would be overridden to wrap the editor
 378      * with a container with a custom border, since one can't assume
 379      * that the editors border can be set directly.
 380      * <p>
 381      * The <code>replaceEditor</code> method is called when the spinners
 382      * editor is changed with <code>JSpinner.setEditor</code>.  If you've
 383      * overriden this method, then you'll probably want to override
 384      * <code>replaceEditor</code> as well.
 385      *
 386      * @return the JSpinners editor JComponent, spinner.getEditor() by default
 387      * @see #installUI
 388      * @see #replaceEditor
 389      * @see JSpinner#getEditor
 390      */
 391     protected JComponent createEditor() {
 392         JComponent editor = spinner.getEditor();
 393         maybeRemoveEditorBorder(editor);
 394         installEditorBorderListener(editor);
 395         editor.setInheritsPopupMenu(true);
 396         updateEditorAlignment(editor);
 397         return editor;
 398     }
 399 
 400 
 401     /**
 402      * Called by the <code>PropertyChangeListener</code> when the
 403      * <code>JSpinner</code> editor property changes.  It's the responsibility
 404      * of this method to remove the old editor and add the new one.  By
 405      * default this operation is just:
 406      * <pre>
 407      * spinner.remove(oldEditor);
 408      * spinner.add(newEditor, "Editor");
 409      * </pre>
 410      * The implementation of <code>replaceEditor</code> should be coordinated
 411      * with the <code>createEditor</code> method.
 412      *
 413      * @param oldEditor an old instance of editor
 414      * @param newEditor a new instance of editor
 415      * @see #createEditor
 416      * @see #createPropertyChangeListener
 417      */
 418     protected void replaceEditor(JComponent oldEditor, JComponent newEditor) {
 419         spinner.remove(oldEditor);
 420         maybeRemoveEditorBorder(newEditor);
 421         installEditorBorderListener(newEditor);
 422         newEditor.setInheritsPopupMenu(true);
 423         spinner.add(newEditor, "Editor");
 424     }
 425 
 426     private void updateEditorAlignment(JComponent editor) {
 427         if (editor instanceof JSpinner.DefaultEditor) {
 428             // if editor alignment isn't set in LAF, we get 0 (CENTER) here
 429             int alignment = UIManager.getInt("Spinner.editorAlignment");
 430             JTextField text = ((JSpinner.DefaultEditor)editor).getTextField();
 431             text.setHorizontalAlignment(alignment);
 432         }
 433     }
 434 
 435     /**
 436      * Remove the border around the inner editor component for LaFs
 437      * that install an outside border around the spinner,
 438      */
 439     private void maybeRemoveEditorBorder(JComponent editor) {
 440         if (!UIManager.getBoolean("Spinner.editorBorderPainted")) {
 441             if (editor instanceof JPanel &&
 442                 editor.getBorder() == null &&
 443                 editor.getComponentCount() > 0) {
 444 
 445                 editor = (JComponent)editor.getComponent(0);
 446             }
 447 
 448             if (editor != null && editor.getBorder() instanceof UIResource) {
 449                 editor.setBorder(null);
 450             }
 451         }
 452     }
 453 
 454     /**
 455      * Remove the border around the inner editor component for LaFs
 456      * that install an outside border around the spinner,
 457      */
 458     private void installEditorBorderListener(JComponent editor) {
 459         if (!UIManager.getBoolean("Spinner.editorBorderPainted")) {
 460             if (editor instanceof JPanel &&
 461                 editor.getBorder() == null &&
 462                 editor.getComponentCount() > 0) {
 463 
 464                 editor = (JComponent)editor.getComponent(0);
 465             }
 466             if (editor != null &&
 467                 (editor.getBorder() == null ||
 468                  editor.getBorder() instanceof UIResource)) {
 469                 editor.addPropertyChangeListener(getHandler());
 470             }
 471         }
 472     }
 473 
 474     private void removeEditorBorderListener(JComponent editor) {
 475         if (!UIManager.getBoolean("Spinner.editorBorderPainted")) {
 476             if (editor instanceof JPanel &&
 477                 editor.getComponentCount() > 0) {
 478 
 479                 editor = (JComponent)editor.getComponent(0);
 480             }
 481             if (editor != null) {
 482                 editor.removePropertyChangeListener(getHandler());
 483             }
 484         }
 485     }
 486 
 487 
 488     /**
 489      * Updates the enabled state of the children Components based on the
 490      * enabled state of the <code>JSpinner</code>.
 491      */
 492     private void updateEnabledState() {
 493         updateEnabledState(spinner, spinner.isEnabled());
 494     }
 495 
 496 
 497     /**
 498      * Recursively updates the enabled state of the child
 499      * <code>Component</code>s of <code>c</code>.
 500      */
 501     private void updateEnabledState(Container c, boolean enabled) {
 502         for (int counter = c.getComponentCount() - 1; counter >= 0;counter--) {
 503             Component child = c.getComponent(counter);
 504 
 505             if (DefaultLookup.getBoolean(spinner, this,
 506                 "Spinner.disableOnBoundaryValues", false)) {
 507                 SpinnerModel model = spinner.getModel();
 508                 if (child.getName() == "Spinner.nextButton" &&
 509                     model.getNextValue() == null) {
 510                     child.setEnabled(false);
 511                 }
 512                 else if (child.getName() == "Spinner.previousButton" &&
 513                          model.getPreviousValue() == null) {
 514                     child.setEnabled(false);
 515                 }
 516                 else {
 517                     child.setEnabled(enabled);
 518                 }
 519             }
 520             else {
 521                 child.setEnabled(enabled);
 522             }
 523             if (child instanceof Container) {
 524                 updateEnabledState((Container)child, enabled);
 525             }
 526         }
 527     }
 528 
 529 
 530     /**
 531      * Installs the keyboard Actions onto the JSpinner.
 532      *
 533      * @since 1.5
 534      */
 535     protected void installKeyboardActions() {
 536         InputMap iMap = getInputMap(JComponent.
 537                                    WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 538 
 539         SwingUtilities.replaceUIInputMap(spinner, JComponent.
 540                                          WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
 541                                          iMap);
 542 
 543         LazyActionMap.installLazyActionMap(spinner, BasicSpinnerUI.class,
 544                 "Spinner.actionMap");
 545     }
 546 
 547     /**
 548      * Returns the InputMap to install for <code>condition</code>.
 549      */
 550     private InputMap getInputMap(int condition) {
 551         if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) {
 552             return (InputMap)DefaultLookup.get(spinner, this,
 553                     "Spinner.ancestorInputMap");
 554         }
 555         return null;
 556     }
 557 
 558     static void loadActionMap(LazyActionMap map) {
 559         map.put("increment", nextButtonHandler);
 560         map.put("decrement", previousButtonHandler);
 561     }
 562 
 563     /**
 564      * Returns the baseline.
 565      *
 566      * @throws NullPointerException {@inheritDoc}
 567      * @throws IllegalArgumentException {@inheritDoc}
 568      * @see javax.swing.JComponent#getBaseline(int, int)
 569      * @since 1.6
 570      */
 571     public int getBaseline(JComponent c, int width, int height) {
 572         super.getBaseline(c, width, height);
 573         JComponent editor = spinner.getEditor();
 574         Insets insets = spinner.getInsets();
 575         width = width - insets.left - insets.right;
 576         height = height - insets.top - insets.bottom;
 577         if (width >= 0 && height >= 0) {
 578             int baseline = editor.getBaseline(width, height);
 579             if (baseline >= 0) {
 580                 return insets.top + baseline;
 581             }
 582         }
 583         return -1;
 584     }
 585 
 586     /**
 587      * Returns an enum indicating how the baseline of the component
 588      * changes as the size changes.
 589      *
 590      * @throws NullPointerException {@inheritDoc}
 591      * @see javax.swing.JComponent#getBaseline(int, int)
 592      * @since 1.6
 593      */
 594     public Component.BaselineResizeBehavior getBaselineResizeBehavior(
 595             JComponent c) {
 596         super.getBaselineResizeBehavior(c);
 597         return spinner.getEditor().getBaselineResizeBehavior();
 598     }
 599 
 600     /**
 601      * A handler for spinner arrow button mouse and action events.  When
 602      * a left mouse pressed event occurs we look up the (enabled) spinner
 603      * that's the source of the event and start the autorepeat timer.  The
 604      * timer fires action events until any button is released at which
 605      * point the timer is stopped and the reference to the spinner cleared.
 606      * The timer doesn't start until after a 300ms delay, so often the
 607      * source of the initial (and final) action event is just the button
 608      * logic for mouse released - which means that we're relying on the fact
 609      * that our mouse listener runs after the buttons mouse listener.
 610      * <p>
 611      * Note that one instance of this handler is shared by all slider previous
 612      * arrow buttons and likewise for all of the next buttons,
 613      * so it doesn't have any state that persists beyond the limits
 614      * of a single button pressed/released gesture.
 615      */
 616     @SuppressWarnings("serial") // Superclass is not serializable across versions
 617     private static class ArrowButtonHandler extends AbstractAction
 618                                             implements FocusListener, MouseListener, UIResource {
 619         final javax.swing.Timer autoRepeatTimer;
 620         final boolean isNext;
 621         JSpinner spinner = null;
 622         JButton arrowButton = null;
 623 
 624         ArrowButtonHandler(String name, boolean isNext) {
 625             super(name);
 626             this.isNext = isNext;
 627             autoRepeatTimer = new javax.swing.Timer(60, this);
 628             autoRepeatTimer.setInitialDelay(300);
 629         }
 630 
 631         private JSpinner eventToSpinner(AWTEvent e) {
 632             Object src = e.getSource();
 633             while ((src instanceof Component) && !(src instanceof JSpinner)) {
 634                 src = ((Component)src).getParent();
 635             }
 636             return (src instanceof JSpinner) ? (JSpinner)src : null;
 637         }
 638 
 639         public void actionPerformed(ActionEvent e) {
 640             JSpinner spinner = this.spinner;
 641 
 642             if (!(e.getSource() instanceof javax.swing.Timer)) {
 643                 // Most likely resulting from being in ActionMap.
 644                 spinner = eventToSpinner(e);
 645                 if (e.getSource() instanceof JButton) {
 646                     arrowButton = (JButton)e.getSource();
 647                 }
 648             } else {
 649                 if (arrowButton!=null && !arrowButton.getModel().isPressed()
 650                     && autoRepeatTimer.isRunning()) {
 651                     autoRepeatTimer.stop();
 652                     spinner = null;
 653                     arrowButton = null;
 654                 }
 655             }
 656             if (spinner != null) {
 657                 try {
 658                     int calendarField = getCalendarField(spinner);
 659                     spinner.commitEdit();
 660                     if (calendarField != -1) {
 661                         ((SpinnerDateModel)spinner.getModel()).
 662                                  setCalendarField(calendarField);
 663                     }
 664                     Object value = (isNext) ? spinner.getNextValue() :
 665                                spinner.getPreviousValue();
 666                     if (value != null) {
 667                         spinner.setValue(value);
 668                         select(spinner);
 669                     }
 670                 } catch (IllegalArgumentException iae) {
 671                     UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 672                 } catch (ParseException pe) {
 673                     UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 674                 }
 675             }
 676         }
 677 
 678         /**
 679          * If the spinner's editor is a DateEditor, this selects the field
 680          * associated with the value that is being incremented.
 681          */
 682         private void select(JSpinner spinner) {
 683             JComponent editor = spinner.getEditor();
 684 
 685             if (editor instanceof JSpinner.DateEditor) {
 686                 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor;
 687                 JFormattedTextField ftf = dateEditor.getTextField();
 688                 Format format = dateEditor.getFormat();
 689                 Object value;
 690 
 691                 if (format != null && (value = spinner.getValue()) != null) {
 692                     SpinnerDateModel model = dateEditor.getModel();
 693                     DateFormat.Field field = DateFormat.Field.ofCalendarField(
 694                         model.getCalendarField());
 695 
 696                     if (field != null) {
 697                         try {
 698                             AttributedCharacterIterator iterator = format.
 699                                 formatToCharacterIterator(value);
 700                             if (!select(ftf, iterator, field) &&
 701                                        field == DateFormat.Field.HOUR0) {
 702                                 select(ftf, iterator, DateFormat.Field.HOUR1);
 703                             }
 704                         }
 705                         catch (IllegalArgumentException iae) {}
 706                     }
 707                 }
 708             }
 709         }
 710 
 711         /**
 712          * Selects the passed in field, returning true if it is found,
 713          * false otherwise.
 714          */
 715         private boolean select(JFormattedTextField ftf,
 716                                AttributedCharacterIterator iterator,
 717                                DateFormat.Field field) {
 718             int max = ftf.getDocument().getLength();
 719 
 720             iterator.first();
 721             do {
 722                 Map<?, ?> attrs = iterator.getAttributes();
 723 
 724                 if (attrs != null && attrs.containsKey(field)){
 725                     int start = iterator.getRunStart(field);
 726                     int end = iterator.getRunLimit(field);
 727 
 728                     if (start != -1 && end != -1 && start <= max &&
 729                                        end <= max) {
 730                         ftf.select(start, end);
 731                     }
 732                     return true;
 733                 }
 734             } while (iterator.next() != CharacterIterator.DONE);
 735             return false;
 736         }
 737 
 738         /**
 739          * Returns the calendarField under the start of the selection, or
 740          * -1 if there is no valid calendar field under the selection (or
 741          * the spinner isn't editing dates.
 742          */
 743         private int getCalendarField(JSpinner spinner) {
 744             JComponent editor = spinner.getEditor();
 745 
 746             if (editor instanceof JSpinner.DateEditor) {
 747                 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor;
 748                 JFormattedTextField ftf = dateEditor.getTextField();
 749                 int start = ftf.getSelectionStart();
 750                 JFormattedTextField.AbstractFormatter formatter =
 751                                     ftf.getFormatter();
 752 
 753                 if (formatter instanceof InternationalFormatter) {
 754                     Format.Field[] fields = ((InternationalFormatter)
 755                                              formatter).getFields(start);
 756 
 757                     for (int counter = 0; counter < fields.length; counter++) {
 758                         if (fields[counter] instanceof DateFormat.Field) {
 759                             int calendarField;
 760 
 761                             if (fields[counter] == DateFormat.Field.HOUR1) {
 762                                 calendarField = Calendar.HOUR;
 763                             }
 764                             else {
 765                                 calendarField = ((DateFormat.Field)
 766                                         fields[counter]).getCalendarField();
 767                             }
 768                             if (calendarField != -1) {
 769                                 return calendarField;
 770                             }
 771                         }
 772                     }
 773                 }
 774             }
 775             return -1;
 776         }
 777 
 778         public void mousePressed(MouseEvent e) {
 779             if (SwingUtilities.isLeftMouseButton(e) && e.getComponent().isEnabled()) {
 780                 spinner = eventToSpinner(e);
 781                 autoRepeatTimer.start();
 782 
 783                 focusSpinnerIfNecessary();
 784             }
 785         }
 786 
 787         public void mouseReleased(MouseEvent e) {
 788             autoRepeatTimer.stop();
 789             arrowButton = null;
 790             spinner = null;
 791         }
 792 
 793         public void mouseClicked(MouseEvent e) {
 794         }
 795 
 796         public void mouseEntered(MouseEvent e) {
 797             if (spinner != null && !autoRepeatTimer.isRunning() && spinner == eventToSpinner(e)) {
 798                 autoRepeatTimer.start();
 799             }
 800         }
 801 
 802         public void mouseExited(MouseEvent e) {
 803             if (autoRepeatTimer.isRunning()) {
 804                 autoRepeatTimer.stop();
 805             }
 806         }
 807 
 808         /**
 809          * Requests focus on a child of the spinner if the spinner doesn't
 810          * have focus.
 811          */
 812         private void focusSpinnerIfNecessary() {
 813             Component fo = KeyboardFocusManager.
 814                               getCurrentKeyboardFocusManager().getFocusOwner();
 815             if (spinner.isRequestFocusEnabled() && (
 816                         fo == null ||
 817                         !SwingUtilities.isDescendingFrom(fo, spinner))) {
 818                 Container root = spinner;
 819 
 820                 if (!root.isFocusCycleRoot()) {
 821                     root = root.getFocusCycleRootAncestor();
 822                 }
 823                 if (root != null) {
 824                     FocusTraversalPolicy ftp = root.getFocusTraversalPolicy();
 825                     Component child = ftp.getComponentAfter(root, spinner);
 826 
 827                     if (child != null && SwingUtilities.isDescendingFrom(
 828                                                         child, spinner)) {
 829                         child.requestFocus();
 830                     }
 831                 }
 832             }
 833         }
 834 
 835         public void focusGained(FocusEvent e) {
 836         }
 837 
 838         public void focusLost(FocusEvent e) {
 839             if (spinner == eventToSpinner(e)) {
 840                 if (autoRepeatTimer.isRunning()) {
 841                     autoRepeatTimer.stop();
 842                 }
 843                 spinner = null;
 844                 if (arrowButton != null) {
 845                     ButtonModel model = arrowButton.getModel();
 846                     model.setPressed(false);
 847                     model.setArmed(false);
 848                     arrowButton = null;
 849                 }
 850             }
 851         }
 852     }
 853 
 854 
 855     private static class Handler implements LayoutManager,
 856             PropertyChangeListener, ChangeListener {
 857         //
 858         // LayoutManager
 859         //
 860         private Component nextButton = null;
 861         private Component previousButton = null;
 862         private Component editor = null;
 863 
 864         public void addLayoutComponent(String name, Component c) {
 865             if ("Next".equals(name)) {
 866                 nextButton = c;
 867             }
 868             else if ("Previous".equals(name)) {
 869                 previousButton = c;
 870             }
 871             else if ("Editor".equals(name)) {
 872                 editor = c;
 873             }
 874         }
 875 
 876         public void removeLayoutComponent(Component c) {
 877             if (c == nextButton) {
 878                 nextButton = null;
 879             }
 880             else if (c == previousButton) {
 881                 previousButton = null;
 882             }
 883             else if (c == editor) {
 884                 editor = null;
 885             }
 886         }
 887 
 888         private Dimension preferredSize(Component c) {
 889             return (c == null) ? zeroSize : c.getPreferredSize();
 890         }
 891 
 892         public Dimension preferredLayoutSize(Container parent) {
 893             Dimension nextD = preferredSize(nextButton);
 894             Dimension previousD = preferredSize(previousButton);
 895             Dimension editorD = preferredSize(editor);
 896 
 897             /* Force the editors height to be a multiple of 2
 898              */
 899             editorD.height = ((editorD.height + 1) / 2) * 2;
 900 
 901             Dimension size = new Dimension(editorD.width, editorD.height);
 902             size.width += Math.max(nextD.width, previousD.width);
 903             Insets insets = parent.getInsets();
 904             size.width += insets.left + insets.right;
 905             size.height += insets.top + insets.bottom;
 906             return size;
 907         }
 908 
 909         public Dimension minimumLayoutSize(Container parent) {
 910             return preferredLayoutSize(parent);
 911         }
 912 
 913         private void setBounds(Component c, int x, int y, int width, int height) {
 914             if (c != null) {
 915                 c.setBounds(x, y, width, height);
 916             }
 917         }
 918 
 919         public void layoutContainer(Container parent) {
 920             int width  = parent.getWidth();
 921             int height = parent.getHeight();
 922 
 923             Insets insets = parent.getInsets();
 924 
 925             if (nextButton == null && previousButton == null) {
 926                 setBounds(editor, insets.left,  insets.top, width - insets.left - insets.right,
 927                         height - insets.top - insets.bottom);
 928 
 929                 return;
 930             }
 931 
 932             Dimension nextD = preferredSize(nextButton);
 933             Dimension previousD = preferredSize(previousButton);
 934             int buttonsWidth = Math.max(nextD.width, previousD.width);
 935             int editorHeight = height - (insets.top + insets.bottom);
 936 
 937             // The arrowButtonInsets value is used instead of the JSpinner's
 938             // insets if not null. Defining this to be (0, 0, 0, 0) causes the
 939             // buttons to be aligned with the outer edge of the spinner's
 940             // border, and leaving it as "null" places the buttons completely
 941             // inside the spinner's border.
 942             Insets buttonInsets = UIManager.getInsets("Spinner.arrowButtonInsets");
 943             if (buttonInsets == null) {
 944                 buttonInsets = insets;
 945             }
 946 
 947             /* Deal with the spinner's componentOrientation property.
 948              */
 949             int editorX, editorWidth, buttonsX;
 950             if (parent.getComponentOrientation().isLeftToRight()) {
 951                 editorX = insets.left;
 952                 editorWidth = width - insets.left - buttonsWidth - buttonInsets.right;
 953                 buttonsX = width - buttonsWidth - buttonInsets.right;
 954             } else {
 955                 buttonsX = buttonInsets.left;
 956                 editorX = buttonsX + buttonsWidth;
 957                 editorWidth = width - buttonInsets.left - buttonsWidth - insets.right;
 958             }
 959 
 960             int nextY = buttonInsets.top;
 961             int nextHeight = (height / 2) + (height % 2) - nextY;
 962             int previousY = buttonInsets.top + nextHeight;
 963             int previousHeight = height - previousY - buttonInsets.bottom;
 964 
 965             setBounds(editor,         editorX,  insets.top, editorWidth, editorHeight);
 966             setBounds(nextButton,     buttonsX, nextY,      buttonsWidth, nextHeight);
 967             setBounds(previousButton, buttonsX, previousY,  buttonsWidth, previousHeight);
 968         }
 969 
 970 
 971         //
 972         // PropertyChangeListener
 973         //
 974         public void propertyChange(PropertyChangeEvent e)
 975         {
 976             String propertyName = e.getPropertyName();
 977             if (e.getSource() instanceof JSpinner) {
 978                 JSpinner spinner = (JSpinner)(e.getSource());
 979                 SpinnerUI spinnerUI = spinner.getUI();
 980 
 981                 if (spinnerUI instanceof BasicSpinnerUI) {
 982                     BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
 983 
 984                     if ("editor".equals(propertyName)) {
 985                         JComponent oldEditor = (JComponent)e.getOldValue();
 986                         JComponent newEditor = (JComponent)e.getNewValue();
 987                         ui.replaceEditor(oldEditor, newEditor);
 988                         ui.updateEnabledState();
 989                         if (oldEditor instanceof JSpinner.DefaultEditor) {
 990                             JTextField tf =
 991                                 ((JSpinner.DefaultEditor)oldEditor).getTextField();
 992                             if (tf != null) {
 993                                 tf.removeFocusListener(nextButtonHandler);
 994                                 tf.removeFocusListener(previousButtonHandler);
 995                             }
 996                         }
 997                         if (newEditor instanceof JSpinner.DefaultEditor) {
 998                             JTextField tf =
 999                                 ((JSpinner.DefaultEditor)newEditor).getTextField();
1000                             if (tf != null) {
1001                                 if (tf.getFont() instanceof UIResource) {
1002                                     tf.setFont(new FontUIResource(spinner.getFont()));
1003                                 }
1004                                 tf.addFocusListener(nextButtonHandler);
1005                                 tf.addFocusListener(previousButtonHandler);
1006                             }
1007                         }
1008                     }
1009                     else if ("enabled".equals(propertyName) ||
1010                              "model".equals(propertyName)) {
1011                         ui.updateEnabledState();
1012                     }
1013                     else if ("font".equals(propertyName) ||
1014                              "graphicsConfiguration".equals(propertyName)) {
1015                         JComponent editor = spinner.getEditor();
1016                         if (editor instanceof JSpinner.DefaultEditor) {
1017                             JTextField tf =
1018                                 ((JSpinner.DefaultEditor)editor).getTextField();
1019                             if (tf != null) {
1020                                 if (tf.getFont() instanceof UIResource) {
1021                                     tf.setFont(new FontUIResource(spinner.getFont()));
1022                                 }
1023                             }
1024                         }
1025                     }
1026                     else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) {
1027                         updateToolTipTextForChildren(spinner);
1028                     } else if ("componentOrientation".equals(propertyName)) {
1029                         ComponentOrientation o
1030                                 = (ComponentOrientation) e.getNewValue();
1031                         if (o != (ComponentOrientation) e.getOldValue()) {
1032                             JComponent editor = spinner.getEditor();
1033                             if (editor != null) {
1034                                 editor.applyComponentOrientation(o);
1035                             }
1036                             spinner.revalidate();
1037                             spinner.repaint();
1038                         }
1039                     }
1040                 }
1041             } else if (e.getSource() instanceof JComponent) {
1042                 JComponent c = (JComponent)e.getSource();
1043                 if ((c.getParent() instanceof JPanel) &&
1044                     (c.getParent().getParent() instanceof JSpinner) &&
1045                     "border".equals(propertyName)) {
1046 
1047                     JSpinner spinner = (JSpinner)c.getParent().getParent();
1048                     SpinnerUI spinnerUI = spinner.getUI();
1049                     if (spinnerUI instanceof BasicSpinnerUI) {
1050                         BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
1051                         ui.maybeRemoveEditorBorder(c);
1052                     }
1053                 }
1054             }
1055         }
1056 
1057         // Syncronizes the ToolTip text for the components within the spinner
1058         // to be the same value as the spinner ToolTip text.
1059         private void updateToolTipTextForChildren(JComponent spinner) {
1060             String toolTipText = spinner.getToolTipText();
1061             Component[] children = spinner.getComponents();
1062             for (int i = 0; i < children.length; i++) {
1063                 if (children[i] instanceof JSpinner.DefaultEditor) {
1064                     JTextField tf = ((JSpinner.DefaultEditor)children[i]).getTextField();
1065                     if (tf != null) {
1066                         tf.setToolTipText(toolTipText);
1067                     }
1068                 } else if (children[i] instanceof JComponent) {
1069                     ((JComponent)children[i]).setToolTipText( spinner.getToolTipText() );
1070                 }
1071             }
1072         }
1073 
1074         public void stateChanged(ChangeEvent e) {
1075             if (e.getSource() instanceof JSpinner) {
1076                 JSpinner spinner = (JSpinner)e.getSource();
1077                 SpinnerUI spinnerUI = spinner.getUI();
1078                 if (DefaultLookup.getBoolean(spinner, spinnerUI,
1079                     "Spinner.disableOnBoundaryValues", false) &&
1080                     spinnerUI instanceof BasicSpinnerUI) {
1081                     BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
1082                     ui.updateEnabledState();
1083                 }
1084             }
1085         }
1086     }
1087 }