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