1 /*
   2  * Copyright (c) 2000, 2010, 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     private static class ArrowButtonHandler extends AbstractAction
 605                                             implements FocusListener, MouseListener, UIResource {
 606         final javax.swing.Timer autoRepeatTimer;
 607         final boolean isNext;
 608         JSpinner spinner = null;
 609         JButton arrowButton = null;
 610 
 611         ArrowButtonHandler(String name, boolean isNext) {
 612             super(name);
 613             this.isNext = isNext;
 614             autoRepeatTimer = new javax.swing.Timer(60, this);
 615             autoRepeatTimer.setInitialDelay(300);
 616         }
 617 
 618         private JSpinner eventToSpinner(AWTEvent e) {
 619             Object src = e.getSource();
 620             while ((src instanceof Component) && !(src instanceof JSpinner)) {
 621                 src = ((Component)src).getParent();
 622             }
 623             return (src instanceof JSpinner) ? (JSpinner)src : null;
 624         }
 625 
 626         public void actionPerformed(ActionEvent e) {
 627             JSpinner spinner = this.spinner;
 628 
 629             if (!(e.getSource() instanceof javax.swing.Timer)) {
 630                 // Most likely resulting from being in ActionMap.
 631                 spinner = eventToSpinner(e);
 632                 if (e.getSource() instanceof JButton) {
 633                     arrowButton = (JButton)e.getSource();
 634                 }
 635             } else {
 636                 if (arrowButton!=null && !arrowButton.getModel().isPressed()
 637                     && autoRepeatTimer.isRunning()) {
 638                     autoRepeatTimer.stop();
 639                     spinner = null;
 640                     arrowButton = null;
 641                 }
 642             }
 643             if (spinner != null) {
 644                 try {
 645                     int calendarField = getCalendarField(spinner);
 646                     spinner.commitEdit();
 647                     if (calendarField != -1) {
 648                         ((SpinnerDateModel)spinner.getModel()).
 649                                  setCalendarField(calendarField);
 650                     }
 651                     Object value = (isNext) ? spinner.getNextValue() :
 652                                spinner.getPreviousValue();
 653                     if (value != null) {
 654                         spinner.setValue(value);
 655                         select(spinner);
 656                     }
 657                 } catch (IllegalArgumentException iae) {
 658                     UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 659                 } catch (ParseException pe) {
 660                     UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 661                 }
 662             }
 663         }
 664 
 665         /**
 666          * If the spinner's editor is a DateEditor, this selects the field
 667          * associated with the value that is being incremented.
 668          */
 669         private void select(JSpinner spinner) {
 670             JComponent editor = spinner.getEditor();
 671 
 672             if (editor instanceof JSpinner.DateEditor) {
 673                 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor;
 674                 JFormattedTextField ftf = dateEditor.getTextField();
 675                 Format format = dateEditor.getFormat();
 676                 Object value;
 677 
 678                 if (format != null && (value = spinner.getValue()) != null) {
 679                     SpinnerDateModel model = dateEditor.getModel();
 680                     DateFormat.Field field = DateFormat.Field.ofCalendarField(
 681                         model.getCalendarField());
 682 
 683                     if (field != null) {
 684                         try {
 685                             AttributedCharacterIterator iterator = format.
 686                                 formatToCharacterIterator(value);
 687                             if (!select(ftf, iterator, field) &&
 688                                        field == DateFormat.Field.HOUR0) {
 689                                 select(ftf, iterator, DateFormat.Field.HOUR1);
 690                             }
 691                         }
 692                         catch (IllegalArgumentException iae) {}
 693                     }
 694                 }
 695             }
 696         }
 697 
 698         /**
 699          * Selects the passed in field, returning true if it is found,
 700          * false otherwise.
 701          */
 702         private boolean select(JFormattedTextField ftf,
 703                                AttributedCharacterIterator iterator,
 704                                DateFormat.Field field) {
 705             int max = ftf.getDocument().getLength();
 706 
 707             iterator.first();
 708             do {
 709                 Map attrs = iterator.getAttributes();
 710 
 711                 if (attrs != null && attrs.containsKey(field)){
 712                     int start = iterator.getRunStart(field);
 713                     int end = iterator.getRunLimit(field);
 714 
 715                     if (start != -1 && end != -1 && start <= max &&
 716                                        end <= max) {
 717                         ftf.select(start, end);
 718                     }
 719                     return true;
 720                 }
 721             } while (iterator.next() != CharacterIterator.DONE);
 722             return false;
 723         }
 724 
 725         /**
 726          * Returns the calendarField under the start of the selection, or
 727          * -1 if there is no valid calendar field under the selection (or
 728          * the spinner isn't editing dates.
 729          */
 730         private int getCalendarField(JSpinner spinner) {
 731             JComponent editor = spinner.getEditor();
 732 
 733             if (editor instanceof JSpinner.DateEditor) {
 734                 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor;
 735                 JFormattedTextField ftf = dateEditor.getTextField();
 736                 int start = ftf.getSelectionStart();
 737                 JFormattedTextField.AbstractFormatter formatter =
 738                                     ftf.getFormatter();
 739 
 740                 if (formatter instanceof InternationalFormatter) {
 741                     Format.Field[] fields = ((InternationalFormatter)
 742                                              formatter).getFields(start);
 743 
 744                     for (int counter = 0; counter < fields.length; counter++) {
 745                         if (fields[counter] instanceof DateFormat.Field) {
 746                             int calendarField;
 747 
 748                             if (fields[counter] == DateFormat.Field.HOUR1) {
 749                                 calendarField = Calendar.HOUR;
 750                             }
 751                             else {
 752                                 calendarField = ((DateFormat.Field)
 753                                         fields[counter]).getCalendarField();
 754                             }
 755                             if (calendarField != -1) {
 756                                 return calendarField;
 757                             }
 758                         }
 759                     }
 760                 }
 761             }
 762             return -1;
 763         }
 764 
 765         public void mousePressed(MouseEvent e) {
 766             if (SwingUtilities.isLeftMouseButton(e) && e.getComponent().isEnabled()) {
 767                 spinner = eventToSpinner(e);
 768                 autoRepeatTimer.start();
 769 
 770                 focusSpinnerIfNecessary();
 771             }
 772         }
 773 
 774         public void mouseReleased(MouseEvent e) {
 775             autoRepeatTimer.stop();
 776             arrowButton = null;
 777             spinner = null;
 778         }
 779 
 780         public void mouseClicked(MouseEvent e) {
 781         }
 782 
 783         public void mouseEntered(MouseEvent e) {
 784             if (spinner != null && !autoRepeatTimer.isRunning() && spinner == eventToSpinner(e)) {
 785                 autoRepeatTimer.start();
 786             }
 787         }
 788 
 789         public void mouseExited(MouseEvent e) {
 790             if (autoRepeatTimer.isRunning()) {
 791                 autoRepeatTimer.stop();
 792             }
 793         }
 794 
 795         /**
 796          * Requests focus on a child of the spinner if the spinner doesn't
 797          * have focus.
 798          */
 799         private void focusSpinnerIfNecessary() {
 800             Component fo = KeyboardFocusManager.
 801                               getCurrentKeyboardFocusManager().getFocusOwner();
 802             if (spinner.isRequestFocusEnabled() && (
 803                         fo == null ||
 804                         !SwingUtilities.isDescendingFrom(fo, spinner))) {
 805                 Container root = spinner;
 806 
 807                 if (!root.isFocusCycleRoot()) {
 808                     root = root.getFocusCycleRootAncestor();
 809                 }
 810                 if (root != null) {
 811                     FocusTraversalPolicy ftp = root.getFocusTraversalPolicy();
 812                     Component child = ftp.getComponentAfter(root, spinner);
 813 
 814                     if (child != null && SwingUtilities.isDescendingFrom(
 815                                                         child, spinner)) {
 816                         child.requestFocus();
 817                     }
 818                 }
 819             }
 820         }
 821 
 822         public void focusGained(FocusEvent e) {
 823         }
 824 
 825         public void focusLost(FocusEvent e) {
 826             if (spinner == eventToSpinner(e)) {
 827                 if (autoRepeatTimer.isRunning()) {
 828                     autoRepeatTimer.stop();
 829                 }
 830                 spinner = null;
 831                 if (arrowButton != null) {
 832                     ButtonModel model = arrowButton.getModel();
 833                     model.setPressed(false);
 834                     model.setArmed(false);
 835                     arrowButton = null;
 836                 }
 837             }
 838         }
 839     }
 840 
 841 
 842     private static class Handler implements LayoutManager,
 843             PropertyChangeListener, ChangeListener {
 844         //
 845         // LayoutManager
 846         //
 847         private Component nextButton = null;
 848         private Component previousButton = null;
 849         private Component editor = null;
 850 
 851         public void addLayoutComponent(String name, Component c) {
 852             if ("Next".equals(name)) {
 853                 nextButton = c;
 854             }
 855             else if ("Previous".equals(name)) {
 856                 previousButton = c;
 857             }
 858             else if ("Editor".equals(name)) {
 859                 editor = c;
 860             }
 861         }
 862 
 863         public void removeLayoutComponent(Component c) {
 864             if (c == nextButton) {
 865                 nextButton = null;
 866             }
 867             else if (c == previousButton) {
 868                 previousButton = null;
 869             }
 870             else if (c == editor) {
 871                 editor = null;
 872             }
 873         }
 874 
 875         private Dimension preferredSize(Component c) {
 876             return (c == null) ? zeroSize : c.getPreferredSize();
 877         }
 878 
 879         public Dimension preferredLayoutSize(Container parent) {
 880             Dimension nextD = preferredSize(nextButton);
 881             Dimension previousD = preferredSize(previousButton);
 882             Dimension editorD = preferredSize(editor);
 883 
 884             /* Force the editors height to be a multiple of 2
 885              */
 886             editorD.height = ((editorD.height + 1) / 2) * 2;
 887 
 888             Dimension size = new Dimension(editorD.width, editorD.height);
 889             size.width += Math.max(nextD.width, previousD.width);
 890             Insets insets = parent.getInsets();
 891             size.width += insets.left + insets.right;
 892             size.height += insets.top + insets.bottom;
 893             return size;
 894         }
 895 
 896         public Dimension minimumLayoutSize(Container parent) {
 897             return preferredLayoutSize(parent);
 898         }
 899 
 900         private void setBounds(Component c, int x, int y, int width, int height) {
 901             if (c != null) {
 902                 c.setBounds(x, y, width, height);
 903             }
 904         }
 905 
 906         public void layoutContainer(Container parent) {
 907             int width  = parent.getWidth();
 908             int height = parent.getHeight();
 909 
 910             Insets insets = parent.getInsets();
 911 
 912             if (nextButton == null && previousButton == null) {
 913                 setBounds(editor, insets.left,  insets.top, width - insets.left - insets.right,
 914                         height - insets.top - insets.bottom);
 915 
 916                 return;
 917             }
 918 
 919             Dimension nextD = preferredSize(nextButton);
 920             Dimension previousD = preferredSize(previousButton);
 921             int buttonsWidth = Math.max(nextD.width, previousD.width);
 922             int editorHeight = height - (insets.top + insets.bottom);
 923 
 924             // The arrowButtonInsets value is used instead of the JSpinner's
 925             // insets if not null. Defining this to be (0, 0, 0, 0) causes the
 926             // buttons to be aligned with the outer edge of the spinner's
 927             // border, and leaving it as "null" places the buttons completely
 928             // inside the spinner's border.
 929             Insets buttonInsets = UIManager.getInsets("Spinner.arrowButtonInsets");
 930             if (buttonInsets == null) {
 931                 buttonInsets = insets;
 932             }
 933 
 934             /* Deal with the spinner's componentOrientation property.
 935              */
 936             int editorX, editorWidth, buttonsX;
 937             if (parent.getComponentOrientation().isLeftToRight()) {
 938                 editorX = insets.left;
 939                 editorWidth = width - insets.left - buttonsWidth - buttonInsets.right;
 940                 buttonsX = width - buttonsWidth - buttonInsets.right;
 941             } else {
 942                 buttonsX = buttonInsets.left;
 943                 editorX = buttonsX + buttonsWidth;
 944                 editorWidth = width - buttonInsets.left - buttonsWidth - insets.right;
 945             }
 946 
 947             int nextY = buttonInsets.top;
 948             int nextHeight = (height / 2) + (height % 2) - nextY;
 949             int previousY = buttonInsets.top + nextHeight;
 950             int previousHeight = height - previousY - buttonInsets.bottom;
 951 
 952             setBounds(editor,         editorX,  insets.top, editorWidth, editorHeight);
 953             setBounds(nextButton,     buttonsX, nextY,      buttonsWidth, nextHeight);
 954             setBounds(previousButton, buttonsX, previousY,  buttonsWidth, previousHeight);
 955         }
 956 
 957 
 958         //
 959         // PropertyChangeListener
 960         //
 961         public void propertyChange(PropertyChangeEvent e)
 962         {
 963             String propertyName = e.getPropertyName();
 964             if (e.getSource() instanceof JSpinner) {
 965                 JSpinner spinner = (JSpinner)(e.getSource());
 966                 SpinnerUI spinnerUI = spinner.getUI();
 967 
 968                 if (spinnerUI instanceof BasicSpinnerUI) {
 969                     BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
 970 
 971                     if ("editor".equals(propertyName)) {
 972                         JComponent oldEditor = (JComponent)e.getOldValue();
 973                         JComponent newEditor = (JComponent)e.getNewValue();
 974                         ui.replaceEditor(oldEditor, newEditor);
 975                         ui.updateEnabledState();
 976                         if (oldEditor instanceof JSpinner.DefaultEditor) {
 977                             JTextField tf =
 978                                 ((JSpinner.DefaultEditor)oldEditor).getTextField();
 979                             if (tf != null) {
 980                                 tf.removeFocusListener(nextButtonHandler);
 981                                 tf.removeFocusListener(previousButtonHandler);
 982                             }
 983                         }
 984                         if (newEditor instanceof JSpinner.DefaultEditor) {
 985                             JTextField tf =
 986                                 ((JSpinner.DefaultEditor)newEditor).getTextField();
 987                             if (tf != null) {
 988                                 if (tf.getFont() instanceof UIResource) {
 989                                     tf.setFont(spinner.getFont());
 990                                 }
 991                                 tf.addFocusListener(nextButtonHandler);
 992                                 tf.addFocusListener(previousButtonHandler);
 993                             }
 994                         }
 995                     }
 996                     else if ("enabled".equals(propertyName) ||
 997                              "model".equals(propertyName)) {
 998                         ui.updateEnabledState();
 999                     }
1000                 else if ("font".equals(propertyName)) {
1001                     JComponent editor = spinner.getEditor();
1002                     if (editor!=null && editor instanceof JSpinner.DefaultEditor) {
1003                         JTextField tf =
1004                             ((JSpinner.DefaultEditor)editor).getTextField();
1005                         if (tf != null) {
1006                             if (tf.getFont() instanceof UIResource) {
1007                                 tf.setFont(spinner.getFont());
1008                             }
1009                         }
1010                     }
1011                 }
1012                 else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) {
1013                     updateToolTipTextForChildren(spinner);
1014                 }
1015                 }
1016             } else if (e.getSource() instanceof JComponent) {
1017                 JComponent c = (JComponent)e.getSource();
1018                 if ((c.getParent() instanceof JPanel) &&
1019                     (c.getParent().getParent() instanceof JSpinner) &&
1020                     "border".equals(propertyName)) {
1021 
1022                     JSpinner spinner = (JSpinner)c.getParent().getParent();
1023                     SpinnerUI spinnerUI = spinner.getUI();
1024                     if (spinnerUI instanceof BasicSpinnerUI) {
1025                         BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
1026                         ui.maybeRemoveEditorBorder(c);
1027                     }
1028                 }
1029             }
1030         }
1031 
1032         // Syncronizes the ToolTip text for the components within the spinner
1033         // to be the same value as the spinner ToolTip text.
1034         private void updateToolTipTextForChildren(JComponent spinner) {
1035             String toolTipText = spinner.getToolTipText();
1036             Component[] children = spinner.getComponents();
1037             for (int i = 0; i < children.length; i++) {
1038                 if (children[i] instanceof JSpinner.DefaultEditor) {
1039                     JTextField tf = ((JSpinner.DefaultEditor)children[i]).getTextField();
1040                     if (tf != null) {
1041                         tf.setToolTipText(toolTipText);
1042                     }
1043                 } else if (children[i] instanceof JComponent) {
1044                     ((JComponent)children[i]).setToolTipText( spinner.getToolTipText() );
1045                 }
1046             }
1047         }
1048 
1049         public void stateChanged(ChangeEvent e) {
1050             if (e.getSource() instanceof JSpinner) {
1051                 JSpinner spinner = (JSpinner)e.getSource();
1052                 SpinnerUI spinnerUI = spinner.getUI();
1053                 if (DefaultLookup.getBoolean(spinner, spinnerUI,
1054                     "Spinner.disableOnBoundaryValues", false) &&
1055                     spinnerUI instanceof BasicSpinnerUI) {
1056                     BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI;
1057                     ui.updateEnabledState();
1058                 }
1059             }
1060         }
1061     }
1062 }