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