1 /*
   2  * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package com.apple.laf;
  26 
  27 import java.awt.*;
  28 import java.awt.event.*;
  29 import java.beans.*;
  30 import java.text.*;
  31 import java.text.AttributedCharacterIterator.Attribute;
  32 import java.text.Format.Field;
  33 import java.util.*;
  34 
  35 import javax.swing.*;
  36 import javax.swing.JSpinner.DefaultEditor;
  37 import javax.swing.plaf.*;
  38 import javax.swing.text.InternationalFormatter;
  39 
  40 import apple.laf.*;
  41 import apple.laf.JRSUIConstants.*;
  42 
  43 import com.apple.laf.AquaUtils.RecyclableSingleton;
  44 import 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  */
  50 public 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             Font font = spinner.getFont();
 265             editor.setFont(font == null ? null : new FontUIResource(font));
 266         }
 267 
 268         final JFormattedTextField editorTextField = ((DefaultEditor) editor).getTextField();
 269         if (editorTextField.getFont() instanceof UIResource) {
 270             Font font = spinner.getFont();
 271             editorTextField.setFont(font == null ? null : new FontUIResource(font));
 272         }
 273         final InputMap spinnerInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 274         final InputMap editorInputMap = editorTextField.getInputMap();
 275         final KeyStroke[] keys = spinnerInputMap.keys();
 276         for (final KeyStroke k : keys) {
 277             editorInputMap.put(k, spinnerInputMap.get(k));
 278         }
 279     }
 280 
 281     void updateEnabledState() {
 282         updateEnabledState(spinner, spinner.isEnabled());
 283     }
 284 
 285     private void updateEnabledState(final Container c, final boolean enabled) {
 286         for (int counter = c.getComponentCount() - 1; counter >= 0; counter--) {
 287             final Component child = c.getComponent(counter);
 288 
 289             child.setEnabled(enabled);
 290             if (child instanceof Container) {
 291                 updateEnabledState((Container) child, enabled);
 292             }
 293         }
 294     }
 295 
 296     private void installKeyboardActions() {
 297         final InputMap iMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
 298         SwingUtilities.replaceUIInputMap(spinner, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, iMap);
 299         SwingUtilities.replaceUIActionMap(spinner, getActionMap());
 300     }
 301 
 302     private InputMap getInputMap(final int condition) {
 303         if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) {
 304             return (InputMap) UIManager.get("Spinner.ancestorInputMap");
 305         }
 306         return null;
 307     }
 308 
 309     private ActionMap getActionMap() {
 310         ActionMap map = (ActionMap) UIManager.get("Spinner.actionMap");
 311 
 312         if (map == null) {
 313             map = createActionMap();
 314             if (map != null) {
 315                 UIManager.getLookAndFeelDefaults().put("Spinner.actionMap", map);
 316             }
 317         }
 318         return map;
 319     }
 320 
 321     private ActionMap createActionMap() {
 322         final ActionMap map = new ActionMapUIResource();
 323         map.put("increment", getNextButtonHandler());
 324         map.put("decrement", getPreviousButtonHandler());
 325         return map;
 326     }
 327 
 328     @SuppressWarnings("serial") // Superclass is not serializable across versions
 329     private static class ArrowButtonHandler extends AbstractAction implements MouseListener {
 330 
 331         final javax.swing.Timer autoRepeatTimer;
 332         final boolean isNext;
 333         JSpinner spinner = null;
 334 
 335         ArrowButtonHandler(final String name, final boolean isNext) {
 336             super(name);
 337             this.isNext = isNext;
 338             autoRepeatTimer = new javax.swing.Timer(60, this);
 339             autoRepeatTimer.setInitialDelay(300);
 340         }
 341 
 342         private JSpinner eventToSpinner(final AWTEvent e) {
 343             Object src = e.getSource();
 344             while ((src instanceof Component) && !(src instanceof JSpinner)) {
 345                 src = ((Component) src).getParent();
 346             }
 347             return (src instanceof JSpinner) ? (JSpinner) src : null;
 348         }
 349 
 350         @Override
 351         public void actionPerformed(final ActionEvent e) {
 352             if (!(e.getSource() instanceof javax.swing.Timer)) {
 353                 // Most likely resulting from being in ActionMap.
 354                 spinner = eventToSpinner(e);
 355             }
 356 
 357             if (spinner == null) {
 358                 return;
 359             }
 360 
 361             try {
 362                 final int calendarField = getCalendarField(spinner);
 363                 spinner.commitEdit();
 364                 if (calendarField != -1) {
 365                     ((SpinnerDateModel) spinner.getModel()).setCalendarField(calendarField);
 366                 }
 367                 final Object value = (isNext) ? spinner.getNextValue() : spinner.getPreviousValue();
 368                 if (value != null) {
 369                     spinner.setValue(value);
 370                     select(spinner);
 371                 }
 372             } catch (final IllegalArgumentException iae) {
 373                 UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 374             } catch (final ParseException pe) {
 375                 UIManager.getLookAndFeel().provideErrorFeedback(spinner);
 376             }
 377         }
 378 
 379         /**
 380          * If the spinner's editor is a DateEditor, this selects the field
 381          * associated with the value that is being incremented.
 382          */
 383         private void select(final JSpinner spinnerComponent) {
 384             final JComponent editor = spinnerComponent.getEditor();
 385             if (!(editor instanceof JSpinner.DateEditor)) {
 386                 return;
 387             }
 388 
 389             final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor) editor;
 390             final JFormattedTextField ftf = dateEditor.getTextField();
 391             final Format format = dateEditor.getFormat();
 392             Object value;
 393             if (format == null || (value = spinnerComponent.getValue()) == null) {
 394                 return;
 395             }
 396 
 397             final SpinnerDateModel model = dateEditor.getModel();
 398             final DateFormat.Field field = DateFormat.Field.ofCalendarField(model.getCalendarField());
 399             if (field == null) {
 400                 return;
 401             }
 402 
 403             try {
 404                 final AttributedCharacterIterator iterator = format.formatToCharacterIterator(value);
 405                 if (!select(ftf, iterator, field) && field == DateFormat.Field.HOUR0) {
 406                     select(ftf, iterator, DateFormat.Field.HOUR1);
 407                 }
 408             } catch (final IllegalArgumentException iae) {
 409             }
 410         }
 411 
 412         /**
 413          * Selects the passed in field, returning true if it is found, false
 414          * otherwise.
 415          */
 416         private boolean select(final JFormattedTextField ftf, final AttributedCharacterIterator iterator, final DateFormat.Field field) {
 417             final int max = ftf.getDocument().getLength();
 418 
 419             iterator.first();
 420             do {
 421                 final Map<Attribute, Object> attrs = iterator.getAttributes();
 422                 if (attrs == null || !attrs.containsKey(field)) {
 423                     continue;
 424                 }
 425 
 426                 final int start = iterator.getRunStart(field);
 427                 final int end = iterator.getRunLimit(field);
 428                 if (start != -1 && end != -1 && start <= max && end <= max) {
 429                     ftf.select(start, end);
 430                 }
 431 
 432                 return true;
 433             } while (iterator.next() != CharacterIterator.DONE);
 434             return false;
 435         }
 436 
 437         /**
 438          * Returns the calendarField under the start of the selection, or -1 if
 439          * there is no valid calendar field under the selection (or the spinner
 440          * isn't editing dates.
 441          */
 442         private int getCalendarField(final JSpinner spinnerComponent) {
 443             final JComponent editor = spinnerComponent.getEditor();
 444             if (!(editor instanceof JSpinner.DateEditor)) {
 445                 return -1;
 446             }
 447 
 448             final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor) editor;
 449             final JFormattedTextField ftf = dateEditor.getTextField();
 450             final int start = ftf.getSelectionStart();
 451             final JFormattedTextField.AbstractFormatter formatter = ftf.getFormatter();
 452             if (!(formatter instanceof InternationalFormatter)) {
 453                 return -1;
 454             }
 455 
 456             final Format.Field[] fields = ((InternationalFormatter) formatter).getFields(start);
 457             for (final Field element : fields) {
 458                 if (!(element instanceof DateFormat.Field)) {
 459                     continue;
 460                 }
 461                 int calendarField;
 462 
 463                 if (element == DateFormat.Field.HOUR1) {
 464                     calendarField = Calendar.HOUR;
 465                 } else {
 466                     calendarField = ((DateFormat.Field) element).getCalendarField();
 467                 }
 468 
 469                 if (calendarField != -1) {
 470                     return calendarField;
 471                 }
 472             }
 473             return -1;
 474         }
 475 
 476         @Override
 477         public void mousePressed(final MouseEvent e) {
 478             if (!SwingUtilities.isLeftMouseButton(e) || !e.getComponent().isEnabled()) {
 479                 return;
 480             }
 481             spinner = eventToSpinner(e);
 482             autoRepeatTimer.start();
 483 
 484             focusSpinnerIfNecessary();
 485         }
 486 
 487         @Override
 488         public void mouseReleased(final MouseEvent e) {
 489             autoRepeatTimer.stop();
 490             spinner = null;
 491         }
 492 
 493         @Override
 494         public void mouseClicked(final MouseEvent e) {
 495         }
 496 
 497         @Override
 498         public void mouseEntered(final MouseEvent e) {
 499         }
 500 
 501         @Override
 502         public void mouseExited(final MouseEvent e) {
 503         }
 504 
 505         /**
 506          * Requests focus on a child of the spinner if the spinner doesn't have
 507          * focus.
 508          */
 509         private void focusSpinnerIfNecessary() {
 510             final Component fo = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
 511             if (!spinner.isRequestFocusEnabled() || (fo != null && (SwingUtilities.isDescendingFrom(fo, spinner)))) {
 512                 return;
 513             }
 514             Container root = spinner;
 515 
 516             if (!root.isFocusCycleRoot()) {
 517                 root = root.getFocusCycleRootAncestor();
 518             }
 519 
 520             if (root == null) {
 521                 return;
 522             }
 523             final FocusTraversalPolicy ftp = root.getFocusTraversalPolicy();
 524             final Component child = ftp.getComponentAfter(root, spinner);
 525 
 526             if (child != null && SwingUtilities.isDescendingFrom(child, spinner)) {
 527                 child.requestFocus();
 528             }
 529         }
 530     }
 531 
 532     @SuppressWarnings("serial") // Superclass is not serializable across versions
 533     class SpinPainter extends JComponent {
 534 
 535         final AquaPainter<JRSUIState> painter = AquaPainter.create(JRSUIStateFactory.getSpinnerArrows());
 536 
 537         ButtonModel fTopModel;
 538         ButtonModel fBottomModel;
 539 
 540         boolean fPressed = false;
 541         boolean fTopPressed = false;
 542 
 543         Dimension kPreferredSize = new Dimension(15, 24); // 19,27 before trimming
 544 
 545         public SpinPainter(final AbstractButton top, final AbstractButton bottom) {
 546             if (top != null) {
 547                 fTopModel = top.getModel();
 548             }
 549 
 550             if (bottom != null) {
 551                 fBottomModel = bottom.getModel();
 552             }
 553         }
 554 
 555         @Override
 556         public void paint(final Graphics g) {
 557             if (spinner.isOpaque()) {
 558                 g.setColor(spinner.getBackground());
 559                 g.fillRect(0, 0, getWidth(), getHeight());
 560             }
 561 
 562             AquaUtilControlSize.applySizeForControl(spinner, painter);
 563 
 564             if (isEnabled()) {
 565                 if (fTopModel != null && fTopModel.isPressed()) {
 566                     painter.state.set(State.PRESSED);
 567                     painter.state.set(BooleanValue.NO);
 568                 } else if (fBottomModel != null && fBottomModel.isPressed()) {
 569                     painter.state.set(State.PRESSED);
 570                     painter.state.set(BooleanValue.YES);
 571                 } else {
 572                     painter.state.set(State.ACTIVE);
 573                 }
 574             } else {
 575                 painter.state.set(State.DISABLED);
 576             }
 577 
 578             final Rectangle bounds = getBounds();
 579             painter.paint(g, spinner, 0, 0, bounds.width, bounds.height);
 580         }
 581 
 582         @Override
 583         public Dimension getPreferredSize() {
 584             final Size size = AquaUtilControlSize.getUserSizeFrom(this);
 585 
 586             if (size == Size.MINI) {
 587                 return new Dimension(kPreferredSize.width, kPreferredSize.height - 8);
 588             }
 589 
 590             return kPreferredSize;
 591         }
 592     }
 593 
 594     /**
 595      * A simple layout manager for the editor and the next/previous buttons. See
 596      * the AquaSpinnerUI javadoc for more information about exactly how the
 597      * components are arranged.
 598      */
 599     static class SpinnerLayout implements LayoutManager {
 600 
 601         private Component nextButton = null;
 602         private Component previousButton = null;
 603         private Component editor = null;
 604         private Component painter = null;
 605 
 606         @Override
 607         public void addLayoutComponent(final String name, final Component c) {
 608             if ("Next".equals(name)) {
 609                 nextButton = c;
 610             } else if ("Previous".equals(name)) {
 611                 previousButton = c;
 612             } else if ("Editor".equals(name)) {
 613                 editor = c;
 614             } else if ("Painter".equals(name)) {
 615                 painter = c;
 616             }
 617         }
 618 
 619         @Override
 620         public void removeLayoutComponent(Component c) {
 621             if (c == nextButton) {
 622                 c = null;
 623             } else if (c == previousButton) {
 624                 previousButton = null;
 625             } else if (c == editor) {
 626                 editor = null;
 627             } else if (c == painter) {
 628                 painter = null;
 629             }
 630         }
 631 
 632         private Dimension preferredSize(final Component c) {
 633             return (c == null) ? new Dimension(0, 0) : c.getPreferredSize();
 634         }
 635 
 636         @Override
 637         public Dimension preferredLayoutSize(final Container parent) {
 638 //            Dimension nextD = preferredSize(nextButton);
 639 //            Dimension previousD = preferredSize(previousButton);
 640             final Dimension editorD = preferredSize(editor);
 641             final Dimension painterD = preferredSize(painter);
 642 
 643             /* Force the editors height to be a multiple of 2
 644              */
 645             editorD.height = ((editorD.height + 1) / 2) * 2;
 646 
 647             final Dimension size = new Dimension(editorD.width, Math.max(painterD.height, editorD.height));
 648             size.width += painterD.width; //Math.max(nextD.width, previousD.width);
 649             final Insets insets = parent.getInsets();
 650             size.width += insets.left + insets.right;
 651             size.height += insets.top + insets.bottom;
 652             return size;
 653         }
 654 
 655         @Override
 656         public Dimension minimumLayoutSize(final Container parent) {
 657             return preferredLayoutSize(parent);
 658         }
 659 
 660         private void setBounds(final Component c, final int x, final int y, final int width, final int height) {
 661             if (c != null) {
 662                 c.setBounds(x, y, width, height);
 663             }
 664         }
 665 
 666         @Override
 667         public void layoutContainer(final Container parent) {
 668             final Insets insets = parent.getInsets();
 669             final int availWidth = parent.getWidth() - (insets.left + insets.right);
 670             final int availHeight = parent.getHeight() - (insets.top + insets.bottom);
 671 
 672             final Dimension painterD = preferredSize(painter);
 673 //            Dimension nextD = preferredSize(nextButton);
 674 //            Dimension previousD = preferredSize(previousButton);
 675             final int nextHeight = availHeight / 2;
 676             final int previousHeight = availHeight - nextHeight;
 677             final int buttonsWidth = painterD.width; //Math.max(nextD.width, previousD.width);
 678             final int editorWidth = availWidth - buttonsWidth;
 679 
 680             /* Deal with the spinners componentOrientation property.
 681              */
 682             int editorX, buttonsX;
 683             if (parent.getComponentOrientation().isLeftToRight()) {
 684                 editorX = insets.left;
 685                 buttonsX = editorX + editorWidth;
 686             } else {
 687                 buttonsX = insets.left;
 688                 editorX = buttonsX + buttonsWidth;
 689             }
 690 
 691             final int previousY = insets.top + nextHeight;
 692             final int painterTop = previousY - (painterD.height / 2);
 693             setBounds(editor, editorX, insets.top, editorWidth, availHeight);
 694             setBounds(nextButton, buttonsX, insets.top, buttonsWidth, nextHeight);
 695             setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight);
 696             setBounds(painter, buttonsX, painterTop, buttonsWidth, painterD.height);
 697         }
 698     }
 699 
 700     /**
 701      * Detect JSpinner property changes we're interested in and delegate.
 702      * Subclasses shouldn't need to replace the default propertyChangeListener
 703      * (although they can by overriding createPropertyChangeListener) since all
 704      * of the interesting property changes are delegated to protected methods.
 705      */
 706     static class PropertyChangeHandler implements PropertyChangeListener {
 707 
 708         @Override
 709         public void propertyChange(final PropertyChangeEvent e) {
 710             final String propertyName = e.getPropertyName();
 711             final JSpinner spinner = (JSpinner) (e.getSource());
 712             final SpinnerUI spinnerUI = spinner.getUI();
 713 
 714             if (spinnerUI instanceof AquaSpinnerUI) {
 715                 final AquaSpinnerUI ui = (AquaSpinnerUI) spinnerUI;
 716 
 717                 if ("editor".equals(propertyName)) {
 718                     final JComponent oldEditor = (JComponent) e.getOldValue();
 719                     final JComponent newEditor = (JComponent) e.getNewValue();
 720                     ui.replaceEditor(oldEditor, newEditor);
 721                     ui.updateEnabledState();
 722                 } else if ("componentOrientation".equals(propertyName)) {
 723                     ComponentOrientation o
 724                             = (ComponentOrientation) e.getNewValue();
 725                     if (o != e.getOldValue()) {
 726                         JComponent editor = spinner.getEditor();
 727                         if (editor != null) {
 728                             editor.applyComponentOrientation(o);
 729                         }
 730                         spinner.revalidate();
 731                         spinner.repaint();
 732                     }
 733                 } else if ("enabled".equals(propertyName)) {
 734                     ui.updateEnabledState();
 735                 } else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) {
 736                     ui.updateToolTipTextForChildren(spinner);
 737                 } else if ("font".equals(propertyName)) {
 738                     JComponent editor = spinner.getEditor();
 739                     if (editor instanceof JSpinner.DefaultEditor) {
 740                         JTextField tf
 741                                 = ((JSpinner.DefaultEditor) editor).getTextField();
 742                         if (tf != null) {
 743                             if (tf.getFont() instanceof UIResource) {
 744                                 Font font = spinner.getFont();
 745                                 tf.setFont(font == null ? null : new FontUIResource(font));
 746                             }
 747                         }
 748                     }
 749                 }
 750             }
 751         }
 752     }
 753 
 754     // Syncronizes the ToolTip text for the components within the spinner
 755     // to be the same value as the spinner ToolTip text.
 756     void updateToolTipTextForChildren(final JComponent spinnerComponent) {
 757         final String toolTipText = spinnerComponent.getToolTipText();
 758         final Component[] children = spinnerComponent.getComponents();
 759         for (final Component element : children) {
 760             if (element instanceof JSpinner.DefaultEditor) {
 761                 final JTextField tf = ((JSpinner.DefaultEditor) element).getTextField();
 762                 if (tf != null) {
 763                     tf.setToolTipText(toolTipText);
 764                 }
 765             } else if (element instanceof JComponent) {
 766                 ((JComponent) element).setToolTipText(toolTipText);
 767             }
 768         }
 769     }
 770 }