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