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