001/* BasicSpinnerUI.java --
002   Copyright (C) 2003, 2004, 2005, 2006,  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.plaf.basic;
040
041import java.awt.Component;
042import java.awt.Container;
043import java.awt.Dimension;
044import java.awt.Insets;
045import java.awt.LayoutManager;
046import java.awt.event.ActionEvent;
047import java.awt.event.ActionListener;
048import java.awt.event.MouseAdapter;
049import java.awt.event.MouseEvent;
050import java.beans.PropertyChangeEvent;
051import java.beans.PropertyChangeListener;
052
053import javax.swing.JButton;
054import javax.swing.JComponent;
055import javax.swing.JSpinner;
056import javax.swing.LookAndFeel;
057import javax.swing.Timer;
058import javax.swing.plaf.ComponentUI;
059import javax.swing.plaf.SpinnerUI;
060
061/**
062 * A UI delegate for the {@link JSpinner} component.
063 *
064 * @author Ka-Hing Cheung
065 *
066 * @since 1.4
067 */
068public class BasicSpinnerUI extends SpinnerUI
069{
070  /**
071   * Creates a new <code>BasicSpinnerUI</code> for the specified
072   * <code>JComponent</code>
073   *
074   * @param c  the component (ignored).
075   *
076   * @return A new instance of {@link BasicSpinnerUI}.
077   */
078  public static ComponentUI createUI(JComponent c)
079  {
080    return new BasicSpinnerUI();
081  }
082
083  /**
084   * Creates an editor component. Really, it just returns
085   * <code>JSpinner.getEditor()</code>
086   *
087   * @return a JComponent as an editor
088   *
089   * @see javax.swing.JSpinner#getEditor
090   */
091  protected JComponent createEditor()
092  {
093    return spinner.getEditor();
094  }
095
096  /**
097   * Creates a <code>LayoutManager</code> that layouts the sub components. The
098   * subcomponents are identifies by the constraint "Next", "Previous" and
099   * "Editor"
100   *
101   * @return a LayoutManager
102   *
103   * @see java.awt.LayoutManager
104   */
105  protected LayoutManager createLayout()
106  {
107    return new DefaultLayoutManager();
108  }
109
110  /**
111   * Creates the "Next" button
112   *
113   * @return the next button component
114   */
115  protected Component createNextButton()
116  {
117    JButton button = new BasicArrowButton(BasicArrowButton.NORTH);
118    return button;
119  }
120
121  /**
122   * Creates the "Previous" button
123   *
124   * @return the previous button component
125   */
126  protected Component createPreviousButton()
127  {
128    JButton button = new BasicArrowButton(BasicArrowButton.SOUTH);
129    return button;
130  }
131
132  /**
133   * Creates the <code>PropertyChangeListener</code> that will be attached by
134   * <code>installListeners</code>. It should watch for the "editor"
135   * property, when it's changed, replace the old editor with the new one,
136   * probably by calling <code>replaceEditor</code>
137   *
138   * @return a PropertyChangeListener
139   *
140   * @see #replaceEditor
141   */
142  protected PropertyChangeListener createPropertyChangeListener()
143  {
144    return new PropertyChangeListener()
145      {
146        public void propertyChange(PropertyChangeEvent event)
147        {
148          // FIXME: Add check for enabled property change. Need to
149          // disable the buttons.
150          if ("editor".equals(event.getPropertyName()))
151            BasicSpinnerUI.this.replaceEditor((JComponent) event.getOldValue(),
152                (JComponent) event.getNewValue());
153          // FIXME: Handle 'font' property change
154        }
155      };
156  }
157
158  /**
159   * Called by <code>installUI</code>. This should set various defaults
160   * obtained from <code>UIManager.getLookAndFeelDefaults</code>, as well as
161   * set the layout obtained from <code>createLayout</code>
162   *
163   * @see javax.swing.UIManager#getLookAndFeelDefaults
164   * @see #createLayout
165   * @see #installUI
166   */
167  protected void installDefaults()
168  {
169    LookAndFeel.installColorsAndFont(spinner, "Spinner.background",
170                                     "Spinner.foreground", "Spinner.font");
171    LookAndFeel.installBorder(spinner, "Spinner.border");
172    JComponent e = spinner.getEditor();
173    if (e instanceof JSpinner.DefaultEditor)
174      {
175        JSpinner.DefaultEditor de = (JSpinner.DefaultEditor) e;
176        de.getTextField().setBorder(null);  
177      }
178    spinner.setLayout(createLayout());
179    spinner.setOpaque(true);
180  }
181
182  /*
183   * Called by <code>installUI</code>, which basically adds the
184   * <code>PropertyChangeListener</code> created by
185   * <code>createPropertyChangeListener</code>
186   *
187   * @see #createPropertyChangeListener
188   * @see #installUI
189   */
190  protected void installListeners()
191  {
192    spinner.addPropertyChangeListener(listener);
193  }
194
195  /*
196   * Install listeners to the next button so that it increments the model
197   */
198  protected void installNextButtonListeners(Component c)
199  {
200    c.addMouseListener(new MouseAdapter()
201        {
202          public void mousePressed(MouseEvent evt)
203          {
204            if (! spinner.isEnabled())
205              return;
206            increment();
207            timer.setInitialDelay(500);
208            timer.start();
209          }
210
211          public void mouseReleased(MouseEvent evt)
212          {
213            timer.stop();
214          }
215
216          void increment()
217          {
218            Object next = BasicSpinnerUI.this.spinner.getNextValue();
219            if (next != null)
220              BasicSpinnerUI.this.spinner.getModel().setValue(next);
221          }
222
223          volatile boolean mouseDown;
224          Timer timer = new Timer(50,
225                                  new ActionListener()
226              {
227                public void actionPerformed(ActionEvent event)
228                {
229                  increment();
230                }
231              });
232        });
233  }
234
235  /*
236   * Install listeners to the previous button so that it decrements the model
237   */
238  protected void installPreviousButtonListeners(Component c)
239  {
240    c.addMouseListener(new MouseAdapter()
241        {
242          public void mousePressed(MouseEvent evt)
243          {
244            if (! spinner.isEnabled())
245              return;
246            decrement();
247            timer.setInitialDelay(500);
248            timer.start();
249          }
250
251          public void mouseReleased(MouseEvent evt)
252          {
253            timer.stop();
254          }
255
256          void decrement()
257          {
258            Object prev = BasicSpinnerUI.this.spinner.getPreviousValue();
259            if (prev != null)
260              BasicSpinnerUI.this.spinner.getModel().setValue(prev);
261          }
262
263          volatile boolean mouseDown;
264          Timer timer = new Timer(50,
265                                  new ActionListener()
266              {
267                public void actionPerformed(ActionEvent event)
268                {
269                  decrement();
270                }
271              });
272        });
273  }
274
275  /**
276   * Install this UI to the <code>JComponent</code>, which in reality, is a
277   * <code>JSpinner</code>. Calls <code>installDefaults</code>,
278   * <code>installListeners</code>, and also adds the buttons and editor.
279   *
280   * @param c DOCUMENT ME!
281   *
282   * @see #installDefaults
283   * @see #installListeners
284   * @see #createNextButton
285   * @see #createPreviousButton
286   * @see #createEditor
287   */
288  public void installUI(JComponent c)
289  {
290    super.installUI(c);
291
292    spinner = (JSpinner) c;
293
294    installDefaults();
295    installListeners();
296
297    Component next = createNextButton();
298    Component previous = createPreviousButton();
299
300    installNextButtonListeners(next);
301    installPreviousButtonListeners(previous);
302
303    c.add(createEditor(), "Editor");
304    c.add(next, "Next");
305    c.add(previous, "Previous");
306  }
307
308  /**
309   * Replace the old editor with the new one
310   *
311   * @param oldEditor the old editor
312   * @param newEditor the new one to replace with
313   */
314  protected void replaceEditor(JComponent oldEditor, JComponent newEditor)
315  {
316    spinner.remove(oldEditor);
317    spinner.add(newEditor);
318  }
319
320  /**
321   * The reverse of <code>installDefaults</code>. Called by
322   * <code>uninstallUI</code>
323   */
324  protected void uninstallDefaults()
325  {
326    spinner.setLayout(null);
327  }
328
329  /**
330   * The reverse of <code>installListeners</code>, called by
331   * <code>uninstallUI</code>
332   */
333  protected void uninstallListeners()
334  {
335    spinner.removePropertyChangeListener(listener);
336  }
337
338  /**
339   * Called when the current L&F is replaced with another one, should call
340   * <code>uninstallDefaults</code> and <code>uninstallListeners</code> as
341   * well as remove the next/previous buttons and the editor
342   *
343   * @param c DOCUMENT ME!
344   */
345  public void uninstallUI(JComponent c)
346  {
347    super.uninstallUI(c);
348
349    uninstallDefaults();
350    uninstallListeners();
351    c.removeAll();
352  }
353
354  /** The spinner for this UI */
355  protected JSpinner spinner;
356
357  /** DOCUMENT ME! */
358  private PropertyChangeListener listener = createPropertyChangeListener();
359
360  /**
361   * A layout manager for the {@link JSpinner} component.  The spinner has
362   * three subcomponents: an editor, a 'next' button and a 'previous' button.
363   */
364  private class DefaultLayoutManager implements LayoutManager
365  {
366    /**
367     * Layout the spinners inner parts.
368     *
369     * @param parent The parent container
370     */
371    public void layoutContainer(Container parent)
372    {
373      synchronized (parent.getTreeLock())
374        {
375          Insets i = parent.getInsets();
376          boolean l2r = parent.getComponentOrientation().isLeftToRight();
377          /*
378            --------------    --------------
379            |        | n |    | n |        |
380            |   e    | - | or | - |   e    |
381            |        | p |    | p |        |
382            --------------    --------------
383          */
384          Dimension e = prefSize(editor);
385          Dimension n = prefSize(next);
386          Dimension p = prefSize(previous);
387          Dimension s = parent.getSize();
388
389          int x = l2r ? i.left : i.right;
390          int y = i.top;
391          int w = Math.max(p.width, n.width);
392          int h = (s.height - i.bottom) / 2;
393          int e_width = s.width - w - i.left - i.right;
394
395          if (l2r)
396            {
397              setBounds(editor, x, y, e_width, 2 * h);
398              x += e_width;
399              setBounds(next, x, y, w, h);
400              y += h;
401              setBounds(previous, x, y, w, h);
402            }
403          else
404            {
405              setBounds(next, x, y + (s.height - e.height) / 2, w, h);
406              y += h;
407              setBounds(previous, x, y + (s.height - e.height) / 2, w, h);
408              x += w;
409              y -= h;
410              setBounds(editor, x, y, e_width, e.height);
411            }
412        }
413    }
414
415    /**
416     * Calculates the minimum layout size.
417     *
418     * @param parent  the parent.
419     *
420     * @return The minimum layout size.
421     */
422    public Dimension minimumLayoutSize(Container parent)
423    {
424      Dimension d = new Dimension();
425
426      if (editor != null)
427        {
428          Dimension tmp = editor.getMinimumSize();
429          d.width += tmp.width;
430          d.height = tmp.height;
431        }
432
433      int nextWidth = 0;
434      int previousWidth = 0;
435
436      if (next != null)
437        {
438          Dimension tmp = next.getMinimumSize();
439          nextWidth = tmp.width;
440        }
441      if (previous != null)
442        {
443          Dimension tmp = previous.getMinimumSize();
444          previousWidth = tmp.width;
445        }
446
447      d.width += Math.max(nextWidth, previousWidth);
448
449      return d;
450    }
451
452    /**
453     * Returns the preferred layout size of the container.
454     *
455     * @param parent DOCUMENT ME!
456     *
457     * @return DOCUMENT ME!
458     */
459    public Dimension preferredLayoutSize(Container parent)
460    {
461      Dimension d = new Dimension();
462
463      if (editor != null)
464        {
465          Dimension tmp = editor.getPreferredSize();
466          d.width += Math.max(tmp.width, 40);
467          d.height = tmp.height;
468        }
469
470      int nextWidth = 0;
471      int previousWidth = 0;
472
473      if (next != null)
474        {
475          Dimension tmp = next.getPreferredSize();
476          nextWidth = tmp.width;
477        }
478      if (previous != null)
479        {
480          Dimension tmp = previous.getPreferredSize();
481          previousWidth = tmp.width;
482        }
483
484      d.width += Math.max(nextWidth, previousWidth);
485      Insets insets = parent.getInsets();
486      d.width = d.width + insets.left + insets.right;
487      d.height = d.height + insets.top + insets.bottom;
488      return d;
489    }
490
491    /**
492     * DOCUMENT ME!
493     *
494     * @param child DOCUMENT ME!
495     */
496    public void removeLayoutComponent(Component child)
497    {
498      if (child == editor)
499        editor = null;
500      else if (child == next)
501        next = null;
502      else if (previous == child)
503        previous = null;
504    }
505
506    /**
507     * DOCUMENT ME!
508     *
509     * @param name DOCUMENT ME!
510     * @param child DOCUMENT ME!
511     */
512    public void addLayoutComponent(String name, Component child)
513    {
514      if ("Editor".equals(name))
515        editor = child;
516      else if ("Next".equals(name))
517        next = child;
518      else if ("Previous".equals(name))
519        previous = child;
520    }
521
522    /**
523     * DOCUMENT ME!
524     *
525     * @param c DOCUMENT ME!
526     *
527     * @return DOCUMENT ME!
528     */
529    private Dimension prefSize(Component c)
530    {
531      if (c == null)
532        return new Dimension();
533      else
534        return c.getPreferredSize();
535    }
536
537    /**
538     * Sets the bounds for the specified component.
539     *
540     * @param c  the component.
541     * @param x  the x-coordinate for the top-left of the component bounds.
542     * @param y  the y-coordinate for the top-left of the component bounds.
543     * @param w  the width of the bounds.
544     * @param h  the height of the bounds.
545     */
546    private void setBounds(Component c, int x, int y, int w, int h)
547    {
548      if (c != null)
549        c.setBounds(x, y, w, h);
550    }
551
552    /** The editor component. */
553    private Component editor;
554
555    /** The next button. */
556    private Component next;
557
558    /** The previous button. */
559    private Component previous;
560  }
561}