1 /* 2 * Copyright (c) 2011, 2014, 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 com.apple.laf; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 import java.beans.*; 31 import java.text.*; 32 import java.text.AttributedCharacterIterator.Attribute; 33 import java.text.Format.Field; 34 import java.util.*; 35 36 import javax.swing.*; 37 import javax.swing.JSpinner.DefaultEditor; 38 import javax.swing.plaf.*; 39 import javax.swing.text.InternationalFormatter; 40 41 import apple.laf.*; 42 import apple.laf.JRSUIConstants.*; 43 44 import com.apple.laf.AquaUtils.RecyclableSingleton; 45 import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor; 46 47 /** 48 * This is originally derived from BasicSpinnerUI, but they made everything private 49 * so we can't subclass! 50 */ 51 public class AquaSpinnerUI extends SpinnerUI { 52 private static final RecyclableSingleton<? extends PropertyChangeListener> propertyChangeListener = new RecyclableSingletonFromDefaultConstructor<PropertyChangeHandler>(PropertyChangeHandler.class); 53 static PropertyChangeListener getPropertyChangeListener() { 54 return propertyChangeListener.get(); 55 } 56 57 private static final RecyclableSingleton<ArrowButtonHandler> nextButtonHandler = new RecyclableSingleton<ArrowButtonHandler>() { 58 @Override 59 protected ArrowButtonHandler getInstance() { 60 return new ArrowButtonHandler("increment", true); 61 } 62 }; 63 static ArrowButtonHandler getNextButtonHandler() { 64 return nextButtonHandler.get(); 65 } 66 private static final RecyclableSingleton<ArrowButtonHandler> previousButtonHandler = new RecyclableSingleton<ArrowButtonHandler>() { 67 @Override 68 protected ArrowButtonHandler getInstance() { 69 return new ArrowButtonHandler("decrement", false); 70 } 71 }; 72 static ArrowButtonHandler getPreviousButtonHandler() { 73 return previousButtonHandler.get(); 74 } 75 76 JSpinner spinner; 77 SpinPainter spinPainter; 78 79 public static ComponentUI createUI(final JComponent c) { 80 return new AquaSpinnerUI(); 81 } 82 83 private void maybeAdd(final Component c, final String s) { 84 if (c != null) { 85 spinner.add(c, s); 86 } 87 } 88 89 boolean wasOpaque; 90 public void installUI(final JComponent c) { 91 this.spinner = (JSpinner)c; 92 installDefaults(); 93 installListeners(); 94 final TransparentButton next = createNextButton(); 95 final TransparentButton prev = createPreviousButton(); 96 spinPainter = new SpinPainter(next, prev); 97 98 maybeAdd(next, "Next"); 99 maybeAdd(prev, "Previous"); 100 maybeAdd(createEditor(), "Editor"); 101 maybeAdd(spinPainter, "Painter"); 102 103 updateEnabledState(); 104 installKeyboardActions(); 105 106 // this doesn't work because JSpinner calls setOpaque(true) directly in it's constructor 107 // LookAndFeel.installProperty(spinner, "opaque", Boolean.FALSE); 108 109 // ...so we have to handle the is/was opaque ourselves 110 wasOpaque = spinner.isOpaque(); 111 spinner.setOpaque(false); 112 } 113 114 public void uninstallUI(final JComponent c) { 115 uninstallDefaults(); 116 uninstallListeners(); 117 spinner.setOpaque(wasOpaque); 118 spinner = null; 119 c.removeAll(); 120 } 121 122 protected void installListeners() { 123 spinner.addPropertyChangeListener(getPropertyChangeListener()); 124 } 125 126 protected void uninstallListeners() { 127 spinner.removePropertyChangeListener(getPropertyChangeListener()); 128 } 129 130 protected void installDefaults() { 131 spinner.setLayout(createLayout()); 132 LookAndFeel.installBorder(spinner, "Spinner.border"); 133 LookAndFeel.installColorsAndFont(spinner, "Spinner.background", "Spinner.foreground", "Spinner.font"); 134 } 135 136 protected void uninstallDefaults() { 137 spinner.setLayout(null); 138 } 139 140 protected LayoutManager createLayout() { 141 return new SpinnerLayout(); 142 } 143 144 protected PropertyChangeListener createPropertyChangeListener() { 145 return new PropertyChangeHandler(); 146 } 147 148 protected TransparentButton createPreviousButton() { 149 final TransparentButton b = new TransparentButton(); 150 b.addActionListener(getPreviousButtonHandler()); 151 b.addMouseListener(getPreviousButtonHandler()); 152 b.setInheritsPopupMenu(true); 153 return b; 154 } 155 156 protected TransparentButton createNextButton() { 157 final TransparentButton b = new TransparentButton(); 158 b.addActionListener(getNextButtonHandler()); 159 b.addMouseListener(getNextButtonHandler()); 160 b.setInheritsPopupMenu(true); 161 return b; 162 } 163 164 /** 165 * {@inheritDoc} 166 */ 167 public int getBaseline(JComponent c, int width, int height) { 168 super.getBaseline(c, width, height); 169 JComponent editor = spinner.getEditor(); 170 Insets insets = spinner.getInsets(); 171 width = width - insets.left - insets.right; 172 height = height - insets.top - insets.bottom; 173 if (width >= 0 && height >= 0) { 174 int baseline = editor.getBaseline(width, height); 175 if (baseline >= 0) { 176 return insets.top + baseline; 177 } 178 } 179 return -1; 180 } 181 182 /** 183 * {@inheritDoc} 184 */ 185 public Component.BaselineResizeBehavior getBaselineResizeBehavior( 186 JComponent c) { 187 super.getBaselineResizeBehavior(c); 188 return spinner.getEditor().getBaselineResizeBehavior(); 189 } 190 191 @SuppressWarnings("serial") // Superclass is not serializable across versions 192 class TransparentButton extends JButton implements SwingConstants { 193 boolean interceptRepaints = false; 194 195 public TransparentButton() { 196 super(); 197 setFocusable(false); 198 // only intercept repaints if we are after this has been initialized 199 // otherwise we can't talk to our containing class 200 interceptRepaints = true; 201 } 202 203 public void paint(final Graphics g) {} 204 205 public void repaint() { 206 // only intercept repaints if we are after this has been initialized 207 // otherwise we can't talk to our containing class 208 if (interceptRepaints) { 209 if (spinPainter == null) return; 210 spinPainter.repaint(); 211 } 212 super.repaint(); 213 } 214 } 215 216 protected JComponent createEditor() { 217 final JComponent editor = spinner.getEditor(); 218 fixupEditor(editor); 219 return editor; 220 } 221 222 protected void replaceEditor(final JComponent oldEditor, final JComponent newEditor) { 223 spinner.remove(oldEditor); 224 fixupEditor(newEditor); 225 spinner.add(newEditor, "Editor"); 226 } 227 228 protected void fixupEditor(final JComponent editor) { 229 if (!(editor instanceof DefaultEditor)) return; 230 231 editor.setOpaque(false); 232 editor.setInheritsPopupMenu(true); 233 234 if (editor.getFont() instanceof UIResource) { 235 editor.setFont(spinner.getFont()); 236 } 237 238 final JFormattedTextField editorTextField = ((DefaultEditor)editor).getTextField(); 239 if (editorTextField.getFont() instanceof UIResource) { 240 editorTextField.setFont(spinner.getFont()); 241 } 242 final InputMap spinnerInputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 243 final InputMap editorInputMap = editorTextField.getInputMap(); 244 final KeyStroke[] keys = spinnerInputMap.keys(); 245 for (final KeyStroke k : keys) { 246 editorInputMap.put(k, spinnerInputMap.get(k)); 247 } 248 } 249 250 void updateEnabledState() { 251 updateEnabledState(spinner, spinner.isEnabled()); 252 } 253 254 private void updateEnabledState(final Container c, final boolean enabled) { 255 for (int counter = c.getComponentCount() - 1; counter >= 0; counter--) { 256 final Component child = c.getComponent(counter); 257 258 child.setEnabled(enabled); 259 if (child instanceof Container) { 260 updateEnabledState((Container)child, enabled); 261 } 262 } 263 } 264 265 private void installKeyboardActions() { 266 final InputMap iMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); 267 SwingUtilities.replaceUIInputMap(spinner, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, iMap); 268 SwingUtilities.replaceUIActionMap(spinner, getActionMap()); 269 } 270 271 private InputMap getInputMap(final int condition) { 272 if (condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) { 273 return (InputMap)UIManager.get("Spinner.ancestorInputMap"); 274 } 275 return null; 276 } 277 278 private ActionMap getActionMap() { 279 ActionMap map = (ActionMap)UIManager.get("Spinner.actionMap"); 280 281 if (map == null) { 282 map = createActionMap(); 283 if (map != null) { 284 UIManager.getLookAndFeelDefaults().put("Spinner.actionMap", map); 285 } 286 } 287 return map; 288 } 289 290 private ActionMap createActionMap() { 291 final ActionMap map = new ActionMapUIResource(); 292 map.put("increment", getNextButtonHandler()); 293 map.put("decrement", getPreviousButtonHandler()); 294 return map; 295 } 296 297 @SuppressWarnings("serial") // Superclass is not serializable across versions 298 private static class ArrowButtonHandler extends AbstractAction implements MouseListener { 299 final javax.swing.Timer autoRepeatTimer; 300 final boolean isNext; 301 JSpinner spinner = null; 302 303 ArrowButtonHandler(final String name, final boolean isNext) { 304 super(name); 305 this.isNext = isNext; 306 autoRepeatTimer = new javax.swing.Timer(60, this); 307 autoRepeatTimer.setInitialDelay(300); 308 } 309 310 private JSpinner eventToSpinner(final AWTEvent e) { 311 Object src = e.getSource(); 312 while ((src instanceof Component) && !(src instanceof JSpinner)) { 313 src = ((Component)src).getParent(); 314 } 315 return (src instanceof JSpinner) ? (JSpinner)src : null; 316 } 317 318 public void actionPerformed(final ActionEvent e) { 319 if (!(e.getSource() instanceof javax.swing.Timer)) { 320 // Most likely resulting from being in ActionMap. 321 spinner = eventToSpinner(e); 322 } 323 324 if (spinner == null) return; 325 326 try { 327 final int calendarField = getCalendarField(spinner); 328 spinner.commitEdit(); 329 if (calendarField != -1) { 330 ((SpinnerDateModel)spinner.getModel()).setCalendarField(calendarField); 331 } 332 final Object value = (isNext) ? spinner.getNextValue() : spinner.getPreviousValue(); 333 if (value != null) { 334 spinner.setValue(value); 335 select(spinner); 336 } 337 } catch (final IllegalArgumentException iae) { 338 UIManager.getLookAndFeel().provideErrorFeedback(spinner); 339 } catch (final ParseException pe) { 340 UIManager.getLookAndFeel().provideErrorFeedback(spinner); 341 } 342 } 343 344 /** 345 * If the spinner's editor is a DateEditor, this selects the field 346 * associated with the value that is being incremented. 347 */ 348 private void select(final JSpinner spinnerComponent) { 349 final JComponent editor = spinnerComponent.getEditor(); 350 if (!(editor instanceof JSpinner.DateEditor)) return; 351 352 final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor; 353 final JFormattedTextField ftf = dateEditor.getTextField(); 354 final Format format = dateEditor.getFormat(); 355 Object value; 356 if (format == null || (value = spinnerComponent.getValue()) == null) return; 357 358 final SpinnerDateModel model = dateEditor.getModel(); 359 final DateFormat.Field field = DateFormat.Field.ofCalendarField(model.getCalendarField()); 360 if (field == null) return; 361 362 try { 363 final AttributedCharacterIterator iterator = format.formatToCharacterIterator(value); 364 if (!select(ftf, iterator, field) && field == DateFormat.Field.HOUR0) { 365 select(ftf, iterator, DateFormat.Field.HOUR1); 366 } 367 } catch (final IllegalArgumentException iae) {} 368 } 369 370 /** 371 * Selects the passed in field, returning true if it is found, 372 * false otherwise. 373 */ 374 private boolean select(final JFormattedTextField ftf, final AttributedCharacterIterator iterator, final DateFormat.Field field) { 375 final int max = ftf.getDocument().getLength(); 376 377 iterator.first(); 378 do { 379 final Map<Attribute,Object> attrs = iterator.getAttributes(); 380 if (attrs == null || !attrs.containsKey(field)) continue; 381 382 final int start = iterator.getRunStart(field); 383 final int end = iterator.getRunLimit(field); 384 if (start != -1 && end != -1 && start <= max && end <= max) { 385 ftf.select(start, end); 386 } 387 388 return true; 389 } while (iterator.next() != CharacterIterator.DONE); 390 return false; 391 } 392 393 /** 394 * Returns the calendarField under the start of the selection, or 395 * -1 if there is no valid calendar field under the selection (or 396 * the spinner isn't editing dates. 397 */ 398 private int getCalendarField(final JSpinner spinnerComponent) { 399 final JComponent editor = spinnerComponent.getEditor(); 400 if (!(editor instanceof JSpinner.DateEditor)) return -1; 401 402 final JSpinner.DateEditor dateEditor = (JSpinner.DateEditor)editor; 403 final JFormattedTextField ftf = dateEditor.getTextField(); 404 final int start = ftf.getSelectionStart(); 405 final JFormattedTextField.AbstractFormatter formatter = ftf.getFormatter(); 406 if (!(formatter instanceof InternationalFormatter)) return -1; 407 408 final Format.Field[] fields = ((InternationalFormatter)formatter).getFields(start); 409 for (final Field element : fields) { 410 if (!(element instanceof DateFormat.Field)) continue; 411 int calendarField; 412 413 if (element == DateFormat.Field.HOUR1) { 414 calendarField = Calendar.HOUR; 415 } else { 416 calendarField = ((DateFormat.Field)element).getCalendarField(); 417 } 418 419 if (calendarField != -1) { 420 return calendarField; 421 } 422 } 423 return -1; 424 } 425 426 public void mousePressed(final MouseEvent e) { 427 if (!SwingUtilities.isLeftMouseButton(e) || !e.getComponent().isEnabled()) return; 428 spinner = eventToSpinner(e); 429 autoRepeatTimer.start(); 430 431 focusSpinnerIfNecessary(); 432 } 433 434 public void mouseReleased(final MouseEvent e) { 435 autoRepeatTimer.stop(); 436 spinner = null; 437 } 438 439 public void mouseClicked(final MouseEvent e) {} 440 public void mouseEntered(final MouseEvent e) {} 441 public void mouseExited(final MouseEvent e) {} 442 443 /** 444 * Requests focus on a child of the spinner if the spinner doesn't 445 * have focus. 446 */ 447 private void focusSpinnerIfNecessary() { 448 final Component fo = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); 449 if (!spinner.isRequestFocusEnabled() || (fo != null && (SwingUtilities.isDescendingFrom(fo, spinner)))) return; 450 Container root = spinner; 451 452 if (!root.isFocusCycleRoot()) { 453 root = root.getFocusCycleRootAncestor(); 454 } 455 456 if (root == null) return; 457 final FocusTraversalPolicy ftp = root.getFocusTraversalPolicy(); 458 final Component child = ftp.getComponentAfter(root, spinner); 459 460 if (child != null && SwingUtilities.isDescendingFrom(child, spinner)) { 461 child.requestFocus(); 462 } 463 } 464 } 465 466 @SuppressWarnings("serial") // Superclass is not serializable across versions 467 class SpinPainter extends JComponent { 468 final AquaPainter<JRSUIState> painter = AquaPainter.create(JRSUIStateFactory.getSpinnerArrows()); 469 470 ButtonModel fTopModel; 471 ButtonModel fBottomModel; 472 473 boolean fPressed = false; 474 boolean fTopPressed = false; 475 476 Dimension kPreferredSize = new Dimension(15, 24); // 19,27 before trimming 477 478 public SpinPainter(final AbstractButton top, final AbstractButton bottom) { 479 if (top != null) { 480 fTopModel = top.getModel(); 481 } 482 483 if (bottom != null) { 484 fBottomModel = bottom.getModel(); 485 } 486 } 487 488 public void paint(final Graphics g) { 489 if (spinner.isOpaque()) { 490 g.setColor(spinner.getBackground()); 491 g.fillRect(0, 0, getWidth(), getHeight()); 492 } 493 494 AquaUtilControlSize.applySizeForControl(spinner, painter); 495 496 if (isEnabled()) { 497 if (fTopModel != null && fTopModel.isPressed()) { 498 painter.state.set(State.PRESSED); 499 painter.state.set(BooleanValue.NO); 500 } else if (fBottomModel != null && fBottomModel.isPressed()) { 501 painter.state.set(State.PRESSED); 502 painter.state.set(BooleanValue.YES); 503 } else { 504 painter.state.set(State.ACTIVE); 505 } 506 } else { 507 painter.state.set(State.DISABLED); 508 } 509 510 final Rectangle bounds = getBounds(); 511 painter.paint(g, spinner, 0, 0, bounds.width, bounds.height); 512 } 513 514 public Dimension getPreferredSize() { 515 final Size size = AquaUtilControlSize.getUserSizeFrom(this); 516 517 if (size == Size.MINI) { 518 return new Dimension(kPreferredSize.width, kPreferredSize.height - 8); 519 } 520 521 return kPreferredSize; 522 } 523 } 524 525 /** 526 * A simple layout manager for the editor and the next/previous buttons. 527 * See the AquaSpinnerUI javadoc for more information about exactly 528 * how the components are arranged. 529 */ 530 static class SpinnerLayout implements LayoutManager { 531 private Component nextButton = null; 532 private Component previousButton = null; 533 private Component editor = null; 534 private Component painter = null; 535 536 public void addLayoutComponent(final String name, final Component c) { 537 if ("Next".equals(name)) { 538 nextButton = c; 539 } else if ("Previous".equals(name)) { 540 previousButton = c; 541 } else if ("Editor".equals(name)) { 542 editor = c; 543 } else if ("Painter".equals(name)) { 544 painter = c; 545 } 546 } 547 548 public void removeLayoutComponent(Component c) { 549 if (c == nextButton) { 550 c = null; 551 } else if (c == previousButton) { 552 previousButton = null; 553 } else if (c == editor) { 554 editor = null; 555 } else if (c == painter) { 556 painter = null; 557 } 558 } 559 560 private Dimension preferredSize(final Component c) { 561 return (c == null) ? new Dimension(0, 0) : c.getPreferredSize(); 562 } 563 564 public Dimension preferredLayoutSize(final Container parent) { 565 // Dimension nextD = preferredSize(nextButton); 566 // Dimension previousD = preferredSize(previousButton); 567 final Dimension editorD = preferredSize(editor); 568 final Dimension painterD = preferredSize(painter); 569 570 /* Force the editors height to be a multiple of 2 571 */ 572 editorD.height = ((editorD.height + 1) / 2) * 2; 573 574 final Dimension size = new Dimension(editorD.width, Math.max(painterD.height, editorD.height)); 575 size.width += painterD.width; //Math.max(nextD.width, previousD.width); 576 final Insets insets = parent.getInsets(); 577 size.width += insets.left + insets.right; 578 size.height += insets.top + insets.bottom; 579 return size; 580 } 581 582 public Dimension minimumLayoutSize(final Container parent) { 583 return preferredLayoutSize(parent); 584 } 585 586 private void setBounds(final Component c, final int x, final int y, final int width, final int height) { 587 if (c != null) { 588 c.setBounds(x, y, width, height); 589 } 590 } 591 592 public void layoutContainer(final Container parent) { 593 final Insets insets = parent.getInsets(); 594 final int availWidth = parent.getWidth() - (insets.left + insets.right); 595 final int availHeight = parent.getHeight() - (insets.top + insets.bottom); 596 597 final Dimension painterD = preferredSize(painter); 598 // Dimension nextD = preferredSize(nextButton); 599 // Dimension previousD = preferredSize(previousButton); 600 final int nextHeight = availHeight / 2; 601 final int previousHeight = availHeight - nextHeight; 602 final int buttonsWidth = painterD.width; //Math.max(nextD.width, previousD.width); 603 final int editorWidth = availWidth - buttonsWidth; 604 605 /* Deal with the spinners componentOrientation property. 606 */ 607 int editorX, buttonsX; 608 if (parent.getComponentOrientation().isLeftToRight()) { 609 editorX = insets.left; 610 buttonsX = editorX + editorWidth; 611 } else { 612 buttonsX = insets.left; 613 editorX = buttonsX + buttonsWidth; 614 } 615 616 final int previousY = insets.top + nextHeight; 617 final int painterTop = previousY - (painterD.height / 2); 618 setBounds(editor, editorX, insets.top, editorWidth, availHeight); 619 setBounds(nextButton, buttonsX, insets.top, buttonsWidth, nextHeight); 620 setBounds(previousButton, buttonsX, previousY, buttonsWidth, previousHeight); 621 setBounds(painter, buttonsX, painterTop, buttonsWidth, painterD.height); 622 } 623 } 624 625 /** 626 * Detect JSpinner property changes we're interested in and delegate. Subclasses 627 * shouldn't need to replace the default propertyChangeListener (although they 628 * can by overriding createPropertyChangeListener) since all of the interesting 629 * property changes are delegated to protected methods. 630 */ 631 static class PropertyChangeHandler implements PropertyChangeListener { 632 public void propertyChange(final PropertyChangeEvent e) { 633 final String propertyName = e.getPropertyName(); 634 final JSpinner spinner = (JSpinner)(e.getSource()); 635 final SpinnerUI spinnerUI = spinner.getUI(); 636 637 if (spinnerUI instanceof AquaSpinnerUI) { 638 final AquaSpinnerUI ui = (AquaSpinnerUI)spinnerUI; 639 640 if ("editor".equals(propertyName)) { 641 final JComponent oldEditor = (JComponent)e.getOldValue(); 642 final JComponent newEditor = (JComponent)e.getNewValue(); 643 ui.replaceEditor(oldEditor, newEditor); 644 ui.updateEnabledState(); 645 } else if ("enabled".equals(propertyName)) { 646 ui.updateEnabledState(); 647 } else if (JComponent.TOOL_TIP_TEXT_KEY.equals(propertyName)) { 648 ui.updateToolTipTextForChildren(spinner); 649 } else if ("font".equals(propertyName)) { 650 JComponent editor = spinner.getEditor(); 651 if (editor != null && editor instanceof JSpinner.DefaultEditor) { 652 JTextField tf = 653 ((JSpinner.DefaultEditor) editor).getTextField(); 654 if (tf != null) { 655 if (tf.getFont() instanceof UIResource) { 656 tf.setFont(spinner.getFont()); 657 } 658 } 659 } 660 } 661 } 662 } 663 } 664 665 // Syncronizes the ToolTip text for the components within the spinner 666 // to be the same value as the spinner ToolTip text. 667 void updateToolTipTextForChildren(final JComponent spinnerComponent) { 668 final String toolTipText = spinnerComponent.getToolTipText(); 669 final Component[] children = spinnerComponent.getComponents(); 670 for (final Component element : children) { 671 if (element instanceof JSpinner.DefaultEditor) { 672 final JTextField tf = ((JSpinner.DefaultEditor)element).getTextField(); 673 if (tf != null) { 674 tf.setToolTipText(toolTipText); 675 } 676 } else if (element instanceof JComponent) { 677 ((JComponent)element).setToolTipText(toolTipText); 678 } 679 } 680 } 681 }