1 /*
   2  * Copyright (c) 2011, 2019, 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             if (spinner != null && !autoRepeatTimer.isRunning() && spinner == eventToSpinner(e)) {
 500                 autoRepeatTimer.start();
 501             }
 502         }
 503 
 504         @Override
 505         public void mouseExited(final MouseEvent e) {
 506             if (autoRepeatTimer.isRunning()) {
 507                 autoRepeatTimer.stop();
 508             }
 509         }
 510 
 511         /**
 512          * Requests focus on a child of the spinner if the spinner doesn't have
 513          * focus.
 514          */
 515         private void focusSpinnerIfNecessary() {
 516             final Component fo = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
 517             if (!spinner.isRequestFocusEnabled() || (fo != null && (SwingUtilities.isDescendingFrom(fo, spinner)))) {
 518                 return;
 519             }
 520             Container root = spinner;
 521 
 522             if (!root.isFocusCycleRoot()) {
 523                 root = root.getFocusCycleRootAncestor();
 524             }
 525 
 526             if (root == null) {
 527                 return;
 528             }
 529             final FocusTraversalPolicy ftp = root.getFocusTraversalPolicy();
 530             final Component child = ftp.getComponentAfter(root, spinner);
 531 
 532             if (child != null && SwingUtilities.isDescendingFrom(child, spinner)) {
 533                 child.requestFocus();
 534             }
 535         }
 536     }
 537 
 538     @SuppressWarnings("serial") // Superclass is not serializable across versions
 539     class SpinPainter extends JComponent {
 540 
 541         final AquaPainter<JRSUIState> painter = AquaPainter.create(JRSUIStateFactory.getSpinnerArrows());
 542 
 543         ButtonModel fTopModel;
 544         ButtonModel fBottomModel;
 545 
 546         boolean fPressed = false;
 547         boolean fTopPressed = false;
 548 
 549         Dimension kPreferredSize = new Dimension(15, 24); // 19,27 before trimming
 550 
 551         public SpinPainter(final AbstractButton top, final AbstractButton bottom) {
 552             if (top != null) {
 553                 fTopModel = top.getModel();
 554             }
 555 
 556             if (bottom != null) {
 557                 fBottomModel = bottom.getModel();
 558             }
 559             setFocusable(false);
 560         }
 561 
 562         @Override
 563         public void paint(final Graphics g) {
 564             if (spinner.isOpaque()) {
 565                 g.setColor(spinner.getBackground());
 566                 g.fillRect(0, 0, getWidth(), getHeight());
 567             }
 568 
 569             AquaUtilControlSize.applySizeForControl(spinner, painter);
 570 
 571             if (isEnabled()) {
 572                 if (fTopModel != null && fTopModel.isPressed()) {
 573                     painter.state.set(State.PRESSED);
 574                     painter.state.set(BooleanValue.NO);
 575                 } else if (fBottomModel != null && fBottomModel.isPressed()) {
 576                     painter.state.set(State.PRESSED);
 577                     painter.state.set(BooleanValue.YES);
 578                 } else {
 579                     painter.state.set(State.ACTIVE);
 580                 }
 581             } else {
 582                 painter.state.set(State.DISABLED);
 583             }
 584 
 585             final Rectangle bounds = getBounds();
 586             painter.paint(g, spinner, 0, 0, bounds.width, bounds.height);
 587         }
 588 
 589         @Override
 590         public Dimension getPreferredSize() {
 591             final Size size = AquaUtilControlSize.getUserSizeFrom(this);
 592 
 593             if (size == Size.MINI) {
 594                 return new Dimension(kPreferredSize.width, kPreferredSize.height - 8);
 595             }
 596 
 597             return kPreferredSize;
 598         }
 599     }
 600 
 601     /**
 602      * A simple layout manager for the editor and the next/previous buttons. See
 603      * the AquaSpinnerUI javadoc for more information about exactly how the
 604      * components are arranged.
 605      */
 606     static class SpinnerLayout implements LayoutManager {
 607 
 608         private Component nextButton = null;
 609         private Component previousButton = null;
 610         private Component editor = null;
 611         private Component painter = null;
 612 
 613         @Override
 614         public void addLayoutComponent(final String name, final Component c) {
 615             if ("Next".equals(name)) {
 616                 nextButton = c;
 617             } else if ("Previous".equals(name)) {
 618                 previousButton = c;
 619             } else if ("Editor".equals(name)) {
 620                 editor = c;
 621             } else if ("Painter".equals(name)) {
 622                 painter = c;
 623             }
 624         }
 625 
 626         @Override
 627         public void removeLayoutComponent(Component c) {
 628             if (c == nextButton) {
 629                 c = null;
 630             } else if (c == previousButton) {
 631                 previousButton = null;
 632             } else if (c == editor) {
 633                 editor = null;
 634             } else if (c == painter) {
 635                 painter = null;
 636             }
 637         }
 638 
 639         private Dimension preferredSize(final Component c) {
 640             return (c == null) ? new Dimension(0, 0) : c.getPreferredSize();
 641         }
 642 
 643         @Override
 644         public Dimension preferredLayoutSize(final Container parent) {
 645 //            Dimension nextD = preferredSize(nextButton);
 646 //            Dimension previousD = preferredSize(previousButton);
 647             final Dimension editorD = preferredSize(editor);
 648             final Dimension painterD = preferredSize(painter);
 649 
 650             /* Force the editors height to be a multiple of 2
 651              */
 652             editorD.height = ((editorD.height + 1) / 2) * 2;
 653 
 654             final Dimension size = new Dimension(editorD.width, Math.max(painterD.height, editorD.height));
 655             size.width += painterD.width; //Math.max(nextD.width, previousD.width);
 656             final Insets insets = parent.getInsets();
 657             size.width += insets.left + insets.right;
 658             size.height += insets.top + insets.bottom;
 659             return size;
 660         }
 661 
 662         @Override
 663         public Dimension minimumLayoutSize(final Container parent) {
 664             return preferredLayoutSize(parent);
 665         }
 666 
 667         private void setBounds(final Component c, final int x, final int y, final int width, final int height) {
 668             if (c != null) {
 669                 c.setBounds(x, y, width, height);
 670             }
 671         }
 672 
 673         @Override
 674         public void layoutContainer(final Container parent) {
 675             final Insets insets = parent.getInsets();
 676             final int availWidth = parent.getWidth() - (insets.left + insets.right);
 677             final int availHeight = parent.getHeight() - (insets.top + insets.bottom);
 678 
 679             final Dimension painterD = preferredSize(painter);
 680 //            Dimension nextD = preferredSize(nextButton);
 681 //            Dimension previousD = preferredSize(previousButton);
 682             final int nextHeight = availHeight / 2;
 683             final int previousHeight = availHeight - nextHeight;
 684             final int buttonsWidth = painterD.width; //Math.max(nextD.width, previousD.width);
 685             final int editorWidth = availWidth - buttonsWidth;
 686 
 687             /* Deal with the spinners componentOrientation property.
 688              */
 689             int editorX, buttonsX;
 690             if (parent.getComponentOrientation().isLeftToRight()) {
 691                 editorX = insets.left;
 692                 buttonsX = editorX + editorWidth;
 693             } else {
 694                 buttonsX = insets.left;
 695                 editorX = buttonsX + buttonsWidth;
 696             }
 697 
 698             final int previousY = insets.top + nextHeight;
 699             final int painterTop = previousY - (painterD.height / 2);
 700             setBounds(editor, editorX, insets.top, editorWidth, availHeight);
 701             setBounds(nextButton, buttonsX, insets.top, buttonsWidth, nextHeight);
 702             setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight);
 703             setBounds(painter, buttonsX, painterTop, buttonsWidth, painterD.height);
 704         }
 705     }
 706 
 707     /**
 708      * Detect JSpinner property changes we're interested in and delegate.
 709      * Subclasses shouldn't need to replace the default propertyChangeListener
 710      * (although they can by overriding createPropertyChangeListener) since all
 711      * of the interesting property changes are delegated to protected methods.
 712      */
 713     static class PropertyChangeHandler implements PropertyChangeListener {
 714 
 715         @Override
 716         public void propertyChange(final PropertyChangeEvent e) {
 717             final String propertyName = e.getPropertyName();
 718             final JSpinner spinner = (JSpinner) (e.getSource());
 719             final SpinnerUI spinnerUI = spinner.getUI();
 720 
 721             if (spinnerUI instanceof AquaSpinnerUI) {
 722                 final AquaSpinnerUI ui = (AquaSpinnerUI) spinnerUI;
 723 
 724                 if ("editor".equals(propertyName)) {
 725                     final JComponent oldEditor = (JComponent) e.getOldValue();
 726                     final JComponent newEditor = (JComponent) e.getNewValue();
 727                     ui.replaceEditor(oldEditor, newEditor);
 728                     ui.updateEnabledState();
 729                 } else if ("componentOrientation".equals(propertyName)) {
 730                     ComponentOrientation o
 731                             = (ComponentOrientation) e.getNewValue();
 732                     if (o != e.getOldValue()) {
 733                         JComponent editor = spinner.getEditor();
 734                         if (editor != null) {
 735                             editor.applyComponentOrientation(o);
 736                         }
 737                         spinner.revalidate();
 738                         spinner.repaint();
 739                     }
 740                 } else if ("enabled".equals(propertyName)) {
 741                     ui.updateEnabledState();
 742                 } else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) {
 743                     ui.updateToolTipTextForChildren(spinner);
 744                 } else if ("font".equals(propertyName)) {
 745                     JComponent editor = spinner.getEditor();
 746                     if (editor instanceof JSpinner.DefaultEditor) {
 747                         JTextField tf
 748                                 = ((JSpinner.DefaultEditor) editor).getTextField();
 749                         if (tf != null) {
 750                             if (tf.getFont() instanceof UIResource) {
 751                                 Font font = spinner.getFont();
 752                                 tf.setFont(font == null ? null : new FontUIResource(font));
 753                             }
 754                         }
 755                     }
 756                 }
 757             }
 758         }
 759     }
 760 
 761     // Syncronizes the ToolTip text for the components within the spinner
 762     // to be the same value as the spinner ToolTip text.
 763     void updateToolTipTextForChildren(final JComponent spinnerComponent) {
 764         final String toolTipText = spinnerComponent.getToolTipText();
 765         final Component[] children = spinnerComponent.getComponents();
 766         for (final Component element : children) {
 767             if (element instanceof JSpinner.DefaultEditor) {
 768                 final JTextField tf = ((JSpinner.DefaultEditor) element).getTextField();
 769                 if (tf != null) {
 770                     tf.setToolTipText(toolTipText);
 771                 }
 772             } else if (element instanceof JComponent) {
 773                 ((JComponent) element).setToolTipText(toolTipText);
 774             }
 775         }
 776     }
 777 }