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