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