1 /* 2 * Copyright (c) 2000, 2020, 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 26 package javax.swing.plaf.basic; 27 28 import java.awt.AWTEvent; 29 import java.awt.Component; 30 import java.awt.ComponentOrientation; 31 import java.awt.Container; 32 import java.awt.Dimension; 33 import java.awt.FocusTraversalPolicy; 34 import java.awt.Font; 35 import java.awt.Insets; 36 import java.awt.KeyboardFocusManager; 37 import java.awt.LayoutManager; 38 import java.awt.event.ActionEvent; 39 import java.awt.event.FocusEvent; 40 import java.awt.event.FocusListener; 41 import java.awt.event.MouseEvent; 42 import java.awt.event.MouseListener; 43 import java.beans.PropertyChangeEvent; 44 import java.beans.PropertyChangeListener; 45 import java.text.AttributedCharacterIterator; 46 import java.text.CharacterIterator; 47 import java.text.DateFormat; 48 import java.text.Format; 49 import java.text.ParseException; 50 import java.util.Calendar; 51 import java.util.Map; 52 53 import javax.swing.AbstractAction; 54 import javax.swing.ButtonModel; 55 import javax.swing.InputMap; 56 import javax.swing.JButton; 57 import javax.swing.JComponent; 58 import javax.swing.JFormattedTextField; 59 import javax.swing.JPanel; 60 import javax.swing.JSpinner; 61 import javax.swing.JTextField; 62 import javax.swing.LookAndFeel; 63 import javax.swing.SpinnerDateModel; 64 import javax.swing.SpinnerModel; 65 import javax.swing.SwingConstants; 66 import javax.swing.SwingUtilities; 67 import javax.swing.UIManager; 68 import javax.swing.border.Border; 69 import javax.swing.border.CompoundBorder; 70 import javax.swing.event.ChangeEvent; 71 import javax.swing.event.ChangeListener; 72 import javax.swing.plaf.ComponentUI; 73 import javax.swing.plaf.FontUIResource; 74 import javax.swing.plaf.SpinnerUI; 75 import javax.swing.plaf.UIResource; 76 import javax.swing.text.InternationalFormatter; 77 78 import sun.swing.DefaultLookup; 79 80 /** 81 * The default Spinner UI delegate. 82 * 83 * @author Hans Muller 84 * @since 1.4 85 */ 86 public class BasicSpinnerUI extends SpinnerUI 87 { 88 /** 89 * The spinner that we're a UI delegate for. Initialized by 90 * the <code>installUI</code> method, and reset to null 91 * by <code>uninstallUI</code>. 92 * 93 * @see #installUI 94 * @see #uninstallUI 95 */ 96 protected JSpinner spinner; 97 private Handler handler; 98 99 100 /** 101 * The mouse/action listeners that are added to the spinner's 102 * arrow buttons. These listeners are shared by all 103 * spinner arrow buttons. 104 * 105 * @see #createNextButton 106 * @see #createPreviousButton 107 */ 108 private static final ArrowButtonHandler nextButtonHandler = new ArrowButtonHandler("increment", true); 109 private static final ArrowButtonHandler previousButtonHandler = new ArrowButtonHandler("decrement", false); 110 private PropertyChangeListener propertyChangeListener; 111 112 113 /** 114 * Used by the default LayoutManager class - SpinnerLayout for 115 * missing (null) editor/nextButton/previousButton children. 116 */ 117 private static final Dimension zeroSize = new Dimension(0, 0); 118 119 /** 120 * Constructs a {@code BasicSpinnerUI}. 121 */ BasicSpinnerUI()122 public BasicSpinnerUI() {} 123 124 /** 125 * Returns a new instance of BasicSpinnerUI. SpinnerListUI 126 * delegates are allocated one per JSpinner. 127 * 128 * @param c the JSpinner (not used) 129 * @see ComponentUI#createUI 130 * @return a new BasicSpinnerUI object 131 */ createUI(JComponent c)132 public static ComponentUI createUI(JComponent c) { 133 return new BasicSpinnerUI(); 134 } 135 136 maybeAdd(Component c, String s)137 private void maybeAdd(Component c, String s) { 138 if (c != null) { 139 spinner.add(c, s); 140 } 141 } 142 143 144 /** 145 * Calls <code>installDefaults</code>, <code>installListeners</code>, 146 * and then adds the components returned by <code>createNextButton</code>, 147 * <code>createPreviousButton</code>, and <code>createEditor</code>. 148 * 149 * @param c the JSpinner 150 * @see #installDefaults 151 * @see #installListeners 152 * @see #createNextButton 153 * @see #createPreviousButton 154 * @see #createEditor 155 */ installUI(JComponent c)156 public void installUI(JComponent c) { 157 this.spinner = (JSpinner)c; 158 installDefaults(); 159 installListeners(); 160 maybeAdd(createNextButton(), "Next"); 161 maybeAdd(createPreviousButton(), "Previous"); 162 maybeAdd(createEditor(), "Editor"); 163 updateEnabledState(); 164 installKeyboardActions(); 165 } 166 167 168 /** 169 * Calls <code>uninstallDefaults</code>, <code>uninstallListeners</code>, 170 * and then removes all of the spinners children. 171 * 172 * @param c the JSpinner (not used) 173 */ uninstallUI(JComponent c)174 public void uninstallUI(JComponent c) { 175 uninstallDefaults(); 176 uninstallListeners(); 177 this.spinner = null; 178 c.removeAll(); 179 } 180 181 182 /** 183 * Initializes <code>PropertyChangeListener</code> with 184 * a shared object that delegates interesting PropertyChangeEvents 185 * to protected methods. 186 * <p> 187 * This method is called by <code>installUI</code>. 188 * 189 * @see #replaceEditor 190 * @see #uninstallListeners 191 */ installListeners()192 protected void installListeners() { 193 propertyChangeListener = createPropertyChangeListener(); 194 spinner.addPropertyChangeListener(propertyChangeListener); 195 if (DefaultLookup.getBoolean(spinner, this, 196 "Spinner.disableOnBoundaryValues", false)) { 197 spinner.addChangeListener(getHandler()); 198 } 199 JComponent editor = spinner.getEditor(); 200 if (editor != null && editor instanceof JSpinner.DefaultEditor) { 201 JTextField tf = ((JSpinner.DefaultEditor)editor).getTextField(); 202 if (tf != null) { 203 tf.addFocusListener(nextButtonHandler); 204 tf.addFocusListener(previousButtonHandler); 205 } 206 } 207 } 208 209 210 /** 211 * Removes the <code>PropertyChangeListener</code> added 212 * by installListeners. 213 * <p> 214 * This method is called by <code>uninstallUI</code>. 215 * 216 * @see #installListeners 217 */ uninstallListeners()218 protected void uninstallListeners() { 219 spinner.removePropertyChangeListener(propertyChangeListener); 220 spinner.removeChangeListener(handler); 221 JComponent editor = spinner.getEditor(); 222 removeEditorBorderListener(editor); 223 if (editor instanceof JSpinner.DefaultEditor) { 224 JTextField tf = ((JSpinner.DefaultEditor)editor).getTextField(); 225 if (tf != null) { 226 tf.removeFocusListener(nextButtonHandler); 227 tf.removeFocusListener(previousButtonHandler); 228 } 229 } 230 propertyChangeListener = null; 231 handler = null; 232 } 233 234 235 /** 236 * Initialize the <code>JSpinner</code> <code>border</code>, 237 * <code>foreground</code>, and <code>background</code>, properties 238 * based on the corresponding "Spinner.*" properties from defaults table. 239 * The <code>JSpinners</code> layout is set to the value returned by 240 * <code>createLayout</code>. This method is called by <code>installUI</code>. 241 * 242 * @see #uninstallDefaults 243 * @see #installUI 244 * @see #createLayout 245 * @see LookAndFeel#installBorder 246 * @see LookAndFeel#installColors 247 */ installDefaults()248 protected void installDefaults() { 249 spinner.setLayout(createLayout()); 250 LookAndFeel.installBorder(spinner, "Spinner.border"); 251 LookAndFeel.installColorsAndFont(spinner, "Spinner.background", "Spinner.foreground", "Spinner.font"); 252 LookAndFeel.installProperty(spinner, "opaque", Boolean.TRUE); 253 254 JComponent editor = spinner.getEditor(); 255 if (editor instanceof JSpinner.DefaultEditor) { 256 JTextField tf = ((JSpinner.DefaultEditor) editor).getTextField(); 257 if (tf != null) { 258 if (tf.getFont() instanceof UIResource) { 259 Font font = spinner.getFont(); 260 tf.setFont(font == null ? null : new FontUIResource(font)); 261 } 262 } 263 } 264 } 265 266 267 /** 268 * Sets the <code>JSpinner's</code> layout manager to null. This 269 * method is called by <code>uninstallUI</code>. 270 * 271 * @see #installDefaults 272 * @see #uninstallUI 273 */ uninstallDefaults()274 protected void uninstallDefaults() { 275 LookAndFeel.uninstallBorder(spinner); 276 spinner.setLayout(null); 277 } 278 279 getHandler()280 private Handler getHandler() { 281 if (handler == null) { 282 handler = new Handler(); 283 } 284 return handler; 285 } 286 287 288 /** 289 * Installs the necessary listeners on the next button, <code>c</code>, 290 * to update the <code>JSpinner</code> in response to a user gesture. 291 * 292 * @param c Component to install the listeners on 293 * @throws NullPointerException if <code>c</code> is null. 294 * @see #createNextButton 295 * @since 1.5 296 */ installNextButtonListeners(Component c)297 protected void installNextButtonListeners(Component c) { 298 installButtonListeners(c, nextButtonHandler); 299 } 300 301 /** 302 * Installs the necessary listeners on the previous button, <code>c</code>, 303 * to update the <code>JSpinner</code> in response to a user gesture. 304 * 305 * @param c Component to install the listeners on. 306 * @throws NullPointerException if <code>c</code> is null. 307 * @see #createPreviousButton 308 * @since 1.5 309 */ installPreviousButtonListeners(Component c)310 protected void installPreviousButtonListeners(Component c) { 311 installButtonListeners(c, previousButtonHandler); 312 } 313 installButtonListeners(Component c, ArrowButtonHandler handler)314 private void installButtonListeners(Component c, 315 ArrowButtonHandler handler) { 316 if (c instanceof JButton) { 317 ((JButton)c).addActionListener(handler); 318 } 319 c.addMouseListener(handler); 320 } 321 322 /** 323 * Creates a <code>LayoutManager</code> that manages the <code>editor</code>, 324 * <code>nextButton</code>, and <code>previousButton</code> 325 * children of the JSpinner. These three children must be 326 * added with a constraint that identifies their role: 327 * "Editor", "Next", and "Previous". The default layout manager 328 * can handle the absence of any of these children. 329 * 330 * @return a LayoutManager for the editor, next button, and previous button. 331 * @see #createNextButton 332 * @see #createPreviousButton 333 * @see #createEditor 334 */ createLayout()335 protected LayoutManager createLayout() { 336 return getHandler(); 337 } 338 339 340 /** 341 * Creates a <code>PropertyChangeListener</code> that can be 342 * added to the JSpinner itself. Typically, this listener 343 * will call replaceEditor when the "editor" property changes, 344 * since it's the <code>SpinnerUI's</code> responsibility to 345 * add the editor to the JSpinner (and remove the old one). 346 * This method is called by <code>installListeners</code>. 347 * 348 * @return A PropertyChangeListener for the JSpinner itself 349 * @see #installListeners 350 */ createPropertyChangeListener()351 protected PropertyChangeListener createPropertyChangeListener() { 352 return getHandler(); 353 } 354 355 356 /** 357 * Creates a decrement button, i.e. component that replaces the spinner 358 * value with the object returned by <code>spinner.getPreviousValue</code>. 359 * By default the <code>previousButton</code> is a {@code JButton}. If the 360 * decrement button is not needed this method should return {@code null}. 361 * 362 * @return a component that will replace the spinner's value with the 363 * previous value in the sequence, or {@code null} 364 * @see #installUI 365 * @see #createNextButton 366 * @see #installPreviousButtonListeners 367 */ createPreviousButton()368 protected Component createPreviousButton() { 369 Component c = createArrowButton(SwingConstants.SOUTH); 370 c.setName("Spinner.previousButton"); 371 installPreviousButtonListeners(c); 372 return c; 373 } 374 375 376 /** 377 * Creates an increment button, i.e. component that replaces the spinner 378 * value with the object returned by <code>spinner.getNextValue</code>. 379 * By default the <code>nextButton</code> is a {@code JButton}. If the 380 * increment button is not needed this method should return {@code null}. 381 * 382 * @return a component that will replace the spinner's value with the 383 * next value in the sequence, or {@code null} 384 * @see #installUI 385 * @see #createPreviousButton 386 * @see #installNextButtonListeners 387 */ createNextButton()388 protected Component createNextButton() { 389 Component c = createArrowButton(SwingConstants.NORTH); 390 c.setName("Spinner.nextButton"); 391 installNextButtonListeners(c); 392 return c; 393 } 394 createArrowButton(int direction)395 private Component createArrowButton(int direction) { 396 JButton b = new BasicArrowButton(direction); 397 Border buttonBorder = UIManager.getBorder("Spinner.arrowButtonBorder"); 398 if (buttonBorder instanceof UIResource) { 399 // Wrap the border to avoid having the UIResource be replaced by 400 // the ButtonUI. This is the opposite of using BorderUIResource. 401 b.setBorder(new CompoundBorder(buttonBorder, null)); 402 } else { 403 b.setBorder(buttonBorder); 404 } 405 b.setInheritsPopupMenu(true); 406 return b; 407 } 408 409 410 /** 411 * This method is called by installUI to get the editor component 412 * of the <code>JSpinner</code>. By default it just returns 413 * <code>JSpinner.getEditor()</code>. Subclasses can override 414 * <code>createEditor</code> to return a component that contains 415 * the spinner's editor or null, if they're going to handle adding 416 * the editor to the <code>JSpinner</code> in an 417 * <code>installUI</code> override. 418 * <p> 419 * Typically this method would be overridden to wrap the editor 420 * with a container with a custom border, since one can't assume 421 * that the editors border can be set directly. 422 * <p> 423 * The <code>replaceEditor</code> method is called when the spinners 424 * editor is changed with <code>JSpinner.setEditor</code>. If you've 425 * overriden this method, then you'll probably want to override 426 * <code>replaceEditor</code> as well. 427 * 428 * @return the JSpinners editor JComponent, spinner.getEditor() by default 429 * @see #installUI 430 * @see #replaceEditor 431 * @see JSpinner#getEditor 432 */ createEditor()433 protected JComponent createEditor() { 434 JComponent editor = spinner.getEditor(); 435 maybeRemoveEditorBorder(editor); 436 installEditorBorderListener(editor); 437 editor.setInheritsPopupMenu(true); 438 updateEditorAlignment(editor); 439 return editor; 440 } 441 442 443 /** 444 * Called by the <code>PropertyChangeListener</code> when the 445 * <code>JSpinner</code> editor property changes. It's the responsibility 446 * of this method to remove the old editor and add the new one. By 447 * default this operation is just: 448 * <pre> 449 * spinner.remove(oldEditor); 450 * spinner.add(newEditor, "Editor"); 451 * </pre> 452 * The implementation of <code>replaceEditor</code> should be coordinated 453 * with the <code>createEditor</code> method. 454 * 455 * @param oldEditor an old instance of editor 456 * @param newEditor a new instance of editor 457 * @see #createEditor 458 * @see #createPropertyChangeListener 459 */ replaceEditor(JComponent oldEditor, JComponent newEditor)460 protected void replaceEditor(JComponent oldEditor, JComponent newEditor) { 461 spinner.remove(oldEditor); 462 maybeRemoveEditorBorder(newEditor); 463 installEditorBorderListener(newEditor); 464 newEditor.setInheritsPopupMenu(true); 465 spinner.add(newEditor, "Editor"); 466 } 467 updateEditorAlignment(JComponent editor)468 private void updateEditorAlignment(JComponent editor) { 469 if (editor instanceof JSpinner.DefaultEditor) { 470 // if editor alignment isn't set in LAF, we get 0 (CENTER) here 471 int alignment = UIManager.getInt("Spinner.editorAlignment"); 472 JTextField text = ((JSpinner.DefaultEditor)editor).getTextField(); 473 text.setHorizontalAlignment(alignment); 474 } 475 } 476 477 /** 478 * Remove the border around the inner editor component for LaFs 479 * that install an outside border around the spinner, 480 */ maybeRemoveEditorBorder(JComponent editor)481 private void maybeRemoveEditorBorder(JComponent editor) { 482 if (!UIManager.getBoolean("Spinner.editorBorderPainted")) { 483 if (editor instanceof JPanel && 484 editor.getBorder() == null && 485 editor.getComponentCount() > 0) { 486 487 editor = (JComponent)editor.getComponent(0); 488 } 489 490 if (editor != null && editor.getBorder() instanceof UIResource) { 491 editor.setBorder(null); 492 } 493 } 494 } 495 496 /** 497 * Remove the border around the inner editor component for LaFs 498 * that install an outside border around the spinner, 499 */ installEditorBorderListener(JComponent editor)500 private void installEditorBorderListener(JComponent editor) { 501 if (!UIManager.getBoolean("Spinner.editorBorderPainted")) { 502 if (editor instanceof JPanel && 503 editor.getBorder() == null && 504 editor.getComponentCount() > 0) { 505 506 editor = (JComponent)editor.getComponent(0); 507 } 508 if (editor != null && 509 (editor.getBorder() == null || 510 editor.getBorder() instanceof UIResource)) { 511 editor.addPropertyChangeListener(getHandler()); 512 } 513 } 514 } 515 removeEditorBorderListener(JComponent editor)516 private void removeEditorBorderListener(JComponent editor) { 517 if (!UIManager.getBoolean("Spinner.editorBorderPainted")) { 518 if (editor instanceof JPanel && 519 editor.getComponentCount() > 0) { 520 521 editor = (JComponent)editor.getComponent(0); 522 } 523 if (editor != null) { 524 editor.removePropertyChangeListener(getHandler()); 525 } 526 } 527 } 528 529 530 /** 531 * Updates the enabled state of the children Components based on the 532 * enabled state of the <code>JSpinner</code>. 533 */ updateEnabledState()534 private void updateEnabledState() { 535 updateEnabledState(spinner, spinner.isEnabled()); 536 } 537 538 539 /** 540 * Recursively updates the enabled state of the child 541 * <code>Component</code>s of <code>c</code>. 542 */ updateEnabledState(Container c, boolean enabled)543 private void updateEnabledState(Container c, boolean enabled) { 544 for (int counter = c.getComponentCount() - 1; counter >= 0;counter--) { 545 Component child = c.getComponent(counter); 546 547 if (DefaultLookup.getBoolean(spinner, this, 548 "Spinner.disableOnBoundaryValues", false)) { 549 SpinnerModel model = spinner.getModel(); 550 if (child.getName() == "Spinner.nextButton" && 551 model.getNextValue() == null) { 552 child.setEnabled(false); 553 } 554 else if (child.getName() == "Spinner.previousButton" && 555 model.getPreviousValue() == null) { 556 child.setEnabled(false); 557 } 558 else { 559 child.setEnabled(enabled); 560 } 561 } 562 else { 563 child.setEnabled(enabled); 564 } 565 if (child instanceof Container) { 566 updateEnabledState((Container)child, enabled); 567 } 568 } 569 } 570 571 572 /** 573 * Installs the keyboard Actions onto the JSpinner. 574 * 575 * @since 1.5 576 */ installKeyboardActions()577 protected void installKeyboardActions() { 578 InputMap iMap = getInputMap(JComponent. 579 WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 580 581 SwingUtilities.replaceUIInputMap(spinner, JComponent. 582 WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, 583 iMap); 584 585 LazyActionMap.installLazyActionMap(spinner, BasicSpinnerUI.class, 586 "Spinner.actionMap"); 587 } 588 589 /** 590 * Returns the InputMap to install for <code>condition</code>. 591 */ getInputMap(int condition)592 private InputMap getInputMap(int condition) { 593 if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) { 594 return (InputMap)DefaultLookup.get(spinner, this, 595 "Spinner.ancestorInputMap"); 596 } 597 return null; 598 } 599 loadActionMap(LazyActionMap map)600 static void loadActionMap(LazyActionMap map) { 601 map.put("increment", nextButtonHandler); 602 map.put("decrement", previousButtonHandler); 603 } 604 605 /** 606 * Returns the baseline. 607 * 608 * @throws NullPointerException {@inheritDoc} 609 * @throws IllegalArgumentException {@inheritDoc} 610 * @see javax.swing.JComponent#getBaseline(int, int) 611 * @since 1.6 612 */ getBaseline(JComponent c, int width, int height)613 public int getBaseline(JComponent c, int width, int height) { 614 super.getBaseline(c, width, height); 615 JComponent editor = spinner.getEditor(); 616 Insets insets = spinner.getInsets(); 617 width = width - insets.left - insets.right; 618 height = height - insets.top - insets.bottom; 619 if (width >= 0 && height >= 0) { 620 int baseline = editor.getBaseline(width, height); 621 if (baseline >= 0) { 622 return insets.top + baseline; 623 } 624 } 625 return -1; 626 } 627 628 /** 629 * Returns an enum indicating how the baseline of the component 630 * changes as the size changes. 631 * 632 * @throws NullPointerException {@inheritDoc} 633 * @see javax.swing.JComponent#getBaseline(int, int) 634 * @since 1.6 635 */ getBaselineResizeBehavior( JComponent c)636 public Component.BaselineResizeBehavior getBaselineResizeBehavior( 637 JComponent c) { 638 super.getBaselineResizeBehavior(c); 639 return spinner.getEditor().getBaselineResizeBehavior(); 640 } 641 642 /** 643 * A handler for spinner arrow button mouse and action events. When 644 * a left mouse pressed event occurs we look up the (enabled) spinner 645 * that's the source of the event and start the autorepeat timer. The 646 * timer fires action events until any button is released at which 647 * point the timer is stopped and the reference to the spinner cleared. 648 * The timer doesn't start until after a 300ms delay, so often the 649 * source of the initial (and final) action event is just the button 650 * logic for mouse released - which means that we're relying on the fact 651 * that our mouse listener runs after the buttons mouse listener. 652 * <p> 653 * Note that one instance of this handler is shared by all slider previous 654 * arrow buttons and likewise for all of the next buttons, 655 * so it doesn't have any state that persists beyond the limits 656 * of a single button pressed/released gesture. 657 */ 658 @SuppressWarnings("serial") // Superclass is not serializable across versions 659 private static class ArrowButtonHandler extends AbstractAction 660 implements FocusListener, MouseListener, UIResource { 661 final javax.swing.Timer autoRepeatTimer; 662 final boolean isNext; 663 JSpinner spinner = null; 664 JButton arrowButton = null; 665 ArrowButtonHandler(String name, boolean isNext)666 ArrowButtonHandler(String name, boolean isNext) { 667 super(name); 668 this.isNext = isNext; 669 autoRepeatTimer = new javax.swing.Timer(60, this); 670 autoRepeatTimer.setInitialDelay(300); 671 } 672 eventToSpinner(AWTEvent e)673 private JSpinner eventToSpinner(AWTEvent e) { 674 Object src = e.getSource(); 675 while ((src instanceof Component) && !(src instanceof JSpinner)) { 676 src = ((Component)src).getParent(); 677 } 678 return (src instanceof JSpinner) ? (JSpinner)src : null; 679 } 680 actionPerformed(ActionEvent e)681 public void actionPerformed(ActionEvent e) { 682 JSpinner spinner = this.spinner; 683 684 if (!(e.getSource() instanceof javax.swing.Timer)) { 685 // Most likely resulting from being in ActionMap. 686 spinner = eventToSpinner(e); 687 if (e.getSource() instanceof JButton) { 688 arrowButton = (JButton)e.getSource(); 689 } 690 } else { 691 if (arrowButton!=null && !arrowButton.getModel().isPressed() 692 && autoRepeatTimer.isRunning()) { 693 autoRepeatTimer.stop(); 694 spinner = null; 695 arrowButton = null; 696 } 697 } 698 if (spinner != null) { 699 try { 700 int calendarField = getCalendarField(spinner); 701 spinner.commitEdit(); 702 if (calendarField != -1) { 703 ((SpinnerDateModel)spinner.getModel()). 704 setCalendarField(calendarField); 705 } 706 Object value = (isNext) ? spinner.getNextValue() : 707 spinner.getPreviousValue(); 708 if (value != null) { 709 spinner.setValue(value); 710 select(spinner); 711 } 712 } catch (IllegalArgumentException iae) { 713 UIManager.getLookAndFeel().provideErrorFeedback(spinner); 714 } catch (ParseException pe) { 715 UIManager.getLookAndFeel().provideErrorFeedback(spinner); 716 } 717 } 718 } 719 720 /** 721 * If the spinner's editor is a DateEditor, this selects the field 722 * associated with the value that is being incremented. 723 */ select(JSpinner spinner)724 private void select(JSpinner spinner) { 725 JComponent editor = spinner.getEditor(); 726 727 if (editor instanceof JSpinner.DateEditor) { 728 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor; 729 JFormattedTextField ftf = dateEditor.getTextField(); 730 Format format = dateEditor.getFormat(); 731 Object value; 732 733 if (format != null && (value = spinner.getValue()) != null) { 734 SpinnerDateModel model = dateEditor.getModel(); 735 DateFormat.Field field = DateFormat.Field.ofCalendarField( 736 model.getCalendarField()); 737 738 if (field != null) { 739 try { 740 AttributedCharacterIterator iterator = format. 741 formatToCharacterIterator(value); 742 if (!select(ftf, iterator, field) && 743 field == DateFormat.Field.HOUR0) { 744 select(ftf, iterator, DateFormat.Field.HOUR1); 745 } 746 } 747 catch (IllegalArgumentException iae) {} 748 } 749 } 750 } 751 } 752 753 /** 754 * Selects the passed in field, returning true if it is found, 755 * false otherwise. 756 */ select(JFormattedTextField ftf, AttributedCharacterIterator iterator, DateFormat.Field field)757 private boolean select(JFormattedTextField ftf, 758 AttributedCharacterIterator iterator, 759 DateFormat.Field field) { 760 int max = ftf.getDocument().getLength(); 761 762 iterator.first(); 763 do { 764 Map<?, ?> attrs = iterator.getAttributes(); 765 766 if (attrs != null && attrs.containsKey(field)){ 767 int start = iterator.getRunStart(field); 768 int end = iterator.getRunLimit(field); 769 770 if (start != -1 && end != -1 && start <= max && 771 end <= max) { 772 ftf.select(start, end); 773 } 774 return true; 775 } 776 } while (iterator.next() != CharacterIterator.DONE); 777 return false; 778 } 779 780 /** 781 * Returns the calendarField under the start of the selection, or 782 * -1 if there is no valid calendar field under the selection (or 783 * the spinner isn't editing dates. 784 */ getCalendarField(JSpinner spinner)785 private int getCalendarField(JSpinner spinner) { 786 JComponent editor = spinner.getEditor(); 787 788 if (editor instanceof JSpinner.DateEditor) { 789 JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor; 790 JFormattedTextField ftf = dateEditor.getTextField(); 791 int start = ftf.getSelectionStart(); 792 JFormattedTextField.AbstractFormatter formatter = 793 ftf.getFormatter(); 794 795 if (formatter instanceof InternationalFormatter) { 796 Format.Field[] fields = ((InternationalFormatter) 797 formatter).getFields(start); 798 799 for (int counter = 0; counter < fields.length; counter++) { 800 if (fields[counter] instanceof DateFormat.Field) { 801 int calendarField; 802 803 if (fields[counter] == DateFormat.Field.HOUR1) { 804 calendarField = Calendar.HOUR; 805 } 806 else { 807 calendarField = ((DateFormat.Field) 808 fields[counter]).getCalendarField(); 809 } 810 if (calendarField != -1) { 811 return calendarField; 812 } 813 } 814 } 815 } 816 } 817 return -1; 818 } 819 mousePressed(MouseEvent e)820 public void mousePressed(MouseEvent e) { 821 if (SwingUtilities.isLeftMouseButton(e) && e.getComponent().isEnabled()) { 822 spinner = eventToSpinner(e); 823 autoRepeatTimer.start(); 824 825 focusSpinnerIfNecessary(); 826 } 827 } 828 mouseReleased(MouseEvent e)829 public void mouseReleased(MouseEvent e) { 830 autoRepeatTimer.stop(); 831 arrowButton = null; 832 spinner = null; 833 } 834 mouseClicked(MouseEvent e)835 public void mouseClicked(MouseEvent e) { 836 } 837 mouseEntered(MouseEvent e)838 public void mouseEntered(MouseEvent e) { 839 if (spinner != null && !autoRepeatTimer.isRunning() && spinner == eventToSpinner(e)) { 840 autoRepeatTimer.start(); 841 } 842 } 843 mouseExited(MouseEvent e)844 public void mouseExited(MouseEvent e) { 845 if (autoRepeatTimer.isRunning()) { 846 autoRepeatTimer.stop(); 847 } 848 } 849 850 /** 851 * Requests focus on a child of the spinner if the spinner doesn't 852 * have focus. 853 */ focusSpinnerIfNecessary()854 private void focusSpinnerIfNecessary() { 855 Component fo = KeyboardFocusManager. 856 getCurrentKeyboardFocusManager().getFocusOwner(); 857 if (spinner.isRequestFocusEnabled() && ( 858 fo == null || 859 !SwingUtilities.isDescendingFrom(fo, spinner))) { 860 Container root = spinner; 861 862 if (!root.isFocusCycleRoot()) { 863 root = root.getFocusCycleRootAncestor(); 864 } 865 if (root != null) { 866 FocusTraversalPolicy ftp = root.getFocusTraversalPolicy(); 867 Component child = ftp.getComponentAfter(root, spinner); 868 869 if (child != null && SwingUtilities.isDescendingFrom( 870 child, spinner)) { 871 child.requestFocus(); 872 } 873 } 874 } 875 } 876 focusGained(FocusEvent e)877 public void focusGained(FocusEvent e) { 878 } 879 focusLost(FocusEvent e)880 public void focusLost(FocusEvent e) { 881 if (spinner == eventToSpinner(e)) { 882 if (autoRepeatTimer.isRunning()) { 883 autoRepeatTimer.stop(); 884 } 885 spinner = null; 886 if (arrowButton != null) { 887 ButtonModel model = arrowButton.getModel(); 888 model.setPressed(false); 889 model.setArmed(false); 890 arrowButton = null; 891 } 892 } 893 } 894 } 895 896 897 private static class Handler implements LayoutManager, 898 PropertyChangeListener, ChangeListener { 899 // 900 // LayoutManager 901 // 902 private Component nextButton = null; 903 private Component previousButton = null; 904 private Component editor = null; 905 addLayoutComponent(String name, Component c)906 public void addLayoutComponent(String name, Component c) { 907 if ("Next".equals(name)) { 908 nextButton = c; 909 } 910 else if ("Previous".equals(name)) { 911 previousButton = c; 912 } 913 else if ("Editor".equals(name)) { 914 editor = c; 915 } 916 } 917 removeLayoutComponent(Component c)918 public void removeLayoutComponent(Component c) { 919 if (c == nextButton) { 920 nextButton = null; 921 } 922 else if (c == previousButton) { 923 previousButton = null; 924 } 925 else if (c == editor) { 926 editor = null; 927 } 928 } 929 preferredSize(Component c)930 private Dimension preferredSize(Component c) { 931 return (c == null) ? zeroSize : c.getPreferredSize(); 932 } 933 preferredLayoutSize(Container parent)934 public Dimension preferredLayoutSize(Container parent) { 935 Dimension nextD = preferredSize(nextButton); 936 Dimension previousD = preferredSize(previousButton); 937 Dimension editorD = preferredSize(editor); 938 939 /* Force the editors height to be a multiple of 2 940 */ 941 editorD.height = ((editorD.height + 1) / 2) * 2; 942 943 Dimension size = new Dimension(editorD.width, editorD.height); 944 size.width += Math.max(nextD.width, previousD.width); 945 Insets insets = parent.getInsets(); 946 size.width += insets.left + insets.right; 947 size.height += insets.top + insets.bottom; 948 return size; 949 } 950 minimumLayoutSize(Container parent)951 public Dimension minimumLayoutSize(Container parent) { 952 return preferredLayoutSize(parent); 953 } 954 setBounds(Component c, int x, int y, int width, int height)955 private void setBounds(Component c, int x, int y, int width, int height) { 956 if (c != null) { 957 c.setBounds(x, y, width, height); 958 } 959 } 960 layoutContainer(Container parent)961 public void layoutContainer(Container parent) { 962 int width = parent.getWidth(); 963 int height = parent.getHeight(); 964 965 Insets insets = parent.getInsets(); 966 967 if (nextButton == null && previousButton == null) { 968 setBounds(editor, insets.left, insets.top, width - insets.left - insets.right, 969 height - insets.top - insets.bottom); 970 971 return; 972 } 973 974 Dimension nextD = preferredSize(nextButton); 975 Dimension previousD = preferredSize(previousButton); 976 int buttonsWidth = Math.max(nextD.width, previousD.width); 977 int editorHeight = height - (insets.top + insets.bottom); 978 979 // The arrowButtonInsets value is used instead of the JSpinner's 980 // insets if not null. Defining this to be (0, 0, 0, 0) causes the 981 // buttons to be aligned with the outer edge of the spinner's 982 // border, and leaving it as "null" places the buttons completely 983 // inside the spinner's border. 984 Insets buttonInsets = UIManager.getInsets("Spinner.arrowButtonInsets"); 985 if (buttonInsets == null) { 986 buttonInsets = insets; 987 } 988 989 /* Deal with the spinner's componentOrientation property. 990 */ 991 int editorX, editorWidth, buttonsX; 992 if (parent.getComponentOrientation().isLeftToRight()) { 993 editorX = insets.left; 994 editorWidth = width - insets.left - buttonsWidth - buttonInsets.right; 995 buttonsX = width - buttonsWidth - buttonInsets.right; 996 } else { 997 buttonsX = buttonInsets.left; 998 editorX = buttonsX + buttonsWidth; 999 editorWidth = width - buttonInsets.left - buttonsWidth - insets.right; 1000 } 1001 1002 int nextY = buttonInsets.top; 1003 int nextHeight = (height / 2) + (height % 2) - nextY; 1004 int previousY = buttonInsets.top + nextHeight; 1005 int previousHeight = height - previousY - buttonInsets.bottom; 1006 1007 setBounds(editor, editorX, insets.top, editorWidth, editorHeight); 1008 setBounds(nextButton, buttonsX, nextY, buttonsWidth, nextHeight); 1009 setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight); 1010 } 1011 1012 1013 // 1014 // PropertyChangeListener 1015 // propertyChange(PropertyChangeEvent e)1016 public void propertyChange(PropertyChangeEvent e) 1017 { 1018 String propertyName = e.getPropertyName(); 1019 if (e.getSource() instanceof JSpinner) { 1020 JSpinner spinner = (JSpinner)(e.getSource()); 1021 SpinnerUI spinnerUI = spinner.getUI(); 1022 1023 if (spinnerUI instanceof BasicSpinnerUI) { 1024 BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI; 1025 1026 if ("editor".equals(propertyName)) { 1027 JComponent oldEditor = (JComponent)e.getOldValue(); 1028 JComponent newEditor = (JComponent)e.getNewValue(); 1029 ui.replaceEditor(oldEditor, newEditor); 1030 ui.updateEnabledState(); 1031 if (oldEditor instanceof JSpinner.DefaultEditor) { 1032 JTextField tf = 1033 ((JSpinner.DefaultEditor)oldEditor).getTextField(); 1034 if (tf != null) { 1035 tf.removeFocusListener(nextButtonHandler); 1036 tf.removeFocusListener(previousButtonHandler); 1037 } 1038 } 1039 if (newEditor instanceof JSpinner.DefaultEditor) { 1040 JTextField tf = 1041 ((JSpinner.DefaultEditor)newEditor).getTextField(); 1042 if (tf != null) { 1043 if (tf.getFont() instanceof UIResource) { 1044 Font font = spinner.getFont(); 1045 tf.setFont(font == null ? null : new FontUIResource(font)); 1046 } 1047 tf.addFocusListener(nextButtonHandler); 1048 tf.addFocusListener(previousButtonHandler); 1049 } 1050 } 1051 } 1052 else if ("enabled".equals(propertyName) || 1053 "model".equals(propertyName)) { 1054 ui.updateEnabledState(); 1055 } 1056 else if ("font".equals(propertyName)) { 1057 JComponent editor = spinner.getEditor(); 1058 if (editor instanceof JSpinner.DefaultEditor) { 1059 JTextField tf = 1060 ((JSpinner.DefaultEditor)editor).getTextField(); 1061 if (tf != null) { 1062 if (tf.getFont() instanceof UIResource) { 1063 Font font = spinner.getFont(); 1064 tf.setFont(font == null ? null : new FontUIResource(font)); 1065 } 1066 } 1067 } 1068 } 1069 else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) { 1070 updateToolTipTextForChildren(spinner); 1071 } else if ("componentOrientation".equals(propertyName)) { 1072 ComponentOrientation o 1073 = (ComponentOrientation) e.getNewValue(); 1074 if (o != (ComponentOrientation) e.getOldValue()) { 1075 JComponent editor = spinner.getEditor(); 1076 if (editor != null) { 1077 editor.applyComponentOrientation(o); 1078 } 1079 spinner.revalidate(); 1080 spinner.repaint(); 1081 } 1082 } 1083 } 1084 } else if (e.getSource() instanceof JComponent) { 1085 JComponent c = (JComponent)e.getSource(); 1086 if ((c.getParent() instanceof JPanel) && 1087 (c.getParent().getParent() instanceof JSpinner) && 1088 "border".equals(propertyName)) { 1089 1090 JSpinner spinner = (JSpinner)c.getParent().getParent(); 1091 SpinnerUI spinnerUI = spinner.getUI(); 1092 if (spinnerUI instanceof BasicSpinnerUI) { 1093 BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI; 1094 ui.maybeRemoveEditorBorder(c); 1095 } 1096 } 1097 } 1098 } 1099 1100 // Syncronizes the ToolTip text for the components within the spinner 1101 // to be the same value as the spinner ToolTip text. updateToolTipTextForChildren(JComponent spinner)1102 private void updateToolTipTextForChildren(JComponent spinner) { 1103 String toolTipText = spinner.getToolTipText(); 1104 Component[] children = spinner.getComponents(); 1105 for (int i = 0; i < children.length; i++) { 1106 if (children[i] instanceof JSpinner.DefaultEditor) { 1107 JTextField tf = ((JSpinner.DefaultEditor)children[i]).getTextField(); 1108 if (tf != null) { 1109 tf.setToolTipText(toolTipText); 1110 } 1111 } else if (children[i] instanceof JComponent) { 1112 ((JComponent)children[i]).setToolTipText( spinner.getToolTipText() ); 1113 } 1114 } 1115 } 1116 stateChanged(ChangeEvent e)1117 public void stateChanged(ChangeEvent e) { 1118 if (e.getSource() instanceof JSpinner) { 1119 JSpinner spinner = (JSpinner)e.getSource(); 1120 SpinnerUI spinnerUI = spinner.getUI(); 1121 if (DefaultLookup.getBoolean(spinner, spinnerUI, 1122 "Spinner.disableOnBoundaryValues", false) && 1123 spinnerUI instanceof BasicSpinnerUI) { 1124 BasicSpinnerUI ui = (BasicSpinnerUI)spinnerUI; 1125 ui.updateEnabledState(); 1126 } 1127 } 1128 } 1129 } 1130 } 1131