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