1 /* 2 * Copyright (c) 2002, 2010, 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</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 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</code> layout manager to null. This 140 * method is called by <code>uninstallUI</code>. 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</code>. By default it just returns 194 * <code>JSpinner.getEditor()</code>. Subclasses can override 195 * <code>createEditor</code> 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</code> in an 198 * <code>installUI</code> 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</code> method is called when the spinners 205 * editor is changed with <code>JSpinner.setEditor</code>. If you've 206 * overriden this method, then you'll probably want to override 207 * <code>replaceEditor</code> 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</code> when the 225 * <code>JSpinner</code> 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</code> should be coordinated 233 * with the <code>createEditor</code> 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(SynthContext.class, c, 282 SynthLookAndFeel.getRegion(c), style, state); 283 } 284 285 /** 286 * Notifies this UI delegate to repaint the specified component. 287 * This method paints the component background, then calls 288 * the {@link #paint(SynthContext,Graphics)} method. 289 * 290 * <p>In general, this method does not need to be overridden by subclasses. 291 * All Look and Feel rendering code should reside in the {@code paint} method. 292 * 293 * @param g the {@code Graphics} object used for painting 294 * @param c the component being painted 295 * @see #paint(SynthContext,Graphics) 296 */ 297 @Override 298 public void update(Graphics g, JComponent c) { 299 SynthContext context = getContext(c); 300 301 SynthLookAndFeel.update(context, g); 302 context.getPainter().paintSpinnerBackground(context, 303 g, 0, 0, c.getWidth(), c.getHeight()); 304 paint(context, g); 305 context.dispose(); 306 } 307 308 309 /** 310 * Paints the specified component according to the Look and Feel. 311 * <p>This method is not used by Synth Look and Feel. 312 * Painting is handled by the {@link #paint(SynthContext,Graphics)} method. 313 * 314 * @param g the {@code Graphics} object used for painting 315 * @param c the component being painted 316 * @see #paint(SynthContext,Graphics) 317 */ 318 @Override 319 public void paint(Graphics g, JComponent c) { 320 SynthContext context = getContext(c); 321 322 paint(context, g); 323 context.dispose(); 324 } 325 326 /** 327 * Paints the specified component. This implementation does nothing. 328 * 329 * @param context context for the component being painted 330 * @param g the {@code Graphics} object used for painting 331 * @see #update(Graphics,JComponent) 332 */ 333 protected void paint(SynthContext context, Graphics g) { 334 } 335 336 /** 337 * @inheritDoc 338 */ 339 @Override 340 public void paintBorder(SynthContext context, Graphics g, int x, 341 int y, int w, int h) { 342 context.getPainter().paintSpinnerBorder(context, g, x, y, w, h); 343 } 344 345 /** 346 * A simple layout manager for the editor and the next/previous buttons. 347 * See the SynthSpinnerUI javadoc for more information about exactly 348 * how the components are arranged. 349 */ 350 private static class SpinnerLayout implements LayoutManager, UIResource 351 { 352 private Component nextButton = null; 353 private Component previousButton = null; 354 private Component editor = null; 355 356 public void addLayoutComponent(String name, Component c) { 357 if ("Next".equals(name)) { 358 nextButton = c; 359 } 360 else if ("Previous".equals(name)) { 361 previousButton = c; 362 } 363 else if ("Editor".equals(name)) { 364 editor = c; 365 } 366 } 367 368 public void removeLayoutComponent(Component c) { 369 if (c == nextButton) { 370 nextButton = null; 371 } 372 else if (c == previousButton) { 373 previousButton = null; 374 } 375 else if (c == editor) { 376 editor = null; 377 } 378 } 379 380 private Dimension preferredSize(Component c) { 381 return (c == null) ? new Dimension(0, 0) : c.getPreferredSize(); 382 } 383 384 public Dimension preferredLayoutSize(Container parent) { 385 Dimension nextD = preferredSize(nextButton); 386 Dimension previousD = preferredSize(previousButton); 387 Dimension editorD = preferredSize(editor); 388 389 /* Force the editors height to be a multiple of 2 390 */ 391 editorD.height = ((editorD.height + 1) / 2) * 2; 392 393 Dimension size = new Dimension(editorD.width, editorD.height); 394 size.width += Math.max(nextD.width, previousD.width); 395 Insets insets = parent.getInsets(); 396 size.width += insets.left + insets.right; 397 size.height += insets.top + insets.bottom; 398 return size; 399 } 400 401 public Dimension minimumLayoutSize(Container parent) { 402 return preferredLayoutSize(parent); 403 } 404 405 private void setBounds(Component c, int x, int y, int width, int height) { 406 if (c != null) { 407 c.setBounds(x, y, width, height); 408 } 409 } 410 411 public void layoutContainer(Container parent) { 412 Insets insets = parent.getInsets(); 413 int availWidth = parent.getWidth() - (insets.left + insets.right); 414 int availHeight = parent.getHeight() - (insets.top + insets.bottom); 415 Dimension nextD = preferredSize(nextButton); 416 Dimension previousD = preferredSize(previousButton); 417 int nextHeight = availHeight / 2; 418 int previousHeight = availHeight - nextHeight; 419 int buttonsWidth = Math.max(nextD.width, previousD.width); 420 int editorWidth = availWidth - buttonsWidth; 421 422 /* Deal with the spinners componentOrientation property. 423 */ 424 int editorX, buttonsX; 425 if (parent.getComponentOrientation().isLeftToRight()) { 426 editorX = insets.left; 427 buttonsX = editorX + editorWidth; 428 } 429 else { 430 buttonsX = insets.left; 431 editorX = buttonsX + buttonsWidth; 432 } 433 434 int previousY = insets.top + nextHeight; 435 setBounds(editor, editorX, insets.top, editorWidth, availHeight); 436 setBounds(nextButton, buttonsX, insets.top, buttonsWidth, nextHeight); 437 setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight); 438 } 439 } 440 441 /** 442 * @inheritDoc 443 */ 444 @Override 445 public void propertyChange(PropertyChangeEvent e) { 446 JSpinner spinner = (JSpinner)(e.getSource()); 447 SpinnerUI spinnerUI = spinner.getUI(); 448 449 if (spinnerUI instanceof SynthSpinnerUI) { 450 SynthSpinnerUI ui = (SynthSpinnerUI)spinnerUI; 451 452 if (SynthLookAndFeel.shouldUpdateStyle(e)) { 453 ui.updateStyle(spinner); 454 } 455 } 456 } 457 458 /** Listen to editor text field focus changes and repaint whole spinner */ 459 private class EditorFocusHandler implements FocusListener{ 460 /** Invoked when a editor text field gains the keyboard focus. */ 461 @Override public void focusGained(FocusEvent e) { 462 spinner.repaint(); 463 } 464 465 /** Invoked when a editor text field loses the keyboard focus. */ 466 @Override public void focusLost(FocusEvent e) { 467 spinner.repaint(); 468 } 469 } 470 }