View Javadoc
1   /*
2    * @(#)MultiThumbSlider.java
3    *
4    * $Date: 2015-01-04 21:15:07 -0500 (Sun, 04 Jan 2015) $
5    *
6    * Copyright (c) 2011 by Jeremy Wood.
7    * All rights reserved.
8    *
9    * The copyright of this software is owned by Jeremy Wood. 
10   * You may not use, copy or modify this software, except in  
11   * accordance with the license agreement you entered into with  
12   * Jeremy Wood. For details see accompanying license terms.
13   * 
14   * This software is probably, but not necessarily, discussed here:
15   * https://javagraphics.java.net/
16   * 
17   * That site should also contain the most recent official version
18   * of this software.  (See the SVN repository for more details.)
19   */
20  package com.bric.multislider;
21  
22  import java.lang.reflect.Array;
23  import java.lang.reflect.Constructor;
24  import java.util.List;
25  import java.util.Vector;
26  
27  import javax.swing.JComponent;
28  import javax.swing.SwingConstants;
29  import javax.swing.UIManager;
30  import javax.swing.event.ChangeEvent;
31  import javax.swing.event.ChangeListener;
32  import javax.swing.plaf.ComponentUI;
33  
34  /**
35   * This JComponent resembles a <code>JSlider</code>, except there are at least two thumbs. A <code>JSlider</code> is designed to
36   * modify one number within a certain range of values. By contrast a <code>MultiThumbSlider</code> actually modifies a
37   * <i>table</i> of data. Each thumb in a <code>MultiThumbSlider</code> should be thought of as a key, and it maps to an abstract
38   * value. In the case of the <code>GradientSlider</code>: each value is a <code>java.awt.Color</code>. Other subclasses could
39   * come along that map to other abstract objects. (For example, a <code>VolumeSlider</code> might map each thumb to a specific
40   * volume level. This type of widget would let the user control fading in/out of an audio track.)
41   * <P>
42   * The slider graphically represents the domain from zero to one, so each thumb is always positioned within that domain. If the
43   * user drags a thumb outside this domain: that thumb disappears.
44   * <P>
45   * There is always a selected thumb in each slider when this slider has the keyboard focus. The user can press the tab key (or
46   * shift-tab) to transfer focus to different thumbs. Also the arrow keys can be used to control the selected thumb.
47   * <P>
48   * The user can click and drag any thumb to a new location. If a thumb is dragged so it is less than zero or greater than one:
49   * then that thumb is removed. If the user clicks between two existing thumbs: a new thumb is created if <code>autoAdd</code> is
50   * set to <code>true</code>. (If <code>autoAdd</code> is set to false: nothing happens.)
51   * <P>
52   * There are unimplemented methods in this class: <code>doDoubleClick()</code> and <code>doPopup()</code>. The UI will invoke
53   * these methods as needed; this gives the user a chance to edit the values represented at a particular point.
54   * <P>
55   * Also using the keyboard:
56   * <ul>
57   * <LI>In a horizontal slider, the user can press modifier+left or modifer+right to insert a new thumb to the left/right of the
58   * currently selected thumb. (Where "modifier" refers to <code>Toolkit.getDefaultTookit().getMenuShortcutKeyMask()</code>. On
59   * Mac this is META, and on Windows this is CONTROL.) Likewise on a vertical slider the up/down arrow keys can be used to add
60   * thumbs.</li>
61   * <LI>The delete/backspace key can be used to remove thumbs.</li>
62   * <LI>In a horizontal slider, the down arrow key can be used to invoke <code>doPopup()</code>. This should invoke a
63   * <code>JPopupMenu</code> that is keyboard accessible, so the user should be able to navigate this component without a mouse.
64   * Likewise on a vertical slider the right arrow key should do the same.</li>
65   * <LI>The space bar or return key invokes <code>doDoubleClick()</code>.</LI>
66   * </ul>
67   * <P>
68   * Because thumbs can be abstractly inserted, the values each thumb represents should be tween-able. That is, if there is a
69   * value at zero and a value at one, the call <code>getValue(.5f)</code> must return a value that is halfway between those
70   * values.
71   * <P>
72   * Also note that although the thumbs must always be between zero and one: the minimum and maximum thumbs do not have to be zero
73   * and one. The user can adjust them so the minimum thumb is, say, .2f, and the maximum thumb is .5f.
74   * @param <T> the type of data each float maps to. For example: in the GradientSlider this value is a Color. Sometimes this
75   *            property may be unnecessary. If this slider is only meant to store the relative position of thumbs, then you may
76   *            set this to a trivial stub-like object like a String or Character.
77   */
78  public class MultiThumbSlider<T> extends JComponent
79  {
80      @SuppressWarnings("javadoc")
81      private static final long serialVersionUID = 1L;
82  
83      /** A set of possible behaviors when one thumb collides with another. */
84      public static enum Collision
85      {
86          /** When the user drags one thumb and it collides with another, nudge the other thumb as far as possible. */
87          NUDGE_OTHER,
88          /** When the user drags one thumb and it collides with another, skip over the other thumb. */
89          JUMP_OVER_OTHER,
90          /**
91           * When the user drags one thumb and it collides with another, bump into the other thumb and don't allow any more
92           * movement.
93           */
94          STOP_AGAINST
95      };
96  
97      /** The property that controls whether clicking between thumbs automatically adds a thumb. */
98      public static final String AUTOADD_PROPERTY = MultiThumbSlider.class.getName() + ".auto-add";
99  
100     /** The property that controls whether the user can remove a thumb (either by dragging or with the delete key). */
101     public static final String REMOVAL_ALLOWED = MultiThumbSlider.class.getName() + ".removal-allowed";
102 
103     /** The property that is changed when <code>setSelectedThumb()</code> is called. */
104     public static final String SELECTED_THUMB_PROPERTY = MultiThumbSlider.class.getName() + ".selected-thumb";
105 
106     /** The property that is changed when <code>setCollisionPolicy(c)</code> is called. */
107     public static final String COLLISION_PROPERTY = MultiThumbSlider.class.getName() + ".collision";
108 
109     /** The property that is changed when <code>setInverted(b)</code> is called. */
110     public static final String INVERTED_PROPERTY = MultiThumbSlider.class.getName() + ".inverted";
111 
112     /** The property that is changed when <code>setInverted(b)</code> is called. */
113     public static final String THUMB_OVERLAP_PROPERTY = MultiThumbSlider.class.getName() + ".thumb-overlap";
114 
115     /** The property that is changed when <code>setMinimumThumbnailCount(b)</code> is called. */
116     public static final String THUMB_MINIMUM_PROPERTY = MultiThumbSlider.class.getName() + ".thumb-minimum";
117 
118     /** The property that is changed when <code>setOrientation(i)</code> is called. */
119     public static final String ORIENTATION_PROPERTY = MultiThumbSlider.class.getName() + ".orientation";
120 
121     /**
122      * The property that is changed when <code>setValues()</code> is called. Note this is used when either the positions or the
123      * values are updated, because they need to be updated at the same time to maintain an exact one-to-one ratio.
124      */
125     public static final String VALUES_PROPERTY = MultiThumbSlider.class.getName() + ".values";
126 
127     /** The property that is changed when <code>setValueIsAdjusting(b)</code> is called. */
128     public static final String ADJUST_PROPERTY = MultiThumbSlider.class.getName() + ".adjusting";
129 
130     /** The property that is changed when <code>setPaintTicks(b)</code> is called. */
131     public static final String PAINT_TICKS_PROPERTY = MultiThumbSlider.class.getName() + ".paint ticks";
132 
133     /** The positions of the thumbs */
134     protected float[] thumbPositions = new float[0];
135 
136     /** The values for each thumb */
137     protected T[] values;
138 
139     /** ChangeListeners registered with this slider. */
140     List<ChangeListener> changeListeners;
141 
142     /**
143      * The orientation constant for a horizontal slider.
144      */
145     public static final int HORIZONTAL = SwingConstants.HORIZONTAL;
146 
147     /**
148      * The orientation constant for a vertical slider.
149      */
150     public static final int VERTICAL = SwingConstants.VERTICAL;
151 
152     /**
153      * Creates a new horizontal MultiThumbSlider.
154      * @param thumbPositions an array of values from zero to one.
155      * @param values an array of values, each value corresponds to a value in <code>thumbPositions</code>.
156      */
157     public MultiThumbSlider(float[] thumbPositions, T[] values)
158     {
159         this(HORIZONTAL, thumbPositions, values);
160     }
161 
162     /**
163      * Creates a new MultiThumbSlider.
164      * @param orientation must be <code>HORIZONTAL</code> or <code>VERTICAL</code>
165      * @param thumbPositions an array of values from zero to one.
166      * @param values an array of values, each value corresponds to a value in <code>thumbPositions</code>.
167      */
168     public MultiThumbSlider(int orientation, float[] thumbPositions, T[] values)
169     {
170         setOrientation(orientation);
171         setValues(thumbPositions, values);
172         setFocusable(true);
173         updateUI();
174     }
175 
176     @SuppressWarnings({"unchecked", "javadoc"})
177     public MultiThumbSliderUi<T> getUI()
178     {
179         return (MultiThumbSliderUi<T>) this.ui;
180     }
181 
182     @Override
183     public void updateUI()
184     {
185         String name = UIManager.getString("MultiThumbSliderUI");
186         if (name == null)
187         {
188             if (Jvm.isMac)
189             {
190                 name = "com.bric.multislider.AquaMultiThumbSliderUI";
191             }
192             else if (Jvm.isWindows)
193             {
194                 name = "com.bric.multislider.VistaMultiThumbSliderUI";
195             }
196             else
197             {
198                 name = "com.bric.multislider.DefaultMultiThumbSliderUI";
199             }
200         }
201         try
202         {
203             Class<?> c = Class.forName(name);
204             Constructor<?>[] constructors = c.getConstructors();
205             for (int a = 0; a < constructors.length; a++)
206             {
207                 Class<?>[] types = constructors[a].getParameterTypes();
208                 if (types.length == 1 && types[0].equals(MultiThumbSlider.class))
209                 {
210                     MultiThumbSliderUi<T> ui = (MultiThumbSliderUi<T>) constructors[a].newInstance(new Object[] {this});
211                     setUI(ui);
212                     return;
213                 }
214             }
215         }
216         catch (ClassNotFoundException e)
217         {
218             throw new RuntimeException("The class \"" + name + "\" could not be found.");
219         }
220         catch (Throwable t)
221         {
222             RuntimeException e = new RuntimeException("The class \"" + name + "\" could not be constructed.");
223             e.initCause(t);
224             throw e;
225         }
226     }
227 
228     /**
229      * @param ui slider
230      */
231     public void setUI(MultiThumbSliderUi<T> ui)
232     {
233         super.setUI((ComponentUI) ui);
234     }
235 
236     /**
237      * This listener will be notified when the colors/positions of this slider are modified.
238      * <P>
239      * Note you can also listen to these events by listening to the <code>VALUES_PROPERTY</code>, but this mechanism is provided
240      * as a convenience to resemble the <code>JSlider</code> model.
241      * @param l the <code>ChangeListener</code> to add.
242      */
243     public void addChangeListener(ChangeListener l)
244     {
245         if (this.changeListeners == null)
246             this.changeListeners = new Vector<ChangeListener>();
247         if (this.changeListeners.contains(l))
248             return;
249         this.changeListeners.add(l);
250     }
251 
252     /**
253      * Removes a <code>ChangeListener</code> from this slider.
254      * @param l the listener to remove
255      */
256     public void removeChangeListener(ChangeListener l)
257     {
258         if (this.changeListeners == null)
259             return;
260         this.changeListeners.remove(l);
261     }
262 
263     /** Invokes all the ChangeListeners. */
264     protected void fireChangeListeners()
265     {
266         if (this.changeListeners == null)
267             return;
268         for (int a = 0; a < this.changeListeners.size(); a++)
269         {
270             try
271             {
272                 (this.changeListeners.get(a)).stateChanged(new ChangeEvent(this));
273             }
274             catch (Throwable t)
275             {
276                 t.printStackTrace();
277             }
278         }
279     }
280 
281     /**
282      * Depending on which thumb is selected, this may shift the focus to the next available thumb, or it may shift the focus to
283      * the next focusable <code>JComponent</code>.
284      */
285     @Override
286     public void transferFocus()
287     {
288         transferFocus(true);
289     }
290 
291     /**
292      * Shifts the focus forward or backward. This may decide to select another thumb, or it may call
293      * <code>super.transferFocus()</code> to let the next JComponent receive the focus.
294      * @param forward whether we're shifting forward or backward
295      */
296     private void transferFocus(boolean forward)
297     {
298         int direction = (forward) ? 1 : -1;
299 
300         // because vertical sliders are technically inverted already:
301         if (getOrientation() == VERTICAL)
302             direction = direction * -1;
303 
304         // because inverted sliders are, well, inverted:
305         if (isInverted())
306             direction = direction * -1;
307 
308         int selectedThumb = getSelectedThumb();
309         if (direction == 1)
310         {
311             if (selectedThumb != this.thumbPositions.length - 1)
312             {
313                 setSelectedThumb(selectedThumb + 1);
314                 return;
315             }
316         }
317         else
318         {
319             if (selectedThumb != 0)
320             {
321                 setSelectedThumb(selectedThumb - 1);
322                 return;
323             }
324         }
325         if (forward)
326         {
327             super.transferFocus();
328         }
329         else
330         {
331             super.transferFocusBackward();
332         }
333     }
334 
335     /**
336      * Depending on which thumb is selected, this may shift the focus to the previous available thumb, or it may shift the focus
337      * to the previous focusable <code>JComponent</code>.
338      */
339     @Override
340     public void transferFocusBackward()
341     {
342         transferFocus(false);
343     }
344 
345     /**
346      * This creates a new value for insertion.
347      * <P>
348      * If the <code>pos</code> argument is outside the domain of thumbs, then a value still needs to be returned.
349      * @param pos a position between zero and one
350      * @return a value that corresponds to the position <code>pos</code>
351      */
352     public T createValueForInsertion(float pos)
353     {
354         throw new NullPointerException(
355                 "this method is undefined. Either auto-adding should be disabled, or this method needs to be overridden to return a value");
356     }
357 
358     /**
359      * Removes a specific thumb
360      * @param thumbIndex the thumb index to remove.
361      */
362     public void removeThumb(int thumbIndex)
363     {
364         if (thumbIndex < 0 || thumbIndex > this.thumbPositions.length)
365             throw new IllegalArgumentException("There is no thumb at index " + thumbIndex + " to remove.");
366 
367         float[] f = new float[this.thumbPositions.length - 1];
368         T[] c = createSimilarArray(this.values, this.values.length - 1);
369         System.arraycopy(this.thumbPositions, 0, f, 0, thumbIndex);
370         System.arraycopy(this.values, 0, c, 0, thumbIndex);
371         System.arraycopy(this.thumbPositions, thumbIndex + 1, f, thumbIndex, f.length - thumbIndex);
372         System.arraycopy(this.values, thumbIndex + 1, c, thumbIndex, f.length - thumbIndex);
373         setValues(f, c);
374     }
375 
376     /**
377      * This is a kludgy casting trick to make our arrays mesh with generics.
378      * @param srcArray source array
379      * @param length the length
380      * @return array of type T
381      */
382     private T[] createSimilarArray(T[] srcArray, int length)
383     {
384         Class<?> componentType = srcArray.getClass().getComponentType();
385         return (T[]) Array.newInstance(componentType, length);
386     }
387 
388     /**
389      * An optional method subclasses can override to react to the user's double-click. When a thumb is double-clicked the user
390      * is trying to edit the value for that thumb. A double-click probably suggests the user wants a detailed set of controls to
391      * edit a value, such as a dialog.
392      * <P>
393      * Note this method will be called with arguments (-1,-1) if the space bar or return key is pressed.
394      * <P>
395      * By default this method does nothing, and returns <code>false</code>
396      * <P>
397      * Note the (x,y) information passed to this method is only provided so subclasses can position components (such as a
398      * JPopupMenu). It can be assumed for a double-click event that the user has selected a thumb (since one click will
399      * click/create a thumb) and intends to edit the currently selected thumb.
400      * @param x the x-value of the mouse click location
401      * @param y the y-value of the mouse click location
402      * @return <code>true</code> if this event was consumed, or acted upon. <code>false</code> if this is unimplemented.
403      */
404     public boolean doDoubleClick(int x, int y)
405     {
406         return false;
407     }
408 
409     /**
410      * An optional method subclasses can override to react to the user's request for a contextual menu. When a thumb is
411      * right-clicked the user is trying to edit the value for that thumb. A right-click probably suggests the user wants very
412      * quick, simple options to adjust a thumb.
413      * <P>
414      * By default this method does nothing, and returns <code>false</code>
415      * @param x the x-value of the mouse click location
416      * @param y the y-value of the mouse click location
417      * @return <code>true</code> if this event was consumed, or acted upon. <code>false</code> if this is unimplemented.
418      */
419     public boolean doPopup(int x, int y)
420     {
421         return false;
422     }
423 
424     /**
425      * Tells if tick marks are to be painted.
426      * @return whether ticks should be painted on this slider.
427      */
428     public boolean isPaintTicks()
429     {
430         Boolean b = (Boolean) getClientProperty(PAINT_TICKS_PROPERTY);
431         if (b == null)
432             return false;
433         return b;
434     }
435 
436     /**
437      * Turns on/off the painted tick marks for this slider.
438      * <P>
439      * This triggers a <code>PropertyChangeEvent</code> for <code>PAINT_TICKS_PROPERTY</code>.
440      * @param b whether tick marks should be painted
441      */
442     public void setPaintTicks(boolean b)
443     {
444         putClientProperty(PAINT_TICKS_PROPERTY, b);
445     }
446 
447     /**
448      * This creats and inserts a thumb at a position indicated.
449      * <P>
450      * This method relies on the abstract <code>getValue(float)</code> to determine what value to put at the new thumb location.
451      * @param pos the new thumb position
452      * @return the index of the newly created thumb
453      */
454     public int addThumb(float pos)
455     {
456         if (pos < 0 || pos > 1)
457             throw new IllegalArgumentException("the new position (" + pos + ") must be between zero and one");
458         T newValue = createValueForInsertion(pos);
459         float[] f = new float[this.thumbPositions.length + 1];
460         T[] c = createSimilarArray(this.values, this.values.length + 1);
461 
462         int newIndex = -1;
463         if (pos < this.thumbPositions[0])
464         {
465             System.arraycopy(this.thumbPositions, 0, f, 1, this.thumbPositions.length);
466             System.arraycopy(this.values, 0, c, 1, this.values.length);
467             newIndex = 0;
468             f[0] = pos;
469             c[0] = newValue;
470         }
471         else if (pos > this.thumbPositions[this.thumbPositions.length - 1])
472         {
473             System.arraycopy(this.thumbPositions, 0, f, 0, this.thumbPositions.length);
474             System.arraycopy(this.values, 0, c, 0, this.values.length);
475             newIndex = f.length - 1;
476             f[f.length - 1] = pos;
477             c[c.length - 1] = newValue;
478         }
479         else
480         {
481             boolean addedYet = false;
482             for (int a = 0; a < f.length; a++)
483             {
484                 if (addedYet == false && this.thumbPositions[a] < pos)
485                 {
486                     f[a] = this.thumbPositions[a];
487                     c[a] = this.values[a];
488                 }
489                 else
490                 {
491                     if (addedYet == false)
492                     {
493                         c[a] = newValue;
494                         f[a] = pos;
495                         addedYet = true;
496                         newIndex = a;
497                     }
498                     else
499                     {
500                         f[a] = this.thumbPositions[a - 1];
501                         c[a] = this.values[a - 1];
502                     }
503                 }
504             }
505         }
506         setValues(f, c);
507         return newIndex;
508     }
509 
510     /**
511      * This is used to notify other objects when the user is in the process of adjusting values in this slider.
512      * <P>
513      * A listener may not want to act on certain changes until this property is <code>false</code> if it is expensive to process
514      * certain changes.
515      * <P>
516      * This triggers a <code>PropertyChangeEvent</code> for <code>ADJUST_PROPERTY</code>.
517      * @param b new value
518      */
519     public void setValueIsAdjusting(boolean b)
520     {
521         putClientProperty(ADJUST_PROPERTY, b);
522     }
523 
524     /**
525      * <code>true</code> if the user is current modifying this component.
526      * @return the value of the <code>adjusting</code> property
527      */
528     public boolean isValueAdjusting()
529     {
530         Boolean b = (Boolean) getClientProperty(ADJUST_PROPERTY);
531         if (b == null)
532             return false;
533         return b;
534     }
535 
536     /**
537      * The thumb positions for this slider.
538      * <P>
539      * There is a one-to-one correspondence between this array and the <code>getValues()</code> array.
540      * <P>
541      * This array is always sorted in ascending order.
542      * @return an array of the positions of thumbs.
543      */
544     public float[] getThumbPositions()
545     {
546         float[] f = new float[this.thumbPositions.length];
547         System.arraycopy(this.thumbPositions, 0, f, 0, f.length);
548         return f;
549     }
550 
551     /**
552      * The values for thumbs for this slider.
553      * <P>
554      * There is a one-to-one correspondence between this array and the <code>getThumbPositions()</code> array.
555      * @return an array of the values associated with each thumb.
556      */
557     public T[] getValues()
558     {
559         T[] c = createSimilarArray(this.values, this.values.length);
560         System.arraycopy(this.values, 0, c, 0, c.length);
561         return c;
562     }
563 
564     /**
565      * @param f an array of floats
566      * @return a string representation of f
567      */
568     private static String toString(float[] f)
569     {
570         StringBuffer sb = new StringBuffer();
571         sb.append('[');
572         for (int a = 0; a < f.length; a++)
573         {
574             sb.append(Float.toString(f[a]));
575             if (a != f.length - 1)
576             {
577                 sb.append(", ");
578             }
579         }
580         sb.append(']');
581         return sb.toString();
582     }
583 
584     /**
585      * This assigns new positions/values for the thumbs in this slider. The two must be assigned at exactly the same time, so
586      * there is always the same number of thumbs/sliders.
587      * <P>
588      * This triggers a <code>PropertyChangeEvent</code> for <code>VALUES_PROPERTY</code>, and possibly for the
589      * <code>SELECTED_THUMB_PROPERTY</code> if that had to be adjusted, too.
590      * @param thumbPositions an array of the new position of each thumb
591      * @param values an array of the value associated with each thumb
592      * @throws IllegalArgumentException if the size of the arrays are different, or if the thumbPositions array is not sorted in
593      *             ascending order.
594      */
595     public void setValues(float[] thumbPositions, T[] values)
596     {
597         if (values.length != thumbPositions.length)
598             throw new IllegalArgumentException("there number of positions (" + thumbPositions.length
599                     + ") must equal the number of values (" + values.length + ")");
600 
601         for (int a = 0; a < values.length; a++)
602         {
603             if (values[a] == null)
604                 throw new NullPointerException();
605             if (a > 0 && thumbPositions[a] < thumbPositions[a - 1])
606                 throw new IllegalArgumentException(
607                         "the thumb positions must be ascending order (" + toString(thumbPositions) + ")");
608             if (thumbPositions[a] < 0 || thumbPositions[a] > 1)
609                 throw new IllegalArgumentException(
610                         "illegal thumb value " + thumbPositions[a] + " (must be between zero and one)");
611         }
612 
613         // don't clone arrays and fire off events if
614         // there really is no change here:
615         if (thumbPositions.length == this.thumbPositions.length)
616         {
617             boolean equal = true;
618             for (int a = 0; a < thumbPositions.length && equal; a++)
619             {
620                 if (thumbPositions[a] != this.thumbPositions[a])
621                     equal = false;
622             }
623             for (int a = 0; a < values.length && equal; a++)
624             {
625                 if (!values[a].equals(this.values[a]))
626                     equal = false;
627             }
628             if (equal)
629                 return; // no change! go home.
630         }
631 
632         this.thumbPositions = new float[thumbPositions.length];
633         System.arraycopy(thumbPositions, 0, this.thumbPositions, 0, thumbPositions.length);
634         this.values = createSimilarArray(values, values.length);
635         System.arraycopy(values, 0, this.values, 0, values.length);
636         int oldThumb = getSelectedThumb();
637         int newThumb = oldThumb;
638         if (newThumb >= thumbPositions.length)
639         {
640             newThumb = thumbPositions.length - 1;
641         }
642         firePropertyChange(VALUES_PROPERTY, null, values);
643         if (oldThumb != newThumb)
644         {
645             setSelectedThumb(newThumb);
646         }
647         fireChangeListeners();
648     }
649 
650     /**
651      * The number of thumbs in this slider.
652      * @return the number of thumbs.
653      */
654     public int getThumbCount()
655     {
656         return this.thumbPositions.length;
657     }
658 
659     /**
660      * Assigns the currently selected thumb. A value of -1 indicates that no thumb is currently selected.
661      * <P>
662      * A slider should always have a selected thumb if it has the keyboard focus, though, so be careful when you modify this.
663      * <P>
664      * This triggers a <code>PropertyChangeEvent</code> for <code>SELECTED_THUMB_PROPERTY</code>.
665      * @param index the new selected thumb
666      */
667     public void setSelectedThumb(int index)
668     {
669         putClientProperty(SELECTED_THUMB_PROPERTY, new Integer(index));
670     }
671 
672     /**
673      * Returns the selected thumb index, or -1 if this component doesn't have the keyboard focus.
674      * @return the selected thumb index
675      */
676     public int getSelectedThumb()
677     {
678         return getSelectedThumb(true);
679     }
680 
681     /**
682      * Returns the currently selected thumb index.
683      * <P>
684      * Note this might be -1, indicating that there is no selected thumb.
685      * <P>
686      * It is recommend you use the <code>getSelectedThumb()</code> method most of the time. This method is made public so UI's
687      * can provide a better user experience as this component gains and loses focus.
688      * @param ignoreIfUnfocused if this component doesn't have focus and this is <code>true</code>, then this returns -1. If
689      *            this is <code>false</code> then this returns the internal value used to store the selected index, but the user
690      *            may not realize this thumb is "selected".
691      * @return the selected thumb
692      */
693     public int getSelectedThumb(boolean ignoreIfUnfocused)
694     {
695         if (hasFocus() == false && ignoreIfUnfocused)
696             return -1;
697         Integer i = (Integer) getClientProperty(SELECTED_THUMB_PROPERTY);
698         if (i == null)
699             return -1;
700         return i.intValue();
701     }
702 
703     /**
704      * Controls whether thumbs are automatically added when the user clicks in a space that doesn't already have a thumb.
705      * @param b whether auto adding is active or not
706      */
707     public void setAutoAdding(boolean b)
708     {
709         putClientProperty(AUTOADD_PROPERTY, b);
710     }
711 
712     /**
713      * @return whether thumbs are automatically added when the user clicks in a space that doesn't already have a thumb.
714      */
715     public boolean isAutoAdding()
716     {
717         Boolean b = (Boolean) getClientProperty(AUTOADD_PROPERTY);
718         if (b == null)
719             return true;
720         return b;
721     }
722 
723     /**
724      * The orientation of this slider.
725      * @return HORIZONTAL or VERTICAL
726      */
727     public int getOrientation()
728     {
729         Integer i = (Integer) getClientProperty(ORIENTATION_PROPERTY);
730         if (i == null)
731             return HORIZONTAL;
732         return i;
733     }
734 
735     /**
736      * Reassign the orientation of this slider.
737      * @param i must be HORIZONTAL or VERTICAL
738      */
739     public void setOrientation(int i)
740     {
741         if (!(i == SwingConstants.HORIZONTAL || i == SwingConstants.VERTICAL))
742             throw new IllegalArgumentException("the orientation must be HORIZONTAL or VERTICAL");
743         putClientProperty(ORIENTATION_PROPERTY, i);
744     }
745 
746     /**
747      * @return whether this slider is inverted or not.
748      */
749     public boolean isInverted()
750     {
751         Boolean b = (Boolean) getClientProperty(INVERTED_PROPERTY);
752         if (b == null)
753             return false;
754         return b;
755     }
756 
757     /**
758      * Assigns whether this slider is inverted or not.
759      * @param b inverted slider or not This triggers a <code>PropertyChangeEvent</code> for <code>INVERTED_PROPERTY</code>.
760      */
761     public void setInverted(boolean b)
762     {
763         putClientProperty(INVERTED_PROPERTY, b);
764     }
765 
766     public Collision getCollisionPolicy()
767     {
768         Collision c = (Collision) getClientProperty(COLLISION_PROPERTY);
769         if (c == null)
770             c = Collision.JUMP_OVER_OTHER;
771         return c;
772     }
773 
774     public void setCollisionPolicy(Collision c)
775     {
776         putClientProperty(COLLISION_PROPERTY, c);
777     }
778 
779     public boolean isThumbRemovalAllowed()
780     {
781         Boolean b = (Boolean) getClientProperty(REMOVAL_ALLOWED);
782         if (b == null)
783             b = true;
784         return b;
785     }
786 
787     public void setThumbRemovalAllowed(boolean b)
788     {
789         putClientProperty(REMOVAL_ALLOWED, b);
790     }
791 
792     public void setMinimumThumbnailCount(int i)
793     {
794         putClientProperty(THUMB_MINIMUM_PROPERTY, i);
795     }
796 
797     public int getMinimumThumbnailCount()
798     {
799         Integer i = (Integer) getClientProperty(THUMB_MINIMUM_PROPERTY);
800         if (i == null)
801             return 1;
802         return i;
803     }
804 
805     public void setThumbOverlap(boolean i)
806     {
807         putClientProperty(THUMB_OVERLAP_PROPERTY, i);
808     }
809 
810     public boolean isThumbOverlap()
811     {
812         Boolean b = (Boolean) getClientProperty(THUMB_OVERLAP_PROPERTY);
813         if (b == null)
814             return false;
815         return b;
816     }
817 }