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