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                     Font font = spinner.getFont();
 220                     tf.setFont(font == null ? null : new FontUIResource(font));
 221                 }
 222             }
 223         }
 224     }
 225 
 226 
 227     /**
 228      * Sets the <code>JSpinner's</code> layout manager to null.  This
 229      * method is called by <code>uninstallUI</code>.
 230      *
 231      * @see #installDefaults
 232      * @see #uninstallUI
 233      */
 234     protected void uninstallDefaults() {
 235         spinner.setLayout(null);
 236     }
 237 
 238 
 239     private Handler getHandler() {
 240         if (handler == null) {
 241             handler = new Handler();
 242         }
 243         return handler;
 244     }
 245 
 246 
 247     /**
 248      * Installs the necessary listeners on the next button, <code>c</code>,
 249      * to update the <code>JSpinner</code> in response to a user gesture.
 250      *
 251      * @param c Component to install the listeners on
 252      * @throws NullPointerException if <code>c</code> is null.
 253      * @see #createNextButton
 254      * @since 1.5
 255      */
 256     protected void installNextButtonListeners(Component c) {
 257         installButtonListeners(c, nextButtonHandler);
 258     }
 259 
 260     /**
 261      * Installs the necessary listeners on the previous button, <code>c</code>,
 262      * to update the <code>JSpinner</code> in response to a user gesture.
 263      *
 264      * @param c Component to install the listeners on.
 265      * @throws NullPointerException if <code>c</code> is null.
 266      * @see #createPreviousButton
 267      * @since 1.5
 268      */
 269     protected void installPreviousButtonListeners(Component c) {
 270         installButtonListeners(c, previousButtonHandler);
 271     }
 272 
 273     private void installButtonListeners(Component c,
 274                                         ArrowButtonHandler handler) {
 275         if (c instanceof JButton) {
 276             ((JButton)c).addActionListener(handler);
 277         }
 278         c.addMouseListener(handler);
 279     }
 280 
 281     /**
 282      * Creates a <code>LayoutManager</code> that manages the <code>editor</code>,
 283      * <code>nextButton</code>, and <code>previousButton</code>
 284      * children of the JSpinner.  These three children must be
 285      * added with a constraint that identifies their role:
 286      * "Editor", "Next", and "Previous". The default layout manager
 287      * can handle the absence of any of these children.
 288      *
 289      * @return a LayoutManager for the editor, next button, and previous button.
 290      * @see #createNextButton
 291      * @see #createPreviousButton
 292      * @see #createEditor
 293      */
 294     protected LayoutManager createLayout() {
 295         return getHandler();
 296     }
 297 
 298 
 299     /**
 300      * Creates a <code>PropertyChangeListener</code> that can be
 301      * added to the JSpinner itself.  Typically, this listener
 302      * will call replaceEditor when the "editor" property changes,
 303      * since it's the <code>SpinnerUI's</code> responsibility to
 304      * add the editor to the JSpinner (and remove the old one).
 305      * This method is called by <code>installListeners</code>.
 306      *
 307      * @return A PropertyChangeListener for the JSpinner itself
 308      * @see #installListeners
 309      */
 310     protected PropertyChangeListener createPropertyChangeListener() {
 311         return getHandler();
 312     }
 313 
 314 
 315     /**
 316      * Creates a decrement button, i.e. component that replaces the spinner
 317      * value with the object returned by <code>spinner.getPreviousValue</code>.
 318      * By default the <code>previousButton</code> is a {@code JButton}. If the
 319      * decrement button is not needed this method should return {@code null}.
 320      *
 321      * @return a component that will replace the spinner's value with the
 322      *     previous value in the sequence, or {@code null}
 323      * @see #installUI
 324      * @see #createNextButton
 325      * @see #installPreviousButtonListeners
 326      */
 327     protected Component createPreviousButton() {
 328         Component c = createArrowButton(SwingConstants.SOUTH);
 329         c.setName("Spinner.previousButton");
 330         installPreviousButtonListeners(c);
 331         return c;
 332     }
 333 
 334 
 335     /**
 336      * Creates an increment button, i.e. component that replaces the spinner
 337      * value with the object returned by <code>spinner.getNextValue</code>.
 338      * By default the <code>nextButton</code> is a {@code JButton}. If the
 339      * increment button is not needed this method should return {@code null}.
 340      *
 341      * @return a component that will replace the spinner's value with the
 342      *     next value in the sequence, or {@code null}
 343      * @see #installUI
 344      * @see #createPreviousButton
 345      * @see #installNextButtonListeners
 346      */
 347     protected Component createNextButton() {
 348         Component c = createArrowButton(SwingConstants.NORTH);
 349         c.setName("Spinner.nextButton");
 350         installNextButtonListeners(c);
 351         return c;
 352     }
 353 
 354     private Component createArrowButton(int direction) {
 355         JButton b = new BasicArrowButton(direction);
 356         Border buttonBorder = UIManager.getBorder("Spinner.arrowButtonBorder");
 357         if (buttonBorder instanceof UIResource) {
 358             // Wrap the border to avoid having the UIResource be replaced by
 359             // the ButtonUI. This is the opposite of using BorderUIResource.
 360             b.setBorder(new CompoundBorder(buttonBorder, null));
 361         } else {
 362             b.setBorder(buttonBorder);
 363         }
 364         b.setInheritsPopupMenu(true);
 365         return b;
 366     }
 367 
 368 
 369     /**
 370      * This method is called by installUI to get the editor component
 371      * of the <code>JSpinner</code>.  By default it just returns
 372      * <code>JSpinner.getEditor()</code>.  Subclasses can override
 373      * <code>createEditor</code> to return a component that contains
 374      * the spinner's editor or null, if they're going to handle adding
 375      * the editor to the <code>JSpinner</code> in an
 376      * <code>installUI</code> override.
 377      * <p>
 378      * Typically this method would be overridden to wrap the editor
 379      * with a container with a custom border, since one can't assume
 380      * that the editors border can be set directly.
 381      * <p>
 382      * The <code>replaceEditor</code> method is called when the spinners
 383      * editor is changed with <code>JSpinner.setEditor</code>.  If you've
 384      * overriden this method, then you'll probably want to override
 385      * <code>replaceEditor</code> as well.
 386      *
 387      * @return the JSpinners editor JComponent, spinner.getEditor() by default
 388      * @see #installUI
 389      * @see #replaceEditor
 390      * @see JSpinner#getEditor
 391      */
 392     protected JComponent createEditor() {
 393         JComponent editor = spinner.getEditor();
 394         maybeRemoveEditorBorder(editor);
 395         installEditorBorderListener(editor);
 396         editor.setInheritsPopupMenu(true);
 397         updateEditorAlignment(editor);
 398         return editor;
 399     }
 400 
 401 
 402     /**
 403      * Called by the <code>PropertyChangeListener</code> when the
 404      * <code>JSpinner</code> editor property changes.  It's the responsibility
 405      * of this method to remove the old editor and add the new one.  By
 406      * default this operation is just:
 407      * <pre>
 408      * spinner.remove(oldEditor);
 409      * spinner.add(newEditor, "Editor");
 410      * </pre>
 411      * The implementation of <code>replaceEditor</code> should be coordinated
 412      * with the <code>createEditor</code> method.
 413      *
 414      * @param oldEditor an old instance of editor
 415      * @param newEditor a new instance of editor
 416      * @see #createEditor
 417      * @see #createPropertyChangeListener
 418      */
 419     protected void replaceEditor(JComponent oldEditor, JComponent newEditor) {
 420         spinner.remove(oldEditor);
 421         maybeRemoveEditorBorder(newEditor);
 422         installEditorBorderListener(newEditor);
 423         newEditor.setInheritsPopupMenu(true);
 424         spinner.add(newEditor, "Editor");
 425     }
 426 
 427     private void updateEditorAlignment(JComponent editor) {
 428         if (editor instanceof JSpinner.DefaultEditor) {
 429             // if editor alignment isn't set in LAF, we get 0 (CENTER) here
 430             int alignment = UIManager.getInt("Spinner.editorAlignment");
 431             JTextField text = ((JSpinner.DefaultEditor)editor).getTextField();
 432             text.setHorizontalAlignment(alignment);
 433         }
 434     }
 435 
 436     /**
 437      * Remove the border around the inner editor component for LaFs
 438      * that install an outside border around the spinner,
 439      */
 440     private void maybeRemoveEditorBorder(JComponent editor) {
 441         if (!UIManager.getBoolean("Spinner.editorBorderPainted")) {
 442             if (editor instanceof JPanel &&
 443                 editor.getBorder() == null &&
 444                 editor.getComponentCount() > 0) {
 445 
 446                 editor = (JComponent)editor.getComponent(0);
 447             }
 448 
 449             if (editor != null && editor.getBorder() instanceof UIResource) {
 450                 editor.setBorder(null);
 451             }
 452         }
 453     }
 454 
 455     /**
 456      * Remove the border around the inner editor component for LaFs
 457      * that install an outside border around the spinner,
 458      */
 459     private void installEditorBorderListener(JComponent editor) {
 460         if (!UIManager.getBoolean("Spinner.editorBorderPainted")) {
 461             if (editor instanceof JPanel &&
 462                 editor.getBorder() == null &&
 463                 editor.getComponentCount() > 0) {
 464 
 465                 editor = (JComponent)editor.getComponent(0);
 466             }
 467             if (editor != null &&
 468                 (editor.getBorder() == null ||
 469                  editor.getBorder() instanceof UIResource)) {
 470                 editor.addPropertyChangeListener(getHandler());
 471             }
 472         }
 473     }
 474 
 475     private void removeEditorBorderListener(JComponent editor) {
 476         if (!UIManager.getBoolean("Spinner.editorBorderPainted")) {
 477             if (editor instanceof JPanel &&
 478                 editor.getComponentCount() > 0) {
 479 
 480                 editor = (JComponent)editor.getComponent(0);
 481             }
 482             if (editor != null) {
 483                 editor.removePropertyChangeListener(getHandler());
 484             }
 485         }
 486     }
 487 
 488 
 489     /**
 490      * Updates the enabled state of the children Components based on the
 491      * enabled state of the <code>JSpinner</code>.
 492      */
 493     private void updateEnabledState() {
 494         updateEnabledState(spinner, spinner.isEnabled());
 495     }
 496 
 497 
 498     /**
 499      * Recursively updates the enabled state of the child
 500      * <code>Component</code>s of <code>c</code>.
 501      */
 502     private void updateEnabledState(Container c, boolean enabled) {
 503         for (int counter = c.getComponentCount() - 1; counter >= 0;counter--) {
 504             Component child = c.getComponent(counter);
 505 
 506             if (DefaultLookup.getBoolean(spinner, this,
 507                 "Spinner.disableOnBoundaryValues", false)) {
 508                 SpinnerModel model = spinner.getModel();
 509                 if (child.getName() == "Spinner.nextButton" &&
 510                     model.getNextValue() == null) {
 511                     child.setEnabled(false);
 512                 }
 513                 else if (child.getName() == "Spinner.previousButton" &&
 514                          model.getPreviousValue() == null) {
 515                     child.setEnabled(false);
 516                 }
 517                 else {
 518                     child.setEnabled(enabled);
 519                 }
 520             }
 521             else {
 522                 child.setEnabled(enabled);
 523             }
 524             if (child instanceof Container) {
 525                 updateEnabledState((Container)child, enabled);
 526             }
 527         }
 528     }
 529 
 530 
 531     /**
 532      * Installs the keyboard Actions onto the JSpinner.
 533      *
 534      * @since 1.5
 535      */
 536     protected void installKeyboardActions() {
 537         InputMap iMap = getInputMap(JComponent.
 538                                    WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 539 
 540         SwingUtilities.replaceUIInputMap(spinner, JComponent.
 541                                          WHEN_ANCESTOR_OF_FOCUSED_COMPONENT,
 542                                          iMap);
 543 
 544         LazyActionMap.installLazyActionMap(spinner, BasicSpinnerUI.class,
 545                 "Spinner.actionMap");
 546     }
 547 
 548     /**
 549      * Returns the InputMap to install for <code>condition</code>.
 550      */
 551     private InputMap getInputMap(int condition) {
 552         if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) {
 553             return (InputMap)DefaultLookup.get(spinner, this,
 554                     "Spinner.ancestorInputMap");
 555         }
 556         return null;
 557     }
 558 
 559     static void loadActionMap(LazyActionMap map) {
 560         map.put("increment", nextButtonHandler);
 561         map.put("decrement", previousButtonHandler);
 562     }
 563 
 564     /**
 565      * Returns the baseline.
 566      *
 567      * @throws NullPointerException {@inheritDoc}
 568      * @throws IllegalArgumentException {@inheritDoc}
 569      * @see javax.swing.JComponent#getBaseline(int, int)
 570      * @since 1.6
 571      */
 572     public int getBaseline(JComponent c, int width, int height) {
 573         super.getBaseline(c, width, height);
 574         JComponent editor = spinner.getEditor();
 575         Insets insets = spinner.getInsets();
 576         width = width - insets.left - insets.right;
 577         height = height - insets.top - insets.bottom;
 578         if (width >= 0 && height >= 0) {
 579             int baseline = editor.getBaseline(width, height);
 580             if (baseline >= 0) {
 581                 return insets.top + baseline;
 582             }
 583         }
 584         return -1;
 585     }
 586 
 587     /**
 588      * Returns an enum indicating how the baseline of the component
 589      * changes as the size changes.
 590      *
 591      * @throws NullPointerException {@inheritDoc}
 592      * @see javax.swing.JComponent#getBaseline(int, int)
 593      * @since 1.6
 594      */
 595     public Component.BaselineResizeBehavior getBaselineResizeBehavior(
 596             JComponent c) {
 597         super.getBaselineResizeBehavior(c);
 598         return spinner.getEditor().getBaselineResizeBehavior();
 599     }
 600 
 601     /**
 602      * A handler for spinner arrow button mouse and action events.  When
 603      * a left mouse pressed event occurs we look up the (enabled) spinner
 604      * that's the source of the event and start the autorepeat timer.  The
 605      * timer fires action events until any button is released at which
 606      * point the timer is stopped and the reference to the spinner cleared.
 607      * The timer doesn't start until after a 300ms delay, so often the
 608      * source of the initial (and final) action event is just the button
 609      * logic for mouse released - which means that we're relying on the fact
 610      * that our mouse listener runs after the buttons mouse listener.
 611      * <p>
 612      * Note that one instance of this handler is shared by all slider previous
 613      * arrow buttons and likewise for all of the next buttons,
 614      * so it doesn't have any state that persists beyond the limits
 615      * of a single button pressed/released gesture.
 616      */
 617     @SuppressWarnings("serial") // Superclass is not serializable across versions
 618     private static class ArrowButtonHandler extends AbstractAction
 619                                             implements FocusListener, MouseListener, UIResource {
 620         final javax.swing.Timer autoRepeatTimer;
 621         final boolean isNext;
 622         JSpinner spinner = null;
 623         JButton arrowButton = null;
 624 
 625         ArrowButtonHandler(String name, boolean isNext) {
 626             super(name);
 627             this.isNext = isNext;
 628             autoRepeatTimer = new javax.swing.Timer(60, this);
 629             autoRepeatTimer.setInitialDelay(300);
 630         }
 631 
 632         private JSpinner eventToSpinner(AWTEvent e) {
 633             Object src = e.getSource();
 634             while ((src instanceof Component) && !(src instanceof JSpinner)) {
 635                 src = ((Component)src).getParent();
 636             }
 637             return (src instanceof JSpinner) ? (JSpinner)src : null;
 638         }
 639 
 640         public void actionPerformed(ActionEvent e) {
 641             JSpinner spinner = this.spinner;
 642 
 643             if (!(e.getSource() instanceof javax.swing.Timer)) {
 644                 // Most likely resulting from being in ActionMap.
 645                 spinner = eventToSpinner(e);
 646                 if (e.getSource() instanceof JButton) {
 647                     arrowButton = (JButton)e.getSource();
 648                 }
 649             } else {
 650                 if (arrowButton!=null && !arrowButton.getModel().isPressed()
 651                     && autoRepeatTimer.isRunning()) {
 652                     autoRepeatTimer.stop();
 653                     spinner = null;
 654                     arrowButton = null;
 655                 }
 656             }
 657             if (spinner != null) {
 658                 try {
 659                     int calendarField = getCalendarField(spinner);
 660                     spinner.commitEdit();
 661                     if (calendarField != -1) {
 662                         ((SpinnerDateModel)spinner.getModel()).
 663                                  setCalendarField(calendarField);
 664                     }
 665                     Object value = (isNext) ? spinner.getNextValue() :
 666                                spinner.getPreviousValue();
 667                     if (value != null) {
 668                         spinner.setValue(value);
 669                         select(spinner);
 670                     }
 671                 } catch (IllegalArgumentException iae) {
 672                     UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 673                 } catch (ParseException pe) {
 674                     UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 675                 }
 676             }
 677         }
 678 
 679         /**
 680          * If the spinner's editor is a DateEditor, this selects the field
 681          * associated with the value that is being incremented.
 682          */
 683         private void select(JSpinner spinner) {
 684             JComponent editor = spinner.getEditor();
 685 
 686             if (editor instanceof JSpinner.DateEditor) {
 687                 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor;
 688                 JFormattedTextField ftf = dateEditor.getTextField();
 689                 Format format = dateEditor.getFormat();
 690                 Object value;
 691 
 692                 if (format != null && (value = spinner.getValue()) != null) {
 693                     SpinnerDateModel model = dateEditor.getModel();
 694                     DateFormat.Field field = DateFormat.Field.ofCalendarField(
 695                         model.getCalendarField());
 696 
 697                     if (field != null) {
 698                         try {
 699                             AttributedCharacterIterator iterator = format.
 700                                 formatToCharacterIterator(value);
 701                             if (!select(ftf, iterator, field) &&
 702                                        field == DateFormat.Field.HOUR0) {
 703                                 select(ftf, iterator, DateFormat.Field.HOUR1);
 704                             }
 705                         }
 706                         catch (IllegalArgumentException iae) {}
 707                     }
 708                 }
 709             }
 710         }
 711 
 712         /**
 713          * Selects the passed in field, returning true if it is found,
 714          * false otherwise.
 715          */
 716         private boolean select(JFormattedTextField ftf,
 717                                AttributedCharacterIterator iterator,
 718                                DateFormat.Field field) {
 719             int max = ftf.getDocument().getLength();
 720 
 721             iterator.first();
 722             do {
 723                 Map<?, ?> attrs = iterator.getAttributes();
 724 
 725                 if (attrs != null && attrs.containsKey(field)){
 726                     int start = iterator.getRunStart(field);
 727                     int end = iterator.getRunLimit(field);
 728 
 729                     if (start != -1 && end != -1 && start <= max &&
 730                                        end <= max) {
 731                         ftf.select(start, end);
 732                     }
 733                     return true;
 734                 }
 735             } while (iterator.next() != CharacterIterator.DONE);
 736             return false;
 737         }
 738 
 739         /**
 740          * Returns the calendarField under the start of the selection, or
 741          * -1 if there is no valid calendar field under the selection (or
 742          * the spinner isn't editing dates.
 743          */
 744         private int getCalendarField(JSpinner spinner) {
 745             JComponent editor = spinner.getEditor();
 746 
 747             if (editor instanceof JSpinner.DateEditor) {
 748                 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor;
 749                 JFormattedTextField ftf = dateEditor.getTextField();
 750                 int start = ftf.getSelectionStart();
 751                 JFormattedTextField.AbstractFormatter formatter =
 752                                     ftf.getFormatter();
 753 
 754                 if (formatter instanceof InternationalFormatter) {
 755                     Format.Field[] fields = ((InternationalFormatter)
 756                                              formatter).getFields(start);
 757 
 758                     for (int counter = 0; counter < fields.length; counter++) {
 759                         if (fields[counter] instanceof DateFormat.Field) {
 760                             int calendarField;
 761 
 762                             if (fields[counter] == DateFormat.Field.HOUR1) {
 763                                 calendarField = Calendar.HOUR;
 764                             }
 765                             else {
 766                                 calendarField = ((DateFormat.Field)
 767                                         fields[counter]).getCalendarField();
 768                             }
 769                             if (calendarField != -1) {
 770                                 return calendarField;
 771                             }
 772                         }
 773                     }
 774                 }
 775             }
 776             return -1;
 777         }
 778 
 779         public void mousePressed(MouseEvent e) {
 780             if (SwingUtilities.isLeftMouseButton(e) && e.getComponent().isEnabled()) {
 781                 spinner = eventToSpinner(e);
 782                 autoRepeatTimer.start();
 783 
 784                 focusSpinnerIfNecessary();
 785             }
 786         }
 787 
 788         public void mouseReleased(MouseEvent e) {
 789             autoRepeatTimer.stop();
 790             arrowButton = null;
 791             spinner = null;
 792         }
 793 
 794         public void mouseClicked(MouseEvent e) {
 795         }
 796 
 797         public void mouseEntered(MouseEvent e) {
 798             if (spinner != null && !autoRepeatTimer.isRunning() && spinner == eventToSpinner(e)) {
 799                 autoRepeatTimer.start();
 800             }
 801         }
 802 
 803         public void mouseExited(MouseEvent e) {
 804             if (autoRepeatTimer.isRunning()) {
 805                 autoRepeatTimer.stop();
 806             }
 807         }
 808 
 809         /**
 810          * Requests focus on a child of the spinner if the spinner doesn't
 811          * have focus.
 812          */
 813         private void focusSpinnerIfNecessary() {
 814             Component fo = KeyboardFocusManager.
 815                               getCurrentKeyboardFocusManager().getFocusOwner();
 816             if (spinner.isRequestFocusEnabled() && (
 817                         fo == null ||
 818                         !SwingUtilities.isDescendingFrom(fo, spinner))) {
 819                 Container root = spinner;
 820 
 821                 if (!root.isFocusCycleRoot()) {
 822                     root = root.getFocusCycleRootAncestor();
 823                 }
 824                 if (root != null) {
 825                     FocusTraversalPolicy ftp = root.getFocusTraversalPolicy();
 826                     Component child = ftp.getComponentAfter(root, spinner);
 827 
 828                     if (child != null && SwingUtilities.isDescendingFrom(
 829                                                         child, spinner)) {
 830                         child.requestFocus();
 831                     }
 832                 }
 833             }
 834         }
 835 
 836         public void focusGained(FocusEvent e) {
 837         }
 838 
 839         public void focusLost(FocusEvent e) {
 840             if (spinner == eventToSpinner(e)) {
 841                 if (autoRepeatTimer.isRunning()) {
 842                     autoRepeatTimer.stop();
 843                 }
 844                 spinner = null;
 845                 if (arrowButton != null) {
 846                     ButtonModel model = arrowButton.getModel();
 847                     model.setPressed(false);
 848                     model.setArmed(false);
 849                     arrowButton = null;
 850                 }
 851             }
 852         }
 853     }
 854 
 855 
 856     private static class Handler implements LayoutManager,
 857             PropertyChangeListener, ChangeListener {
 858         //
 859         // LayoutManager
 860         //
 861         private Component nextButton = null;
 862         private Component previousButton = null;
 863         private Component editor = null;
 864 
 865         public void addLayoutComponent(String name, Component c) {
 866             if ("Next".equals(name)) {
 867                 nextButton = c;
 868             }
 869             else if ("Previous".equals(name)) {
 870                 previousButton = c;
 871             }
 872             else if ("Editor".equals(name)) {
 873                 editor = c;
 874             }
 875         }
 876 
 877         public void removeLayoutComponent(Component c) {
 878             if (c == nextButton) {
 879                 nextButton = null;
 880             }
 881             else if (c == previousButton) {
 882                 previousButton = null;
 883             }
 884             else if (c == editor) {
 885                 editor = null;
 886             }
 887         }
 888 
 889         private Dimension preferredSize(Component c) {
 890             return (c == null) ? zeroSize : c.getPreferredSize();
 891         }
 892 
 893         public Dimension preferredLayoutSize(Container parent) {
 894             Dimension nextD = preferredSize(nextButton);
 895             Dimension previousD = preferredSize(previousButton);
 896             Dimension editorD = preferredSize(editor);
 897 
 898             /* Force the editors height to be a multiple of 2
 899              */
 900             editorD.height = ((editorD.height + 1) / 2) * 2;
 901 
 902             Dimension size = new Dimension(editorD.width, editorD.height);
 903             size.width += Math.max(nextD.width, previousD.width);
 904             Insets insets = parent.getInsets();
 905             size.width += insets.left + insets.right;
 906             size.height += insets.top + insets.bottom;
 907             return size;
 908         }
 909 
 910         public Dimension minimumLayoutSize(Container parent) {
 911             return preferredLayoutSize(parent);
 912         }
 913 
 914         private void setBounds(Component c, int x, int y, int width, int height) {
 915             if (c != null) {
 916                 c.setBounds(x, y, width, height);
 917             }
 918         }
 919 
 920         public void layoutContainer(Container parent) {
 921             int width  = parent.getWidth();
 922             int height = parent.getHeight();
 923 
 924             Insets insets = parent.getInsets();
 925 
 926             if (nextButton == null && previousButton == null) {
 927                 setBounds(editor, insets.left,  insets.top, width - insets.left - insets.right,
 928                         height - insets.top - insets.bottom);
 929 
 930                 return;
 931             }
 932 
 933             Dimension nextD = preferredSize(nextButton);
 934             Dimension previousD = preferredSize(previousButton);
 935             int buttonsWidth = Math.max(nextD.width, previousD.width);
 936             int editorHeight = height - (insets.top + insets.bottom);
 937 
 938             // The arrowButtonInsets value is used instead of the JSpinner's
 939             // insets if not null. Defining this to be (0, 0, 0, 0) causes the
 940             // buttons to be aligned with the outer edge of the spinner's
 941             // border, and leaving it as "null" places the buttons completely
 942             // inside the spinner's border.
 943             Insets buttonInsets = UIManager.getInsets("Spinner.arrowButtonInsets");
 944             if (buttonInsets == null) {
 945                 buttonInsets = insets;
 946             }
 947 
 948             /* Deal with the spinner's componentOrientation property.
 949              */
 950             int editorX, editorWidth, buttonsX;
 951             if (parent.getComponentOrientation().isLeftToRight()) {
 952                 editorX = insets.left;
 953                 editorWidth = width - insets.left - buttonsWidth - buttonInsets.right;
 954                 buttonsX = width - buttonsWidth - buttonInsets.right;
 955             } else {
 956                 buttonsX = buttonInsets.left;
 957                 editorX = buttonsX + buttonsWidth;
 958                 editorWidth = width - buttonInsets.left - buttonsWidth - insets.right;
 959             }
 960 
 961             int nextY = buttonInsets.top;
 962             int nextHeight = (height / 2) + (height % 2) - nextY;
 963             int previousY = buttonInsets.top + nextHeight;
 964             int previousHeight = height - previousY - buttonInsets.bottom;
 965 
 966             setBounds(editor,         editorX,  insets.top, editorWidth, editorHeight);
 967             setBounds(nextButton,     buttonsX, nextY,      buttonsWidth, nextHeight);
 968             setBounds(previousButton, buttonsX, previousY,  buttonsWidth, previousHeight);
 969         }
 970 
 971 
 972         //
 973         // PropertyChangeListener
 974         //
 975         public void propertyChange(PropertyChangeEvent e)
 976         {
 977             String propertyName = e.getPropertyName();
 978             if (e.getSource() instanceof JSpinner) {
 979                 JSpinner spinner = (JSpinner)(e.getSource());
 980                 SpinnerUI spinnerUI = spinner.getUI();
 981 
 982                 if (spinnerUI instanceof BasicSpinnerUI) {
 983                     BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
 984 
 985                     if ("editor".equals(propertyName)) {
 986                         JComponent oldEditor = (JComponent)e.getOldValue();
 987                         JComponent newEditor = (JComponent)e.getNewValue();
 988                         ui.replaceEditor(oldEditor, newEditor);
 989                         ui.updateEnabledState();
 990                         if (oldEditor instanceof JSpinner.DefaultEditor) {
 991                             JTextField tf =
 992                                 ((JSpinner.DefaultEditor)oldEditor).getTextField();
 993                             if (tf != null) {
 994                                 tf.removeFocusListener(nextButtonHandler);
 995                                 tf.removeFocusListener(previousButtonHandler);
 996                             }
 997                         }
 998                         if (newEditor instanceof JSpinner.DefaultEditor) {
 999                             JTextField tf =
1000                                 ((JSpinner.DefaultEditor)newEditor).getTextField();
1001                             if (tf != null) {
1002                                 if (tf.getFont() instanceof UIResource) {
1003                                     Font font = spinner.getFont();
1004                                     tf.setFont(font == null ? null : new FontUIResource(font));
1005                                 }
1006                                 tf.addFocusListener(nextButtonHandler);
1007                                 tf.addFocusListener(previousButtonHandler);
1008                             }
1009                         }
1010                     }
1011                     else if ("enabled".equals(propertyName) ||
1012                              "model".equals(propertyName)) {
1013                         ui.updateEnabledState();
1014                     }
1015                     else if ("font".equals(propertyName)) {
1016                         JComponent editor = spinner.getEditor();
1017                         if (editor instanceof JSpinner.DefaultEditor) {
1018                             JTextField tf =
1019                                 ((JSpinner.DefaultEditor)editor).getTextField();
1020                             if (tf != null) {
1021                                 if (tf.getFont() instanceof UIResource) {
1022                                     Font font = spinner.getFont();
1023                                     tf.setFont(font == null ? null : new FontUIResource(font));
1024                                 }
1025                             }
1026                         }
1027                     }
1028                     else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) {
1029                         updateToolTipTextForChildren(spinner);
1030                     } else if ("componentOrientation".equals(propertyName)) {
1031                         ComponentOrientation o
1032                                 = (ComponentOrientation) e.getNewValue();
1033                         if (o != (ComponentOrientation) e.getOldValue()) {
1034                             JComponent editor = spinner.getEditor();
1035                             if (editor != null) {
1036                                 editor.applyComponentOrientation(o);
1037                             }
1038                             spinner.revalidate();
1039                             spinner.repaint();
1040                         }
1041                     }
1042                 }
1043             } else if (e.getSource() instanceof JComponent) {
1044                 JComponent c = (JComponent)e.getSource();
1045                 if ((c.getParent() instanceof JPanel) &&
1046                     (c.getParent().getParent() instanceof JSpinner) &&
1047                     "border".equals(propertyName)) {
1048 
1049                     JSpinner spinner = (JSpinner)c.getParent().getParent();
1050                     SpinnerUI spinnerUI = spinner.getUI();
1051                     if (spinnerUI instanceof BasicSpinnerUI) {
1052                         BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
1053                         ui.maybeRemoveEditorBorder(c);
1054                     }
1055                 }
1056             }
1057         }
1058 
1059         // Syncronizes the ToolTip text for the components within the spinner
1060         // to be the same value as the spinner ToolTip text.
1061         private void updateToolTipTextForChildren(JComponent spinner) {
1062             String toolTipText = spinner.getToolTipText();
1063             Component[] children = spinner.getComponents();
1064             for (int i = 0; i < children.length; i++) {
1065                 if (children[i] instanceof JSpinner.DefaultEditor) {
1066                     JTextField tf = ((JSpinner.DefaultEditor)children[i]).getTextField();
1067                     if (tf != null) {
1068                         tf.setToolTipText(toolTipText);
1069                     }
1070                 } else if (children[i] instanceof JComponent) {
1071                     ((JComponent)children[i]).setToolTipText( spinner.getToolTipText() );
1072                 }
1073             }
1074         }
1075 
1076         public void stateChanged(ChangeEvent e) {
1077             if (e.getSource() instanceof JSpinner) {
1078                 JSpinner spinner = (JSpinner)e.getSource();
1079                 SpinnerUI spinnerUI = spinner.getUI();
1080                 if (DefaultLookup.getBoolean(spinner, spinnerUI,
1081                     "Spinner.disableOnBoundaryValues", false) &&
1082                     spinnerUI instanceof BasicSpinnerUI) {
1083                     BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
1084                     ui.updateEnabledState();
1085                 }
1086             }
1087         }
1088     }
1089 }