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