/*
 * Copyright (c) 2011, 2019, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.apple.laf;

import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.text.*;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.Format.Field;
import java.util.*;

import javax.swing.*;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.plaf.*;
import javax.swing.text.InternationalFormatter;

import apple.laf.*;
import apple.laf.JRSUIConstants.*;

import com.apple.laf.AquaUtils.RecyclableSingleton;
import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor;

This is originally derived from BasicSpinnerUI, but they made everything private so we can't subclass!
/** * This is originally derived from BasicSpinnerUI, but they made everything * private so we can't subclass! */
public class AquaSpinnerUI extends SpinnerUI { private static final RecyclableSingleton<? extends PropertyChangeListener> propertyChangeListener = new RecyclableSingletonFromDefaultConstructor<>(PropertyChangeHandler.class); static PropertyChangeListener getPropertyChangeListener() { return propertyChangeListener.get(); } private static final RecyclableSingleton<ArrowButtonHandler> nextButtonHandler = new RecyclableSingleton<ArrowButtonHandler>() { @Override protected ArrowButtonHandler getInstance() { return new ArrowButtonHandler("increment", true); } }; static ArrowButtonHandler getNextButtonHandler() { return nextButtonHandler.get(); } private static final RecyclableSingleton<ArrowButtonHandler> previousButtonHandler = new RecyclableSingleton<ArrowButtonHandler>() { @Override protected ArrowButtonHandler getInstance() { return new ArrowButtonHandler("decrement", false); } }; static ArrowButtonHandler getPreviousButtonHandler() { return previousButtonHandler.get(); } private JSpinner spinner; private SpinPainter spinPainter; private TransparentButton next; private TransparentButton prev; public static ComponentUI createUI(final JComponent c) { return new AquaSpinnerUI(); } private void maybeAdd(final Component c, final String s) { if (c != null) { spinner.add(c, s); } } boolean wasOpaque; @Override public void installUI(final JComponent c) { this.spinner = (JSpinner) c; installDefaults(); installListeners(); next = createNextButton(); prev = createPreviousButton(); spinPainter = new SpinPainter(next, prev); maybeAdd(next, "Next"); maybeAdd(prev, "Previous"); maybeAdd(createEditor(), "Editor"); maybeAdd(spinPainter, "Painter"); updateEnabledState(); installKeyboardActions(); // this doesn't work because JSpinner calls setOpaque(true) directly in it's constructor // LookAndFeel.installProperty(spinner, "opaque", Boolean.FALSE); // ...so we have to handle the is/was opaque ourselves wasOpaque = spinner.isOpaque(); spinner.setOpaque(false); } @Override public void uninstallUI(final JComponent c) { uninstallDefaults(); uninstallListeners(); spinner.setOpaque(wasOpaque); spinPainter = null; spinner = null; // AquaButtonUI install some listeners to all parents, which means that // we need to uninstall UI here to remove those listeners, because after // we remove them from spinner we lost the latest reference to them, // and our standard uninstallUI machinery will not call them. next.getUI().uninstallUI(next); prev.getUI().uninstallUI(prev); next = null; prev = null; c.removeAll(); } protected void installListeners() { spinner.addPropertyChangeListener(getPropertyChangeListener()); } protected void uninstallListeners() { spinner.removePropertyChangeListener(getPropertyChangeListener()); } protected void installDefaults() { spinner.setLayout(createLayout()); LookAndFeel.installBorder(spinner, "Spinner.border"); LookAndFeel.installColorsAndFont(spinner, "Spinner.background", "Spinner.foreground", "Spinner.font"); } protected void uninstallDefaults() { spinner.setLayout(null); } protected LayoutManager createLayout() { return new SpinnerLayout(); } protected PropertyChangeListener createPropertyChangeListener() { return new PropertyChangeHandler(); } protected TransparentButton createPreviousButton() { final TransparentButton b = new TransparentButton(); b.addActionListener(getPreviousButtonHandler()); b.addMouseListener(getPreviousButtonHandler()); b.setInheritsPopupMenu(true); return b; } protected TransparentButton createNextButton() { final TransparentButton b = new TransparentButton(); b.addActionListener(getNextButtonHandler()); b.addMouseListener(getNextButtonHandler()); b.setInheritsPopupMenu(true); return b; }
{@inheritDoc}
/** * {@inheritDoc} */
@Override public int getBaseline(JComponent c, int width, int height) { super.getBaseline(c, width, height); JComponent editor = spinner.getEditor(); Insets insets = spinner.getInsets(); width = width - insets.left - insets.right; height = height - insets.top - insets.bottom; if (width >= 0 && height >= 0) { int baseline = editor.getBaseline(width, height); if (baseline >= 0) { return insets.top + baseline; } } return -1; }
{@inheritDoc}
/** * {@inheritDoc} */
@Override public Component.BaselineResizeBehavior getBaselineResizeBehavior( JComponent c) { super.getBaselineResizeBehavior(c); return spinner.getEditor().getBaselineResizeBehavior(); } @SuppressWarnings("serial") // Superclass is not serializable across versions class TransparentButton extends JButton implements SwingConstants { boolean interceptRepaints = false; public TransparentButton() { super(); setFocusable(false); // only intercept repaints if we are after this has been initialized // otherwise we can't talk to our containing class interceptRepaints = true; } @Override public void paint(final Graphics g) { } @Override public void repaint() { // only intercept repaints if we are after this has been initialized // otherwise we can't talk to our containing class if (interceptRepaints) { if (spinPainter == null) { return; } spinPainter.repaint(); } super.repaint(); } } protected JComponent createEditor() { final JComponent editor = spinner.getEditor(); fixupEditor(editor); return editor; } protected void replaceEditor(final JComponent oldEditor, final JComponent newEditor) { spinner.remove(oldEditor); fixupEditor(newEditor); spinner.add(newEditor, "Editor"); } protected void fixupEditor(final JComponent editor) { if (!(editor instanceof DefaultEditor)) { return; } editor.setOpaque(false); editor.setInheritsPopupMenu(true); if (editor.getFont() instanceof UIResource) { Font font = spinner.getFont(); editor.setFont(font == null ? null : new FontUIResource(font)); } final JFormattedTextField editorTextField = ((DefaultEditor) editor).getTextField(); if (editorTextField.getFont() instanceof UIResource) { Font font = spinner.getFont(); editorTextField.setFont(font == null ? null : new FontUIResource(font)); } final InputMap spinnerInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); final InputMap editorInputMap = editorTextField.getInputMap(); final KeyStroke[] keys = spinnerInputMap.keys(); for (final KeyStroke k : keys) { editorInputMap.put(k, spinnerInputMap.get(k)); } } void updateEnabledState() { updateEnabledState(spinner, spinner.isEnabled()); } private void updateEnabledState(final Container c, final boolean enabled) { for (int counter = c.getComponentCount() - 1; counter >= 0; counter--) { final Component child = c.getComponent(counter); child.setEnabled(enabled); if (child instanceof Container) { updateEnabledState((Container) child, enabled); } } } private void installKeyboardActions() { final InputMap iMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); SwingUtilities.replaceUIInputMap(spinner, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, iMap); SwingUtilities.replaceUIActionMap(spinner, getActionMap()); } private InputMap getInputMap(final int condition) { if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) { return (InputMap) UIManager.get("Spinner.ancestorInputMap"); } return null; } private ActionMap getActionMap() { ActionMap map = (ActionMap) UIManager.get("Spinner.actionMap"); if (map == null) { map = createActionMap(); if (map != null) { UIManager.getLookAndFeelDefaults().put("Spinner.actionMap", map); } } return map; } private ActionMap createActionMap() { final ActionMap map = new ActionMapUIResource(); map.put("increment", getNextButtonHandler()); map.put("decrement", getPreviousButtonHandler()); return map; } @SuppressWarnings("serial") // Superclass is not serializable across versions private static class ArrowButtonHandler extends AbstractAction implements MouseListener { final javax.swing.Timer autoRepeatTimer; final boolean isNext; JSpinner spinner = null; ArrowButtonHandler(final String name, final boolean isNext) { super(name); this.isNext = isNext; autoRepeatTimer = new javax.swing.Timer(60, this); autoRepeatTimer.setInitialDelay(300); } private JSpinner eventToSpinner(final AWTEvent e) { Object src = e.getSource(); while ((src instanceof Component) && !(src instanceof JSpinner)) { src = ((Component) src).getParent(); } return (src instanceof JSpinner) ? (JSpinner) src : null; } @Override public void actionPerformed(final ActionEvent e) { if (!(e.getSource() instanceof javax.swing.Timer)) { // Most likely resulting from being in ActionMap. spinner = eventToSpinner(e); } if (spinner == null) { return; } try { final int calendarField = getCalendarField(spinner); spinner.commitEdit(); if (calendarField != -1) { ((SpinnerDateModel) spinner.getModel()).setCalendarField(calendarField); } final Object value = (isNext) ? spinner.getNextValue() : spinner.getPreviousValue(); if (value != null) { spinner.setValue(value); select(spinner); } } catch (final IllegalArgumentException iae) { UIManager.getLookAndFeel().provideErrorFeedback(spinner); } catch (final ParseException pe) { UIManager.getLookAndFeel().provideErrorFeedback(spinner); } }
If the spinner's editor is a DateEditor, this selects the field associated with the value that is being incremented.
/** * If the spinner's editor is a DateEditor, this selects the field * associated with the value that is being incremented. */
private void select(final JSpinner spinnerComponent) { final JComponent editor = spinnerComponent.getEditor(); if (!(editor instanceof JSpinner.DateEditor)) { return; } final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor) editor; final JFormattedTextField ftf = dateEditor.getTextField(); final Format format = dateEditor.getFormat(); Object value; if (format == null || (value = spinnerComponent.getValue()) == null) { return; } final SpinnerDateModel model = dateEditor.getModel(); final DateFormat.Field field = DateFormat.Field.ofCalendarField(model.getCalendarField()); if (field == null) { return; } try { final AttributedCharacterIterator iterator = format.formatToCharacterIterator(value); if (!select(ftf, iterator, field) && field == DateFormat.Field.HOUR0) { select(ftf, iterator, DateFormat.Field.HOUR1); } } catch (final IllegalArgumentException iae) { } }
Selects the passed in field, returning true if it is found, false otherwise.
/** * Selects the passed in field, returning true if it is found, false * otherwise. */
private boolean select(final JFormattedTextField ftf, final AttributedCharacterIterator iterator, final DateFormat.Field field) { final int max = ftf.getDocument().getLength(); iterator.first(); do { final Map<Attribute, Object> attrs = iterator.getAttributes(); if (attrs == null || !attrs.containsKey(field)) { continue; } final int start = iterator.getRunStart(field); final int end = iterator.getRunLimit(field); if (start != -1 && end != -1 && start <= max && end <= max) { ftf.select(start, end); } return true; } while (iterator.next() != CharacterIterator.DONE); return false; }
Returns the calendarField under the start of the selection, or -1 if there is no valid calendar field under the selection (or the spinner isn't editing dates.
/** * Returns the calendarField under the start of the selection, or -1 if * there is no valid calendar field under the selection (or the spinner * isn't editing dates. */
private int getCalendarField(final JSpinner spinnerComponent) { final JComponent editor = spinnerComponent.getEditor(); if (!(editor instanceof JSpinner.DateEditor)) { return -1; } final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor) editor; final JFormattedTextField ftf = dateEditor.getTextField(); final int start = ftf.getSelectionStart(); final JFormattedTextField.AbstractFormatter formatter = ftf.getFormatter(); if (!(formatter instanceof InternationalFormatter)) { return -1; } final Format.Field[] fields = ((InternationalFormatter) formatter).getFields(start); for (final Field element : fields) { if (!(element instanceof DateFormat.Field)) { continue; } int calendarField; if (element == DateFormat.Field.HOUR1) { calendarField = Calendar.HOUR; } else { calendarField = ((DateFormat.Field) element).getCalendarField(); } if (calendarField != -1) { return calendarField; } } return -1; } @Override public void mousePressed(final MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e) || !e.getComponent().isEnabled()) { return; } spinner = eventToSpinner(e); autoRepeatTimer.start(); focusSpinnerIfNecessary(); } @Override public void mouseReleased(final MouseEvent e) { autoRepeatTimer.stop(); spinner = null; } @Override public void mouseClicked(final MouseEvent e) { } @Override public void mouseEntered(final MouseEvent e) { if (spinner != null && !autoRepeatTimer.isRunning() && spinner == eventToSpinner(e)) { autoRepeatTimer.start(); } } @Override public void mouseExited(final MouseEvent e) { if (autoRepeatTimer.isRunning()) { autoRepeatTimer.stop(); } }
Requests focus on a child of the spinner if the spinner doesn't have focus.
/** * Requests focus on a child of the spinner if the spinner doesn't have * focus. */
private void focusSpinnerIfNecessary() { final Component fo = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); if (!spinner.isRequestFocusEnabled() || (fo != null && (SwingUtilities.isDescendingFrom(fo, spinner)))) { return; } Container root = spinner; if (!root.isFocusCycleRoot()) { root = root.getFocusCycleRootAncestor(); } if (root == null) { return; } final FocusTraversalPolicy ftp = root.getFocusTraversalPolicy(); final Component child = ftp.getComponentAfter(root, spinner); if (child != null && SwingUtilities.isDescendingFrom(child, spinner)) { child.requestFocus(); } } } @SuppressWarnings("serial") // Superclass is not serializable across versions class SpinPainter extends JComponent { final AquaPainter<JRSUIState> painter = AquaPainter.create(JRSUIStateFactory.getSpinnerArrows()); ButtonModel fTopModel; ButtonModel fBottomModel; boolean fPressed = false; boolean fTopPressed = false; Dimension kPreferredSize = new Dimension(15, 24); // 19,27 before trimming public SpinPainter(final AbstractButton top, final AbstractButton bottom) { if (top != null) { fTopModel = top.getModel(); } if (bottom != null) { fBottomModel = bottom.getModel(); } } @Override public void paint(final Graphics g) { if (spinner.isOpaque()) { g.setColor(spinner.getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); } AquaUtilControlSize.applySizeForControl(spinner, painter); if (isEnabled()) { if (fTopModel != null && fTopModel.isPressed()) { painter.state.set(State.PRESSED); painter.state.set(BooleanValue.NO); } else if (fBottomModel != null && fBottomModel.isPressed()) { painter.state.set(State.PRESSED); painter.state.set(BooleanValue.YES); } else { painter.state.set(State.ACTIVE); } } else { painter.state.set(State.DISABLED); } final Rectangle bounds = getBounds(); painter.paint(g, spinner, 0, 0, bounds.width, bounds.height); } @Override public Dimension getPreferredSize() { final Size size = AquaUtilControlSize.getUserSizeFrom(this); if (size == Size.MINI) { return new Dimension(kPreferredSize.width, kPreferredSize.height - 8); } return kPreferredSize; } }
A simple layout manager for the editor and the next/previous buttons. See the AquaSpinnerUI javadoc for more information about exactly how the components are arranged.
/** * A simple layout manager for the editor and the next/previous buttons. See * the AquaSpinnerUI javadoc for more information about exactly how the * components are arranged. */
static class SpinnerLayout implements LayoutManager { private Component nextButton = null; private Component previousButton = null; private Component editor = null; private Component painter = null; @Override public void addLayoutComponent(final String name, final Component c) { if ("Next".equals(name)) { nextButton = c; } else if ("Previous".equals(name)) { previousButton = c; } else if ("Editor".equals(name)) { editor = c; } else if ("Painter".equals(name)) { painter = c; } } @Override public void removeLayoutComponent(Component c) { if (c == nextButton) { c = null; } else if (c == previousButton) { previousButton = null; } else if (c == editor) { editor = null; } else if (c == painter) { painter = null; } } private Dimension preferredSize(final Component c) { return (c == null) ? new Dimension(0, 0) : c.getPreferredSize(); } @Override public Dimension preferredLayoutSize(final Container parent) { // Dimension nextD = preferredSize(nextButton); // Dimension previousD = preferredSize(previousButton); final Dimension editorD = preferredSize(editor); final Dimension painterD = preferredSize(painter); /* Force the editors height to be a multiple of 2 */ editorD.height = ((editorD.height + 1) / 2) * 2; final Dimension size = new Dimension(editorD.width, Math.max(painterD.height, editorD.height)); size.width += painterD.width; //Math.max(nextD.width, previousD.width); final Insets insets = parent.getInsets(); size.width += insets.left + insets.right; size.height += insets.top + insets.bottom; return size; } @Override public Dimension minimumLayoutSize(final Container parent) { return preferredLayoutSize(parent); } private void setBounds(final Component c, final int x, final int y, final int width, final int height) { if (c != null) { c.setBounds(x, y, width, height); } } @Override public void layoutContainer(final Container parent) { final Insets insets = parent.getInsets(); final int availWidth = parent.getWidth() - (insets.left + insets.right); final int availHeight = parent.getHeight() - (insets.top + insets.bottom); final Dimension painterD = preferredSize(painter); // Dimension nextD = preferredSize(nextButton); // Dimension previousD = preferredSize(previousButton); final int nextHeight = availHeight / 2; final int previousHeight = availHeight - nextHeight; final int buttonsWidth = painterD.width; //Math.max(nextD.width, previousD.width); final int editorWidth = availWidth - buttonsWidth; /* Deal with the spinners componentOrientation property. */ int editorX, buttonsX; if (parent.getComponentOrientation().isLeftToRight()) { editorX = insets.left; buttonsX = editorX + editorWidth; } else { buttonsX = insets.left; editorX = buttonsX + buttonsWidth; } final int previousY = insets.top + nextHeight; final int painterTop = previousY - (painterD.height / 2); setBounds(editor, editorX, insets.top, editorWidth, availHeight); setBounds(nextButton, buttonsX, insets.top, buttonsWidth, nextHeight); setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight); setBounds(painter, buttonsX, painterTop, buttonsWidth, painterD.height); } }
Detect JSpinner property changes we're interested in and delegate. Subclasses shouldn't need to replace the default propertyChangeListener (although they can by overriding createPropertyChangeListener) since all of the interesting property changes are delegated to protected methods.
/** * Detect JSpinner property changes we're interested in and delegate. * Subclasses shouldn't need to replace the default propertyChangeListener * (although they can by overriding createPropertyChangeListener) since all * of the interesting property changes are delegated to protected methods. */
static class PropertyChangeHandler implements PropertyChangeListener { @Override public void propertyChange(final PropertyChangeEvent e) { final String propertyName = e.getPropertyName(); final JSpinner spinner = (JSpinner) (e.getSource()); final SpinnerUI spinnerUI = spinner.getUI(); if (spinnerUI instanceof AquaSpinnerUI) { final AquaSpinnerUI ui = (AquaSpinnerUI) spinnerUI; if ("editor".equals(propertyName)) { final JComponent oldEditor = (JComponent) e.getOldValue(); final JComponent newEditor = (JComponent) e.getNewValue(); ui.replaceEditor(oldEditor, newEditor); ui.updateEnabledState(); } else if ("componentOrientation".equals(propertyName)) { ComponentOrientation o = (ComponentOrientation) e.getNewValue(); if (o != e.getOldValue()) { JComponent editor = spinner.getEditor(); if (editor != null) { editor.applyComponentOrientation(o); } spinner.revalidate(); spinner.repaint(); } } else if ("enabled".equals(propertyName)) { ui.updateEnabledState(); } else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) { ui.updateToolTipTextForChildren(spinner); } else if ("font".equals(propertyName)) { JComponent editor = spinner.getEditor(); if (editor instanceof JSpinner.DefaultEditor) { JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField(); if (tf != null) { if (tf.getFont() instanceof UIResource) { Font font = spinner.getFont(); tf.setFont(font == null ? null : new FontUIResource(font)); } } } } } } } // Syncronizes the ToolTip text for the components within the spinner // to be the same value as the spinner ToolTip text. void updateToolTipTextForChildren(final JComponent spinnerComponent) { final String toolTipText = spinnerComponent.getToolTipText(); final Component[] children = spinnerComponent.getComponents(); for (final Component element : children) { if (element instanceof JSpinner.DefaultEditor) { final JTextField tf = ((JSpinner.DefaultEditor) element).getTextField(); if (tf != null) { tf.setToolTipText(toolTipText); } } else if (element instanceof JComponent) { ((JComponent) element).setToolTipText(toolTipText); } } } }