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