1 /*
2  * Copyright (c) 2002, 2013, 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 javax.swing.plaf.synth;
26 
27 import java.awt.*;
28 import java.awt.event.*;
29 import javax.swing.*;
30 import javax.swing.plaf.*;
31 import javax.swing.plaf.basic.BasicSpinnerUI;
32 import java.beans.*;
33 
34 /**
35  * Provides the Synth L&F UI delegate for
36  * {@link javax.swing.JSpinner}.
37  *
38  * @author Hans Muller
39  * @author Joshua Outwater
40  * @since 1.7
41  */
42 public class SynthSpinnerUI extends BasicSpinnerUI
43                             implements PropertyChangeListener, SynthUI {
44     private SynthStyle style;
45     /**
46      * A FocusListener implementation which causes the entire spinner to be
47      * repainted whenever the editor component (typically a text field) becomes
48      * focused, or loses focus. This is necessary because since SynthSpinnerUI
49      * is composed of an editor and two buttons, it is necessary that all three
50      * components indicate that they are "focused" so that they can be drawn
51      * appropriately. The repaint is used to ensure that the buttons are drawn
52      * in the new focused or unfocused state, mirroring that of the editor.
53      */
54     private EditorFocusHandler editorFocusHandler = new EditorFocusHandler();
55 
56     /**
57      * Returns a new instance of SynthSpinnerUI.
58      *
59      * @param c the JSpinner (not used)
60      * @see ComponentUI#createUI
61      * @return a new SynthSpinnerUI object
62      */
createUI(JComponent c)63     public static ComponentUI createUI(JComponent c) {
64         return new SynthSpinnerUI();
65     }
66 
67     /**
68      * {@inheritDoc}
69      */
70     @Override
installListeners()71     protected void installListeners() {
72         super.installListeners();
73         spinner.addPropertyChangeListener(this);
74         JComponent editor = spinner.getEditor();
75         if (editor instanceof JSpinner.DefaultEditor) {
76             JTextField tf = ((JSpinner.DefaultEditor)editor).getTextField();
77             if (tf != null) {
78                 tf.addFocusListener(editorFocusHandler);
79             }
80         }
81     }
82 
83     /**
84      * {@inheritDoc}
85      */
86     @Override
uninstallListeners()87     protected void uninstallListeners() {
88         super.uninstallListeners();
89         spinner.removePropertyChangeListener(this);
90         JComponent editor = spinner.getEditor();
91         if (editor instanceof JSpinner.DefaultEditor) {
92             JTextField tf = ((JSpinner.DefaultEditor)editor).getTextField();
93             if (tf != null) {
94                 tf.removeFocusListener(editorFocusHandler);
95             }
96         }
97     }
98 
99     /**
100      * Initializes the <code>JSpinner</code> <code>border</code>,
101      * <code>foreground</code>, and <code>background</code>, properties
102      * based on the corresponding "Spinner.*" properties from defaults table.
103      * The <code>JSpinners</code> layout is set to the value returned by
104      * <code>createLayout</code>.  This method is called by <code>installUI</code>.
105      *
106      * @see #uninstallDefaults
107      * @see #installUI
108      * @see #createLayout
109      * @see LookAndFeel#installBorder
110      * @see LookAndFeel#installColors
111      */
112     @Override
installDefaults()113     protected void installDefaults() {
114         LayoutManager layout = spinner.getLayout();
115 
116         if (layout == null || layout instanceof UIResource) {
117             spinner.setLayout(createLayout());
118         }
119         updateStyle(spinner);
120     }
121 
122 
updateStyle(JSpinner c)123     private void updateStyle(JSpinner c) {
124         SynthContext context = getContext(c, ENABLED);
125         SynthStyle oldStyle = style;
126         style = SynthLookAndFeel.updateStyle(context, this);
127         if (style != oldStyle) {
128             if (oldStyle != null) {
129                 // Only call installKeyboardActions as uninstall is not
130                 // public.
131                 installKeyboardActions();
132             }
133         }
134     }
135 
136 
137     /**
138      * Sets the <code>JSpinner's</code> layout manager to null.  This
139      * method is called by <code>uninstallUI</code>.
140      *
141      * @see #installDefaults
142      * @see #uninstallUI
143      */
144     @Override
uninstallDefaults()145     protected void uninstallDefaults() {
146         if (spinner.getLayout() instanceof UIResource) {
147             spinner.setLayout(null);
148         }
149 
150         SynthContext context = getContext(spinner, ENABLED);
151 
152         style.uninstallDefaults(context);
153         style = null;
154     }
155 
156     /**
157      * {@inheritDoc}
158      */
159     @Override
createLayout()160     protected LayoutManager createLayout() {
161         return new SpinnerLayout();
162     }
163 
164 
165     /**
166      * {@inheritDoc}
167      */
168     @Override
createPreviousButton()169     protected Component createPreviousButton() {
170         JButton b = new SynthArrowButton(SwingConstants.SOUTH);
171         b.setName("Spinner.previousButton");
172         installPreviousButtonListeners(b);
173         return b;
174     }
175 
176 
177     /**
178      * {@inheritDoc}
179      */
180     @Override
createNextButton()181     protected Component createNextButton() {
182         JButton b = new SynthArrowButton(SwingConstants.NORTH);
183         b.setName("Spinner.nextButton");
184         installNextButtonListeners(b);
185         return b;
186     }
187 
188 
189     /**
190      * This method is called by installUI to get the editor component
191      * of the <code>JSpinner</code>.  By default it just returns
192      * <code>JSpinner.getEditor()</code>.  Subclasses can override
193      * <code>createEditor</code> to return a component that contains
194      * the spinner's editor or null, if they're going to handle adding
195      * the editor to the <code>JSpinner</code> in an
196      * <code>installUI</code> override.
197      * <p>
198      * Typically this method would be overridden to wrap the editor
199      * with a container with a custom border, since one can't assume
200      * that the editors border can be set directly.
201      * <p>
202      * The <code>replaceEditor</code> method is called when the spinners
203      * editor is changed with <code>JSpinner.setEditor</code>.  If you've
204      * overriden this method, then you'll probably want to override
205      * <code>replaceEditor</code> as well.
206      *
207      * @return the JSpinners editor JComponent, spinner.getEditor() by default
208      * @see #installUI
209      * @see #replaceEditor
210      * @see JSpinner#getEditor
211      */
212     @Override
createEditor()213     protected JComponent createEditor() {
214         JComponent editor = spinner.getEditor();
215         editor.setName("Spinner.editor");
216         updateEditorAlignment(editor);
217         return editor;
218     }
219 
220 
221     /**
222      * Called by the <code>PropertyChangeListener</code> when the
223      * <code>JSpinner</code> editor property changes.  It's the responsibility
224      * of this method to remove the old editor and add the new one.  By
225      * default this operation is just:
226      * <pre>
227      * spinner.remove(oldEditor);
228      * spinner.add(newEditor, "Editor");
229      * </pre>
230      * The implementation of <code>replaceEditor</code> should be coordinated
231      * with the <code>createEditor</code> method.
232      *
233      * @see #createEditor
234      * @see #createPropertyChangeListener
235      */
236     @Override
replaceEditor(JComponent oldEditor, JComponent newEditor)237     protected void replaceEditor(JComponent oldEditor, JComponent newEditor) {
238         spinner.remove(oldEditor);
239         spinner.add(newEditor, "Editor");
240         if (oldEditor instanceof JSpinner.DefaultEditor) {
241             JTextField tf = ((JSpinner.DefaultEditor)oldEditor).getTextField();
242             if (tf != null) {
243                 tf.removeFocusListener(editorFocusHandler);
244             }
245         }
246         if (newEditor instanceof JSpinner.DefaultEditor) {
247             JTextField tf = ((JSpinner.DefaultEditor)newEditor).getTextField();
248             if (tf != null) {
249                 tf.addFocusListener(editorFocusHandler);
250             }
251         }
252     }
253 
updateEditorAlignment(JComponent editor)254     private void updateEditorAlignment(JComponent editor) {
255         if (editor instanceof JSpinner.DefaultEditor) {
256             SynthContext context = getContext(spinner);
257             Integer alignment = (Integer)context.getStyle().get(
258                     context, "Spinner.editorAlignment");
259             JTextField text = ((JSpinner.DefaultEditor)editor).getTextField();
260             if (alignment != null) {
261                 text.setHorizontalAlignment(alignment);
262 
263             }
264             // copy across the sizeVariant property to the editor
265             text.putClientProperty("JComponent.sizeVariant",
266                     spinner.getClientProperty("JComponent.sizeVariant"));
267         }
268     }
269 
270     /**
271      * {@inheritDoc}
272      */
273     @Override
getContext(JComponent c)274     public SynthContext getContext(JComponent c) {
275         return getContext(c, SynthLookAndFeel.getComponentState(c));
276     }
277 
getContext(JComponent c, int state)278     private SynthContext getContext(JComponent c, int state) {
279         return SynthContext.getContext(c, style, state);
280     }
281 
282     /**
283      * Notifies this UI delegate to repaint the specified component.
284      * This method paints the component background, then calls
285      * the {@link #paint(SynthContext,Graphics)} method.
286      *
287      * <p>In general, this method does not need to be overridden by subclasses.
288      * All Look and Feel rendering code should reside in the {@code paint} method.
289      *
290      * @param g the {@code Graphics} object used for painting
291      * @param c the component being painted
292      * @see #paint(SynthContext,Graphics)
293      */
294     @Override
update(Graphics g, JComponent c)295     public void update(Graphics g, JComponent c) {
296         SynthContext context = getContext(c);
297 
298         SynthLookAndFeel.update(context, g);
299         context.getPainter().paintSpinnerBackground(context,
300                           g, 0, 0, c.getWidth(), c.getHeight());
301         paint(context, g);
302     }
303 
304 
305     /**
306      * Paints the specified component according to the Look and Feel.
307      * <p>This method is not used by Synth Look and Feel.
308      * Painting is handled by the {@link #paint(SynthContext,Graphics)} method.
309      *
310      * @param g the {@code Graphics} object used for painting
311      * @param c the component being painted
312      * @see #paint(SynthContext,Graphics)
313      */
314     @Override
paint(Graphics g, JComponent c)315     public void paint(Graphics g, JComponent c) {
316         SynthContext context = getContext(c);
317 
318         paint(context, g);
319     }
320 
321     /**
322      * Paints the specified component. This implementation does nothing.
323      *
324      * @param context context for the component being painted
325      * @param g the {@code Graphics} object used for painting
326      * @see #update(Graphics,JComponent)
327      */
paint(SynthContext context, Graphics g)328     protected void paint(SynthContext context, Graphics g) {
329     }
330 
331     /**
332      * {@inheritDoc}
333      */
334     @Override
paintBorder(SynthContext context, Graphics g, int x, int y, int w, int h)335     public void paintBorder(SynthContext context, Graphics g, int x,
336                             int y, int w, int h) {
337         context.getPainter().paintSpinnerBorder(context, g, x, y, w, h);
338     }
339 
340     /**
341      * A simple layout manager for the editor and the next/previous buttons.
342      * See the SynthSpinnerUI javadoc for more information about exactly
343      * how the components are arranged.
344      */
345     private static class SpinnerLayout implements LayoutManager, UIResource
346     {
347         private Component nextButton = null;
348         private Component previousButton = null;
349         private Component editor = null;
350 
addLayoutComponent(String name, Component c)351         public void addLayoutComponent(String name, Component c) {
352             if ("Next".equals(name)) {
353                 nextButton = c;
354             }
355             else if ("Previous".equals(name)) {
356                 previousButton = c;
357             }
358             else if ("Editor".equals(name)) {
359                 editor = c;
360             }
361         }
362 
removeLayoutComponent(Component c)363         public void removeLayoutComponent(Component c) {
364             if (c == nextButton) {
365                 nextButton = null;
366             }
367             else if (c == previousButton) {
368                 previousButton = null;
369             }
370             else if (c == editor) {
371                 editor = null;
372             }
373         }
374 
preferredSize(Component c)375         private Dimension preferredSize(Component c) {
376             return (c == null) ? new Dimension(0, 0) : c.getPreferredSize();
377         }
378 
preferredLayoutSize(Container parent)379         public Dimension preferredLayoutSize(Container parent) {
380             Dimension nextD = preferredSize(nextButton);
381             Dimension previousD = preferredSize(previousButton);
382             Dimension editorD = preferredSize(editor);
383 
384             /* Force the editors height to be a multiple of 2
385              */
386             editorD.height = ((editorD.height + 1) / 2) * 2;
387 
388             Dimension size = new Dimension(editorD.width, editorD.height);
389             size.width += Math.max(nextD.width, previousD.width);
390             Insets insets = parent.getInsets();
391             size.width += insets.left + insets.right;
392             size.height += insets.top + insets.bottom;
393             return size;
394         }
395 
minimumLayoutSize(Container parent)396         public Dimension minimumLayoutSize(Container parent) {
397             return preferredLayoutSize(parent);
398         }
399 
setBounds(Component c, int x, int y, int width, int height)400         private void setBounds(Component c, int x, int y, int width, int height) {
401             if (c != null) {
402                 c.setBounds(x, y, width, height);
403             }
404         }
405 
layoutContainer(Container parent)406         public void layoutContainer(Container parent) {
407             Insets insets = parent.getInsets();
408             int availWidth = parent.getWidth() - (insets.left + insets.right);
409             int availHeight = parent.getHeight() - (insets.top + insets.bottom);
410             Dimension nextD = preferredSize(nextButton);
411             Dimension previousD = preferredSize(previousButton);
412             int nextHeight = availHeight / 2;
413             int previousHeight = availHeight - nextHeight;
414             int buttonsWidth = Math.max(nextD.width, previousD.width);
415             int editorWidth = availWidth - buttonsWidth;
416 
417             /* Deal with the spinners componentOrientation property.
418              */
419             int editorX, buttonsX;
420             if (parent.getComponentOrientation().isLeftToRight()) {
421                 editorX = insets.left;
422                 buttonsX = editorX + editorWidth;
423             }
424             else {
425                 buttonsX = insets.left;
426                 editorX = buttonsX + buttonsWidth;
427             }
428 
429             int previousY = insets.top + nextHeight;
430             setBounds(editor, editorX, insets.top, editorWidth, availHeight);
431             setBounds(nextButton, buttonsX, insets.top, buttonsWidth, nextHeight);
432             setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight);
433         }
434     }
435 
436     /**
437      * {@inheritDoc}
438      */
439     @Override
propertyChange(PropertyChangeEvent e)440     public void propertyChange(PropertyChangeEvent e) {
441         JSpinner spinner = (JSpinner)(e.getSource());
442         SpinnerUI spinnerUI = spinner.getUI();
443 
444         if (spinnerUI instanceof SynthSpinnerUI) {
445             SynthSpinnerUI ui = (SynthSpinnerUI)spinnerUI;
446 
447             if (SynthLookAndFeel.shouldUpdateStyle(e)) {
448                 ui.updateStyle(spinner);
449             }
450         }
451     }
452 
453     /** Listen to editor text field focus changes and repaint whole spinner */
454     private class EditorFocusHandler implements FocusListener{
455         /** Invoked when a editor text field gains the keyboard focus. */
focusGained(FocusEvent e)456         @Override public void focusGained(FocusEvent e) {
457             spinner.repaint();
458         }
459 
460         /** Invoked when a editor text field loses the keyboard focus. */
focusLost(FocusEvent e)461         @Override public void focusLost(FocusEvent e) {
462             spinner.repaint();
463         }
464     }
465 }
466