1/*
2 * Copyright (c) 2011, 2015, 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 */
25package com.apple.laf;
26
27import java.awt.*;
28import java.awt.event.*;
29import java.beans.*;
30import java.text.*;
31import java.text.AttributedCharacterIterator.Attribute;
32import java.text.Format.Field;
33import java.util.*;
34
35import javax.swing.*;
36import javax.swing.JSpinner.DefaultEditor;
37import javax.swing.plaf.*;
38import javax.swing.text.InternationalFormatter;
39
40import apple.laf.*;
41import apple.laf.JRSUIConstants.*;
42
43import com.apple.laf.AquaUtils.RecyclableSingleton;
44import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor;
45
46/**
47 * This is originally derived from BasicSpinnerUI, but they made everything
48 * private so we can't subclass!
49 */
50public class AquaSpinnerUI extends SpinnerUI {
51
52    private static final RecyclableSingleton<? extends PropertyChangeListener> propertyChangeListener
53            = new RecyclableSingletonFromDefaultConstructor<>(PropertyChangeHandler.class);
54
55    static PropertyChangeListener getPropertyChangeListener() {
56        return propertyChangeListener.get();
57    }
58
59    private static final RecyclableSingleton<ArrowButtonHandler> nextButtonHandler
60            = new RecyclableSingleton<ArrowButtonHandler>() {
61                @Override
62                protected ArrowButtonHandler getInstance() {
63                    return new ArrowButtonHandler("increment", true);
64                }
65            };
66
67    static ArrowButtonHandler getNextButtonHandler() {
68        return nextButtonHandler.get();
69    }
70    private static final RecyclableSingleton<ArrowButtonHandler> previousButtonHandler
71            = new RecyclableSingleton<ArrowButtonHandler>() {
72                @Override
73                protected ArrowButtonHandler getInstance() {
74                    return new ArrowButtonHandler("decrement", false);
75                }
76            };
77
78    static ArrowButtonHandler getPreviousButtonHandler() {
79        return previousButtonHandler.get();
80    }
81
82    private JSpinner spinner;
83    private SpinPainter spinPainter;
84    private TransparentButton next;
85    private TransparentButton prev;
86
87    public static ComponentUI createUI(final JComponent c) {
88        return new AquaSpinnerUI();
89    }
90
91    private void maybeAdd(final Component c, final String s) {
92        if (c != null) {
93            spinner.add(c, s);
94        }
95    }
96
97    boolean wasOpaque;
98
99    @Override
100    public void installUI(final JComponent c) {
101        this.spinner = (JSpinner) c;
102        installDefaults();
103        installListeners();
104        next = createNextButton();
105        prev = createPreviousButton();
106        spinPainter = new SpinPainter(next, prev);
107
108        maybeAdd(next, "Next");
109        maybeAdd(prev, "Previous");
110        maybeAdd(createEditor(), "Editor");
111        maybeAdd(spinPainter, "Painter");
112
113        updateEnabledState();
114        installKeyboardActions();
115
116        // this doesn't work because JSpinner calls setOpaque(true) directly in it's constructor
117        //    LookAndFeel.installProperty(spinner, "opaque", Boolean.FALSE);
118        // ...so we have to handle the is/was opaque ourselves
119        wasOpaque = spinner.isOpaque();
120        spinner.setOpaque(false);
121    }
122
123    @Override
124    public void uninstallUI(final JComponent c) {
125        uninstallDefaults();
126        uninstallListeners();
127        spinner.setOpaque(wasOpaque);
128        spinPainter = null;
129        spinner = null;
130        // AquaButtonUI install some listeners to all parents, which means that
131        // we need to uninstall UI here to remove those listeners, because after
132        // we remove them from spinner we lost the latest reference to them,
133        // and our standard uninstallUI machinery will not call them.
134        next.getUI().uninstallUI(next);
135        prev.getUI().uninstallUI(prev);
136        next = null;
137        prev = null;
138        c.removeAll();
139    }
140
141    protected void installListeners() {
142        spinner.addPropertyChangeListener(getPropertyChangeListener());
143    }
144
145    protected void uninstallListeners() {
146        spinner.removePropertyChangeListener(getPropertyChangeListener());
147    }
148
149    protected void installDefaults() {
150        spinner.setLayout(createLayout());
151        LookAndFeel.installBorder(spinner, "Spinner.border");
152        LookAndFeel.installColorsAndFont(spinner, "Spinner.background", "Spinner.foreground", "Spinner.font");
153    }
154
155    protected void uninstallDefaults() {
156        spinner.setLayout(null);
157    }
158
159    protected LayoutManager createLayout() {
160        return new SpinnerLayout();
161    }
162
163    protected PropertyChangeListener createPropertyChangeListener() {
164        return new PropertyChangeHandler();
165    }
166
167    protected TransparentButton createPreviousButton() {
168        final TransparentButton b = new TransparentButton();
169        b.addActionListener(getPreviousButtonHandler());
170        b.addMouseListener(getPreviousButtonHandler());
171        b.setInheritsPopupMenu(true);
172        return b;
173    }
174
175    protected TransparentButton createNextButton() {
176        final TransparentButton b = new TransparentButton();
177        b.addActionListener(getNextButtonHandler());
178        b.addMouseListener(getNextButtonHandler());
179        b.setInheritsPopupMenu(true);
180        return b;
181    }
182
183    /**
184     * {@inheritDoc}
185     */
186    @Override
187    public int getBaseline(JComponent c, int width, int height) {
188        super.getBaseline(c, width, height);
189        JComponent editor = spinner.getEditor();
190        Insets insets = spinner.getInsets();
191        width = width - insets.left - insets.right;
192        height = height - insets.top - insets.bottom;
193        if (width >= 0 && height >= 0) {
194            int baseline = editor.getBaseline(width, height);
195            if (baseline >= 0) {
196                return insets.top + baseline;
197            }
198        }
199        return -1;
200    }
201
202    /**
203     * {@inheritDoc}
204     */
205    @Override
206    public Component.BaselineResizeBehavior getBaselineResizeBehavior(
207            JComponent c) {
208        super.getBaselineResizeBehavior(c);
209        return spinner.getEditor().getBaselineResizeBehavior();
210    }
211
212    @SuppressWarnings("serial") // Superclass is not serializable across versions
213    class TransparentButton extends JButton implements SwingConstants {
214
215        boolean interceptRepaints = false;
216
217        public TransparentButton() {
218            super();
219            setFocusable(false);
220            // only intercept repaints if we are after this has been initialized
221            // otherwise we can't talk to our containing class
222            interceptRepaints = true;
223        }
224
225        @Override
226        public void paint(final Graphics g) {
227        }
228
229        @Override
230        public void repaint() {
231            // only intercept repaints if we are after this has been initialized
232            // otherwise we can't talk to our containing class
233            if (interceptRepaints) {
234                if (spinPainter == null) {
235                    return;
236                }
237                spinPainter.repaint();
238            }
239            super.repaint();
240        }
241    }
242
243    protected JComponent createEditor() {
244        final JComponent editor = spinner.getEditor();
245        fixupEditor(editor);
246        return editor;
247    }
248
249    protected void replaceEditor(final JComponent oldEditor, final JComponent newEditor) {
250        spinner.remove(oldEditor);
251        fixupEditor(newEditor);
252        spinner.add(newEditor, "Editor");
253    }
254
255    protected void fixupEditor(final JComponent editor) {
256        if (!(editor instanceof DefaultEditor)) {
257            return;
258        }
259
260        editor.setOpaque(false);
261        editor.setInheritsPopupMenu(true);
262
263        if (editor.getFont() instanceof UIResource) {
264            editor.setFont(new FontUIResource(spinner.getFont()));
265        }
266
267        final JFormattedTextField editorTextField = ((DefaultEditor) editor).getTextField();
268        if (editorTextField.getFont() instanceof UIResource) {
269            editorTextField.setFont(new FontUIResource(spinner.getFont()));
270        }
271        final InputMap spinnerInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
272        final InputMap editorInputMap = editorTextField.getInputMap();
273        final KeyStroke[] keys = spinnerInputMap.keys();
274        for (final KeyStroke k : keys) {
275            editorInputMap.put(k, spinnerInputMap.get(k));
276        }
277    }
278
279    void updateEnabledState() {
280        updateEnabledState(spinner, spinner.isEnabled());
281    }
282
283    private void updateEnabledState(final Container c, final boolean enabled) {
284        for (int counter = c.getComponentCount() - 1; counter >= 0; counter--) {
285            final Component child = c.getComponent(counter);
286
287            child.setEnabled(enabled);
288            if (child instanceof Container) {
289                updateEnabledState((Container) child, enabled);
290            }
291        }
292    }
293
294    private void installKeyboardActions() {
295        final InputMap iMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
296        SwingUtilities.replaceUIInputMap(spinner, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, iMap);
297        SwingUtilities.replaceUIActionMap(spinner, getActionMap());
298    }
299
300    private InputMap getInputMap(final int condition) {
301        if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) {
302            return (InputMap) UIManager.get("Spinner.ancestorInputMap");
303        }
304        return null;
305    }
306
307    private ActionMap getActionMap() {
308        ActionMap map = (ActionMap) UIManager.get("Spinner.actionMap");
309
310        if (map == null) {
311            map = createActionMap();
312            if (map != null) {
313                UIManager.getLookAndFeelDefaults().put("Spinner.actionMap", map);
314            }
315        }
316        return map;
317    }
318
319    private ActionMap createActionMap() {
320        final ActionMap map = new ActionMapUIResource();
321        map.put("increment", getNextButtonHandler());
322        map.put("decrement", getPreviousButtonHandler());
323        return map;
324    }
325
326    @SuppressWarnings("serial") // Superclass is not serializable across versions
327    private static class ArrowButtonHandler extends AbstractAction implements MouseListener {
328
329        final javax.swing.Timer autoRepeatTimer;
330        final boolean isNext;
331        JSpinner spinner = null;
332
333        ArrowButtonHandler(final String name, final boolean isNext) {
334            super(name);
335            this.isNext = isNext;
336            autoRepeatTimer = new javax.swing.Timer(60, this);
337            autoRepeatTimer.setInitialDelay(300);
338        }
339
340        private JSpinner eventToSpinner(final AWTEvent e) {
341            Object src = e.getSource();
342            while ((src instanceof Component) && !(src instanceof JSpinner)) {
343                src = ((Component) src).getParent();
344            }
345            return (src instanceof JSpinner) ? (JSpinner) src : null;
346        }
347
348        @Override
349        public void actionPerformed(final ActionEvent e) {
350            if (!(e.getSource() instanceof javax.swing.Timer)) {
351                // Most likely resulting from being in ActionMap.
352                spinner = eventToSpinner(e);
353            }
354
355            if (spinner == null) {
356                return;
357            }
358
359            try {
360                final int calendarField = getCalendarField(spinner);
361                spinner.commitEdit();
362                if (calendarField != -1) {
363                    ((SpinnerDateModel) spinner.getModel()).setCalendarField(calendarField);
364                }
365                final Object value = (isNext) ? spinner.getNextValue() : spinner.getPreviousValue();
366                if (value != null) {
367                    spinner.setValue(value);
368                    select(spinner);
369                }
370            } catch (final IllegalArgumentException iae) {
371                UIManager.getLookAndFeel().provideErrorFeedback(spinner);
372            } catch (final ParseException pe) {
373                UIManager.getLookAndFeel().provideErrorFeedback(spinner);
374            }
375        }
376
377        /**
378         * If the spinner's editor is a DateEditor, this selects the field
379         * associated with the value that is being incremented.
380         */
381        private void select(final JSpinner spinnerComponent) {
382            final JComponent editor = spinnerComponent.getEditor();
383            if (!(editor instanceof JSpinner.DateEditor)) {
384                return;
385            }
386
387            final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor) editor;
388            final JFormattedTextField ftf = dateEditor.getTextField();
389            final Format format = dateEditor.getFormat();
390            Object value;
391            if (format == null || (value = spinnerComponent.getValue()) == null) {
392                return;
393            }
394
395            final SpinnerDateModel model = dateEditor.getModel();
396            final DateFormat.Field field = DateFormat.Field.ofCalendarField(model.getCalendarField());
397            if (field == null) {
398                return;
399            }
400
401            try {
402                final AttributedCharacterIterator iterator = format.formatToCharacterIterator(value);
403                if (!select(ftf, iterator, field) && field == DateFormat.Field.HOUR0) {
404                    select(ftf, iterator, DateFormat.Field.HOUR1);
405                }
406            } catch (final IllegalArgumentException iae) {
407            }
408        }
409
410        /**
411         * Selects the passed in field, returning true if it is found, false
412         * otherwise.
413         */
414        private boolean select(final JFormattedTextField ftf, final AttributedCharacterIterator iterator, final DateFormat.Field field) {
415            final int max = ftf.getDocument().getLength();
416
417            iterator.first();
418            do {
419                final Map<Attribute, Object> attrs = iterator.getAttributes();
420                if (attrs == null || !attrs.containsKey(field)) {
421                    continue;
422                }
423
424                final int start = iterator.getRunStart(field);
425                final int end = iterator.getRunLimit(field);
426                if (start != -1 && end != -1 && start <= max && end <= max) {
427                    ftf.select(start, end);
428                }
429
430                return true;
431            } while (iterator.next() != CharacterIterator.DONE);
432            return false;
433        }
434
435        /**
436         * Returns the calendarField under the start of the selection, or -1 if
437         * there is no valid calendar field under the selection (or the spinner
438         * isn't editing dates.
439         */
440        private int getCalendarField(final JSpinner spinnerComponent) {
441            final JComponent editor = spinnerComponent.getEditor();
442            if (!(editor instanceof JSpinner.DateEditor)) {
443                return -1;
444            }
445
446            final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor) editor;
447            final JFormattedTextField ftf = dateEditor.getTextField();
448            final int start = ftf.getSelectionStart();
449            final JFormattedTextField.AbstractFormatter formatter = ftf.getFormatter();
450            if (!(formatter instanceof InternationalFormatter)) {
451                return -1;
452            }
453
454            final Format.Field[] fields = ((InternationalFormatter) formatter).getFields(start);
455            for (final Field element : fields) {
456                if (!(element instanceof DateFormat.Field)) {
457                    continue;
458                }
459                int calendarField;
460
461                if (element == DateFormat.Field.HOUR1) {
462                    calendarField = Calendar.HOUR;
463                } else {
464                    calendarField = ((DateFormat.Field) element).getCalendarField();
465                }
466
467                if (calendarField != -1) {
468                    return calendarField;
469                }
470            }
471            return -1;
472        }
473
474        @Override
475        public void mousePressed(final MouseEvent e) {
476            if (!SwingUtilities.isLeftMouseButton(e) || !e.getComponent().isEnabled()) {
477                return;
478            }
479            spinner = eventToSpinner(e);
480            autoRepeatTimer.start();
481
482            focusSpinnerIfNecessary();
483        }
484
485        @Override
486        public void mouseReleased(final MouseEvent e) {
487            autoRepeatTimer.stop();
488            spinner = null;
489        }
490
491        @Override
492        public void mouseClicked(final MouseEvent e) {
493        }
494
495        @Override
496        public void mouseEntered(final MouseEvent e) {
497        }
498
499        @Override
500        public void mouseExited(final MouseEvent e) {
501        }
502
503        /**
504         * Requests focus on a child of the spinner if the spinner doesn't have
505         * focus.
506         */
507        private void focusSpinnerIfNecessary() {
508            final Component fo = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
509            if (!spinner.isRequestFocusEnabled() || (fo != null && (SwingUtilities.isDescendingFrom(fo, spinner)))) {
510                return;
511            }
512            Container root = spinner;
513
514            if (!root.isFocusCycleRoot()) {
515                root = root.getFocusCycleRootAncestor();
516            }
517
518            if (root == null) {
519                return;
520            }
521            final FocusTraversalPolicy ftp = root.getFocusTraversalPolicy();
522            final Component child = ftp.getComponentAfter(root, spinner);
523
524            if (child != null && SwingUtilities.isDescendingFrom(child, spinner)) {
525                child.requestFocus();
526            }
527        }
528    }
529
530    @SuppressWarnings("serial") // Superclass is not serializable across versions
531    class SpinPainter extends JComponent {
532
533        final AquaPainter<JRSUIState> painter = AquaPainter.create(JRSUIStateFactory.getSpinnerArrows());
534
535        ButtonModel fTopModel;
536        ButtonModel fBottomModel;
537
538        boolean fPressed = false;
539        boolean fTopPressed = false;
540
541        Dimension kPreferredSize = new Dimension(15, 24); // 19,27 before trimming
542
543        public SpinPainter(final AbstractButton top, final AbstractButton bottom) {
544            if (top != null) {
545                fTopModel = top.getModel();
546            }
547
548            if (bottom != null) {
549                fBottomModel = bottom.getModel();
550            }
551        }
552
553        @Override
554        public void paint(final Graphics g) {
555            if (spinner.isOpaque()) {
556                g.setColor(spinner.getBackground());
557                g.fillRect(0, 0, getWidth(), getHeight());
558            }
559
560            AquaUtilControlSize.applySizeForControl(spinner, painter);
561
562            if (isEnabled()) {
563                if (fTopModel != null && fTopModel.isPressed()) {
564                    painter.state.set(State.PRESSED);
565                    painter.state.set(BooleanValue.NO);
566                } else if (fBottomModel != null && fBottomModel.isPressed()) {
567                    painter.state.set(State.PRESSED);
568                    painter.state.set(BooleanValue.YES);
569                } else {
570                    painter.state.set(State.ACTIVE);
571                }
572            } else {
573                painter.state.set(State.DISABLED);
574            }
575
576            final Rectangle bounds = getBounds();
577            painter.paint(g, spinner, 0, 0, bounds.width, bounds.height);
578        }
579
580        @Override
581        public Dimension getPreferredSize() {
582            final Size size = AquaUtilControlSize.getUserSizeFrom(this);
583
584            if (size == Size.MINI) {
585                return new Dimension(kPreferredSize.width, kPreferredSize.height - 8);
586            }
587
588            return kPreferredSize;
589        }
590    }
591
592    /**
593     * A simple layout manager for the editor and the next/previous buttons. See
594     * the AquaSpinnerUI javadoc for more information about exactly how the
595     * components are arranged.
596     */
597    static class SpinnerLayout implements LayoutManager {
598
599        private Component nextButton = null;
600        private Component previousButton = null;
601        private Component editor = null;
602        private Component painter = null;
603
604        @Override
605        public void addLayoutComponent(final String name, final Component c) {
606            if ("Next".equals(name)) {
607                nextButton = c;
608            } else if ("Previous".equals(name)) {
609                previousButton = c;
610            } else if ("Editor".equals(name)) {
611                editor = c;
612            } else if ("Painter".equals(name)) {
613                painter = c;
614            }
615        }
616
617        @Override
618        public void removeLayoutComponent(Component c) {
619            if (c == nextButton) {
620                c = null;
621            } else if (c == previousButton) {
622                previousButton = null;
623            } else if (c == editor) {
624                editor = null;
625            } else if (c == painter) {
626                painter = null;
627            }
628        }
629
630        private Dimension preferredSize(final Component c) {
631            return (c == null) ? new Dimension(0, 0) : c.getPreferredSize();
632        }
633
634        @Override
635        public Dimension preferredLayoutSize(final Container parent) {
636//            Dimension nextD = preferredSize(nextButton);
637//            Dimension previousD = preferredSize(previousButton);
638            final Dimension editorD = preferredSize(editor);
639            final Dimension painterD = preferredSize(painter);
640
641            /* Force the editors height to be a multiple of 2
642             */
643            editorD.height = ((editorD.height + 1) / 2) * 2;
644
645            final Dimension size = new Dimension(editorD.width, Math.max(painterD.height, editorD.height));
646            size.width += painterD.width; //Math.max(nextD.width, previousD.width);
647            final Insets insets = parent.getInsets();
648            size.width += insets.left + insets.right;
649            size.height += insets.top + insets.bottom;
650            return size;
651        }
652
653        @Override
654        public Dimension minimumLayoutSize(final Container parent) {
655            return preferredLayoutSize(parent);
656        }
657
658        private void setBounds(final Component c, final int x, final int y, final int width, final int height) {
659            if (c != null) {
660                c.setBounds(x, y, width, height);
661            }
662        }
663
664        @Override
665        public void layoutContainer(final Container parent) {
666            final Insets insets = parent.getInsets();
667            final int availWidth = parent.getWidth() - (insets.left + insets.right);
668            final int availHeight = parent.getHeight() - (insets.top + insets.bottom);
669
670            final Dimension painterD = preferredSize(painter);
671//            Dimension nextD = preferredSize(nextButton);
672//            Dimension previousD = preferredSize(previousButton);
673            final int nextHeight = availHeight / 2;
674            final int previousHeight = availHeight - nextHeight;
675            final int buttonsWidth = painterD.width; //Math.max(nextD.width, previousD.width);
676            final int editorWidth = availWidth - buttonsWidth;
677
678            /* Deal with the spinners componentOrientation property.
679             */
680            int editorX, buttonsX;
681            if (parent.getComponentOrientation().isLeftToRight()) {
682                editorX = insets.left;
683                buttonsX = editorX + editorWidth;
684            } else {
685                buttonsX = insets.left;
686                editorX = buttonsX + buttonsWidth;
687            }
688
689            final int previousY = insets.top + nextHeight;
690            final int painterTop = previousY - (painterD.height / 2);
691            setBounds(editor, editorX, insets.top, editorWidth, availHeight);
692            setBounds(nextButton, buttonsX, insets.top, buttonsWidth, nextHeight);
693            setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight);
694            setBounds(painter, buttonsX, painterTop, buttonsWidth, painterD.height);
695        }
696    }
697
698    /**
699     * Detect JSpinner property changes we're interested in and delegate.
700     * Subclasses shouldn't need to replace the default propertyChangeListener
701     * (although they can by overriding createPropertyChangeListener) since all
702     * of the interesting property changes are delegated to protected methods.
703     */
704    static class PropertyChangeHandler implements PropertyChangeListener {
705
706        @Override
707        public void propertyChange(final PropertyChangeEvent e) {
708            final String propertyName = e.getPropertyName();
709            final JSpinner spinner = (JSpinner) (e.getSource());
710            final SpinnerUI spinnerUI = spinner.getUI();
711
712            if (spinnerUI instanceof AquaSpinnerUI) {
713                final AquaSpinnerUI ui = (AquaSpinnerUI) spinnerUI;
714
715                if ("editor".equals(propertyName)) {
716                    final JComponent oldEditor = (JComponent) e.getOldValue();
717                    final JComponent newEditor = (JComponent) e.getNewValue();
718                    ui.replaceEditor(oldEditor, newEditor);
719                    ui.updateEnabledState();
720                } else if ("componentOrientation".equals(propertyName)) {
721                    ComponentOrientation o
722                            = (ComponentOrientation) e.getNewValue();
723                    if (o != e.getOldValue()) {
724                        JComponent editor = spinner.getEditor();
725                        if (editor != null) {
726                            editor.applyComponentOrientation(o);
727                        }
728                        spinner.revalidate();
729                        spinner.repaint();
730                    }
731                } else if ("enabled".equals(propertyName)) {
732                    ui.updateEnabledState();
733                } else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) {
734                    ui.updateToolTipTextForChildren(spinner);
735                } else if ("font".equals(propertyName)) {
736                    JComponent editor = spinner.getEditor();
737                    if (editor instanceof JSpinner.DefaultEditor) {
738                        JTextField tf
739                                = ((JSpinner.DefaultEditor) editor).getTextField();
740                        if (tf != null) {
741                            if (tf.getFont() instanceof UIResource) {
742                                tf.setFont(new FontUIResource(spinner.getFont()));
743                            }
744                        }
745                    }
746                }
747            }
748        }
749    }
750
751    // Syncronizes the ToolTip text for the components within the spinner
752    // to be the same value as the spinner ToolTip text.
753    void updateToolTipTextForChildren(final JComponent spinnerComponent) {
754        final String toolTipText = spinnerComponent.getToolTipText();
755        final Component[] children = spinnerComponent.getComponents();
756        for (final Component element : children) {
757            if (element instanceof JSpinner.DefaultEditor) {
758                final JTextField tf = ((JSpinner.DefaultEditor) element).getTextField();
759                if (tf != null) {
760                    tf.setToolTipText(toolTipText);
761                }
762            } else if (element instanceof JComponent) {
763                ((JComponent) element).setToolTipText(toolTipText);
764            }
765        }
766    }
767}
768