1/*******************************************************************************
2 * Copyright (c) 2011 Google, Inc.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 *    Google, Inc. - initial API and implementation
10 *******************************************************************************/
11package org.eclipse.wb.core.controls;
12
13import org.eclipse.swt.SWT;
14import org.eclipse.swt.events.FocusAdapter;
15import org.eclipse.swt.events.FocusEvent;
16import org.eclipse.swt.events.KeyAdapter;
17import org.eclipse.swt.events.KeyEvent;
18import org.eclipse.swt.events.SelectionAdapter;
19import org.eclipse.swt.events.SelectionEvent;
20import org.eclipse.swt.graphics.Color;
21import org.eclipse.swt.graphics.Point;
22import org.eclipse.swt.graphics.Rectangle;
23import org.eclipse.swt.widgets.Button;
24import org.eclipse.swt.widgets.Composite;
25import org.eclipse.swt.widgets.Display;
26import org.eclipse.swt.widgets.Event;
27import org.eclipse.swt.widgets.Layout;
28import org.eclipse.swt.widgets.Spinner;
29import org.eclipse.swt.widgets.Text;
30
31import java.text.DecimalFormat;
32import java.text.MessageFormat;
33import java.text.ParseException;
34
35/**
36 * Custom implementation of {@link Spinner}.
37 *
38 * @author scheglov_ke
39 * @coverage core.control
40 */
41public class CSpinner extends Composite {
42  private static final Color COLOR_VALID = Display.getCurrent().getSystemColor(
43      SWT.COLOR_LIST_BACKGROUND);
44  private static final Color COLOR_INVALID = new Color(null, 255, 230, 230);
45  private int m_minimum = 0;
46  private int m_maximum = 100;
47  private int m_increment = 1;
48  private int m_value = 0;
49  private int m_multiplier = 1;
50  private String m_formatPattern = "0";
51  private DecimalFormat m_format = new DecimalFormat(m_formatPattern);
52  ////////////////////////////////////////////////////////////////////////////
53  //
54  // GUI fields
55  //
56  ////////////////////////////////////////////////////////////////////////////
57  private final Button m_button;
58  private final Text m_text;
59  private final Spinner m_spinner;
60  private Composite win32Hack;
61
62  ////////////////////////////////////////////////////////////////////////////
63  //
64  // Constructor
65  //
66  ////////////////////////////////////////////////////////////////////////////
67  public CSpinner(Composite parent, int style) {
68    super(parent, style);
69    m_button = new Button(this, SWT.ARROW | SWT.DOWN);
70    {
71      int textStyle = SWT.SINGLE | SWT.RIGHT;
72      if (IS_OS_MAC_OSX_COCOA) {
73        textStyle |= SWT.BORDER;
74      }
75      m_text = new Text(this, textStyle);
76      m_text.setText("" + m_value);
77      m_text.addKeyListener(new KeyAdapter() {
78        @Override
79        public void keyPressed(KeyEvent e) {
80          if (e.keyCode == SWT.ARROW_UP || e.keyCode == SWT.ARROW_DOWN) {
81            e.doit = false;
82            updateValue(e.keyCode);
83          }
84        }
85
86        @Override
87        public void keyReleased(KeyEvent e) {
88          try {
89            m_value = (int) (m_format.parse(m_text.getText()).doubleValue() * m_multiplier);
90            if (m_value < m_minimum || m_value > m_maximum) {
91              m_text.setBackground(COLOR_INVALID);
92              setState(MessageFormat.format(
93                  Messages.CSpinner_outOfRange,
94                  m_value,
95                  m_minimum,
96                  m_maximum));
97              notifySelectionListeners(false);
98            } else {
99              setState(null);
100              notifySelectionListeners(true);
101            }
102          } catch (ParseException ex) {
103            setState(MessageFormat.format(
104                Messages.CSpinner_canNotParse,
105                m_text.getText(),
106                m_formatPattern));
107            notifySelectionListeners(false);
108          }
109        }
110      });
111    }
112    if (!IS_OS_MAC_OSX) {
113      win32Hack = new Composite(this, SWT.NONE);
114      win32Hack.setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
115      win32Hack.moveAbove(null);
116      win32Hack.moveBelow(m_text);
117    }
118    {
119      m_spinner = new Spinner(this, SWT.VERTICAL);
120      m_spinner.setMinimum(0);
121      m_spinner.setMaximum(50);
122      m_spinner.setIncrement(1);
123      m_spinner.setPageIncrement(1);
124      m_spinner.setSelection(25);
125      m_spinner.addFocusListener(new FocusAdapter() {
126        @Override
127        public void focusGained(FocusEvent e) {
128          setFocus();
129        }
130      });
131      m_spinner.addSelectionListener(new SelectionAdapter() {
132        @Override
133        public void widgetSelected(SelectionEvent e) {
134          m_text.forceFocus();
135          if (m_spinner.getSelection() > 25) {
136            updateValue(SWT.ARROW_UP);
137          } else {
138            updateValue(SWT.ARROW_DOWN);
139          }
140          m_spinner.setSelection(25);
141        }
142      });
143      setBackground(getDisplay().getSystemColor(SWT.COLOR_WHITE));
144      if (IS_OS_WINDOWS_XP || IS_OS_WINDOWS_2003) {
145        setLayout(new WindowsXpLayout());
146      } else if (IS_OS_WINDOWS_VISTA || IS_OS_WINDOWS_7) {
147        setLayout(new WindowsVistaLayout());
148      } else if (IS_OS_LINUX) {
149        setLayout(new LinuxLayout());
150      } else if (IS_OS_MAC_OSX) {
151        if (IS_OS_MAC_OSX_COCOA) {
152          setLayout(new MacCocoaLayout());
153        } else {
154          setLayout(new MacLayout());
155        }
156      } else {
157        setLayout(new WindowsXpLayout());
158      }
159    }
160  }
161
162  ////////////////////////////////////////////////////////////////////////////
163  //
164  // Access
165  //
166  ////////////////////////////////////////////////////////////////////////////
167  @Override
168  public void setEnabled(boolean enabled) {
169    super.setEnabled(enabled);
170    m_text.setEnabled(enabled);
171    m_spinner.setEnabled(enabled);
172  }
173
174  /**
175   * Sets the number of decimal places used by the receiver.
176   * <p>
177   * See {@link Spinner#setDigits(int)}.
178   */
179  public void setDigits(int digits) {
180    m_formatPattern = "0.";
181    m_multiplier = 1;
182    for (int i = 0; i < digits; i++) {
183      m_formatPattern += "0";
184      m_multiplier *= 10;
185    }
186    m_format = new DecimalFormat(m_formatPattern);
187    updateText();
188  }
189
190  /**
191   * Sets minimum and maximum using single invocation.
192   */
193  public void setRange(int minimum, int maximum) {
194    setMinimum(minimum);
195    setMaximum(maximum);
196  }
197
198  /**
199   * @return the minimum value that the receiver will allow.
200   */
201  public int getMinimum() {
202    return m_minimum;
203  }
204
205  /**
206   * Sets the minimum value that the receiver will allow.
207   */
208  public void setMinimum(int minimum) {
209    m_minimum = minimum;
210    setSelection(Math.max(m_value, m_minimum));
211  }
212
213  /**
214   * Sets the maximum value that the receiver will allow.
215   */
216  public void setMaximum(int maximum) {
217    m_maximum = maximum;
218    setSelection(Math.min(m_value, m_maximum));
219  }
220
221  /**
222   * Sets the amount that the receiver's value will be modified by when the up/down arrows are
223   * pressed to the argument, which must be at least one.
224   */
225  public void setIncrement(int increment) {
226    m_increment = increment;
227  }
228
229  /**
230   * Sets the <em>value</em>, which is the receiver's position, to the argument. If the argument is
231   * not within the range specified by minimum and maximum, it will be adjusted to fall within this
232   * range.
233   */
234  public void setSelection(int newValue) {
235    newValue = Math.min(Math.max(m_minimum, newValue), m_maximum);
236    if (newValue != m_value) {
237      m_value = newValue;
238      updateText();
239      // set valid state
240      setState(null);
241    }
242  }
243
244  private void updateText() {
245    String text = m_format.format((double) m_value / m_multiplier);
246    m_text.setText(text);
247    m_text.selectAll();
248  }
249
250  /**
251   * @return the <em>selection</em>, which is the receiver's position.
252   */
253  public int getSelection() {
254    return m_value;
255  }
256
257  ////////////////////////////////////////////////////////////////////////////
258  //
259  // Update
260  //
261  ////////////////////////////////////////////////////////////////////////////
262  /**
263   * Updates {@link #m_value} into given direction.
264   */
265  private void updateValue(int direction) {
266    // prepare new value
267    int newValue;
268    {
269      newValue = m_value;
270      if (direction == SWT.ARROW_UP) {
271        newValue += m_increment;
272      }
273      if (direction == SWT.ARROW_DOWN) {
274        newValue -= m_increment;
275      }
276    }
277    // update value
278    setSelection(newValue);
279    notifySelectionListeners(true);
280  }
281
282  /**
283   * Sets the valid/invalid state.
284   *
285   * @param message
286   *          the message to show, or <code>null</code> if valid.
287   */
288  private void setState(String message) {
289    m_text.setToolTipText(message);
290    if (message == null) {
291      m_text.setBackground(COLOR_VALID);
292    } else {
293      m_text.setBackground(COLOR_INVALID);
294    }
295  }
296
297  /**
298   * Notifies {@link SWT#Selection} listeners with value and state.
299   */
300  private void notifySelectionListeners(boolean valid) {
301    Event event = new Event();
302    event.detail = m_value;
303    event.doit = valid;
304    notifyListeners(SWT.Selection, event);
305  }
306
307  ////////////////////////////////////////////////////////////////////////////
308  //
309  // Windows XP
310  //
311  ////////////////////////////////////////////////////////////////////////////
312  /**
313   * Implementation of {@link Layout} for Windows XP.
314   */
315  private class WindowsXpLayout extends Layout {
316    @Override
317    protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
318      Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
319      size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width;
320      // add Text widget margin
321      size.y += 2;
322      // apply hints
323      if (wHint != SWT.DEFAULT) {
324        size.x = Math.min(size.x, wHint);
325      }
326      if (hHint != SWT.DEFAULT) {
327        size.y = Math.min(size.y, hHint);
328      }
329      // OK, final size
330      return size;
331    }
332
333    @Override
334    protected void layout(Composite composite, boolean flushCache) {
335      Rectangle cRect = composite.getClientArea();
336      if (cRect.isEmpty()) {
337        return;
338      }
339      // prepare size of Text
340      Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
341      // prepare size of Spinner
342      Point sSize;
343      sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
344      sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height));
345      sSize.x = Math.min(sSize.x, cRect.width);
346      // prepare width of arrows part of Spinner
347      int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
348      // set bounds for Spinner and Text
349      m_spinner.setBounds(
350          cRect.x + cRect.width - sSize.x + 1,
351          cRect.y - 1,
352          sSize.x,
353          cRect.height + 2);
354      m_text.setBounds(cRect.x, cRect.y + 1, cRect.width - arrowWidth, tSize.y);
355      win32Hack.setBounds(cRect.x, cRect.y, cRect.width - arrowWidth, sSize.y);
356    }
357  }
358  ////////////////////////////////////////////////////////////////////////////
359  //
360  // Windows Vista
361  //
362  ////////////////////////////////////////////////////////////////////////////
363  /**
364   * Implementation of {@link Layout} for Windows Vista.
365   */
366  private class WindowsVistaLayout extends Layout {
367    @Override
368    protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
369      Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
370      size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width;
371      // add Text widget margin
372      size.y += 3;
373      // apply hints
374      if (wHint != SWT.DEFAULT) {
375        size.x = Math.min(size.x, wHint);
376      }
377      if (hHint != SWT.DEFAULT) {
378        size.y = Math.min(size.y, hHint);
379      }
380      // OK, final size
381      return size;
382    }
383
384    @Override
385    protected void layout(Composite composite, boolean flushCache) {
386      Rectangle cRect = composite.getClientArea();
387      if (cRect.isEmpty()) {
388        return;
389      }
390      // prepare size of Text
391      Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
392      // prepare size of Spinner
393      Point sSize;
394      sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
395      sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height));
396      sSize.x = Math.min(sSize.x, cRect.width);
397      // prepare width of arrows part of Spinner
398      int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
399      // set bounds for Spinner and Text
400      m_spinner.setBounds(
401          cRect.x + cRect.width - sSize.x + 1,
402          cRect.y - 1,
403          sSize.x,
404          cRect.height + 2);
405      m_text.setBounds(cRect.x, cRect.y + 1, cRect.width - arrowWidth, tSize.y);
406      win32Hack.setBounds(cRect.x, cRect.y, cRect.width - arrowWidth, sSize.y);
407    }
408  }
409  ////////////////////////////////////////////////////////////////////////////
410  //
411  // Linux
412  //
413  ////////////////////////////////////////////////////////////////////////////
414  /**
415   * Implementation of {@link Layout} for Linux.
416   */
417  private class LinuxLayout extends Layout {
418    @Override
419    protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
420      Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
421      size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width;
422      // apply hints
423      if (wHint != SWT.DEFAULT) {
424        size.x = Math.min(size.x, wHint);
425      }
426      if (hHint != SWT.DEFAULT) {
427        size.y = Math.min(size.y, hHint);
428      }
429      // OK, final size
430      return size;
431    }
432
433    @Override
434    protected void layout(Composite composite, boolean flushCache) {
435      Rectangle cRect = composite.getClientArea();
436      if (cRect.isEmpty()) {
437        return;
438      }
439      // prepare size of Text
440      Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
441      // prepare size of Spinner
442      Point sSize;
443      sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
444      sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height));
445      sSize.x = Math.min(sSize.x, cRect.width);
446      // prepare width of arrows part of Spinner
447      int arrowWidth;
448      {
449        m_spinner.setSize(sSize);
450        arrowWidth = sSize.x - m_spinner.getClientArea().width;
451      }
452      // set bounds for Spinner and Text
453      m_spinner.setBounds(cRect.x + cRect.width - sSize.x, cRect.y - 2, sSize.x, cRect.height + 4);
454      m_text.setBounds(cRect.x, cRect.y, cRect.width - arrowWidth, tSize.y);
455    }
456  }
457  ////////////////////////////////////////////////////////////////////////////
458  //
459  // MacOSX
460  //
461  ////////////////////////////////////////////////////////////////////////////
462  /**
463   * Implementation of {@link Layout} for MacOSX.
464   */
465  private class MacLayout extends Layout {
466    @Override
467    protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
468      Point size = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
469      size.x += m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT).x - m_spinner.getClientArea().width;
470      // add Text widget margin
471      size.y += 4;
472      // apply hints
473      if (wHint != SWT.DEFAULT) {
474        size.x = Math.min(size.x, wHint);
475      }
476      if (hHint != SWT.DEFAULT) {
477        size.y = Math.min(size.y, hHint);
478      }
479      // OK, final size
480      return size;
481    }
482
483    @Override
484    protected void layout(Composite composite, boolean flushCache) {
485      Rectangle cRect = composite.getClientArea();
486      if (cRect.isEmpty()) {
487        return;
488      }
489      // prepare size of Text
490      Point tSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
491      tSize.y += 4;
492      // prepare size of Spinner
493      Point sSize;
494      sSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
495      sSize.y = Math.min(sSize.y, Math.min(tSize.y, cRect.height));
496      sSize.x = Math.min(sSize.x, cRect.width);
497      // prepare width of arrows part of Spinner
498      int arrowWidth = m_button.computeSize(-1, -1).x;
499      // set bounds for Spinner and Text
500      m_spinner.setBounds(cRect.x + cRect.width - sSize.x, cRect.y, sSize.x, cRect.height);
501      m_text.setBounds(cRect.x, cRect.y + 2, cRect.width - arrowWidth - 2, tSize.y);
502    }
503  }
504  /**
505   * Implementation of {@link Layout} for MacOSX Cocoa.
506   */
507  private class MacCocoaLayout extends Layout {
508    @Override
509    protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
510      Point textSize = m_text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
511      Point spinnerSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT);
512      int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
513      int width = textSize.x + arrowWidth;
514      int height = Math.max(spinnerSize.y, textSize.y);
515      // apply hints
516      if (wHint != SWT.DEFAULT) {
517        width = Math.min(width, wHint);
518      }
519      if (hHint != SWT.DEFAULT) {
520        height = Math.min(height, hHint);
521      }
522      return new Point(width, height);
523    }
524
525    @Override
526    protected void layout(Composite composite, boolean flushCache) {
527      Rectangle clientArea = composite.getClientArea();
528      if (clientArea.isEmpty()) {
529        return;
530      }
531      // prepare size of Spinner
532      Point spinnerSize = m_spinner.computeSize(SWT.DEFAULT, SWT.DEFAULT, flushCache);
533      // prepare width of arrows part of Spinner
534      int arrowWidth = m_button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;
535      m_spinner.setBounds(clientArea.x + clientArea.width - arrowWidth - 1, clientArea.y
536          + clientArea.height
537          - spinnerSize.y, arrowWidth + 2, spinnerSize.y);
538      m_text.setBounds(
539          clientArea.x + 2,
540          clientArea.y + 2,
541          clientArea.width - arrowWidth - 5,
542          clientArea.y + clientArea.height - 4);
543    }
544  }
545
546  ////////////////////////////////////////////////////////////////////////////
547  //
548  // System utils
549  //
550  ////////////////////////////////////////////////////////////////////////////
551  private static final String OS_NAME = System.getProperty("os.name");
552  private static final String OS_VERSION = System.getProperty("os.version");
553  private static final String WS_TYPE = SWT.getPlatform();
554  private static final boolean IS_OS_MAC_OSX = isOS("Mac OS X");
555  private static final boolean IS_OS_MAC_OSX_COCOA = IS_OS_MAC_OSX && "cocoa".equals(WS_TYPE);
556  private static final boolean IS_OS_LINUX = isOS("Linux") || isOS("LINUX");
557  private static final boolean IS_OS_WINDOWS_XP = isWindowsVersion("5.1");
558  private static final boolean IS_OS_WINDOWS_2003 = isWindowsVersion("5.2");
559  private static final boolean IS_OS_WINDOWS_VISTA = isWindowsVersion("6.0");
560  private static final boolean IS_OS_WINDOWS_7 = isWindowsVersion("6.1");
561
562  private static boolean isOS(String osName) {
563    return OS_NAME != null && OS_NAME.startsWith(osName);
564  }
565
566  private static boolean isWindowsVersion(String windowsVersion) {
567    return isOS("Windows") && OS_VERSION != null && OS_VERSION.startsWith(windowsVersion);
568  }
569}
570