View Javadoc
1   /*
2    * @(#)MultiThumbSliderUI.java
3    *
4    * $Date: 2015-01-04 20:37:28 -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.awt.Component;
23  import java.awt.Dimension;
24  import java.awt.Graphics;
25  import java.awt.Graphics2D;
26  import java.awt.Insets;
27  import java.awt.Rectangle;
28  import java.awt.RenderingHints;
29  import java.awt.Shape;
30  import java.awt.Toolkit;
31  import java.awt.event.ComponentEvent;
32  import java.awt.event.ComponentListener;
33  import java.awt.event.FocusEvent;
34  import java.awt.event.FocusListener;
35  import java.awt.event.KeyEvent;
36  import java.awt.event.KeyListener;
37  import java.awt.event.MouseEvent;
38  import java.awt.event.MouseListener;
39  import java.awt.event.MouseMotionListener;
40  import java.awt.geom.AffineTransform;
41  import java.awt.geom.Ellipse2D;
42  import java.awt.geom.GeneralPath;
43  import java.awt.geom.Point2D;
44  import java.awt.geom.Rectangle2D;
45  import java.beans.PropertyChangeEvent;
46  import java.beans.PropertyChangeListener;
47  import java.lang.reflect.Array;
48  import java.util.LinkedHashSet;
49  import java.util.Set;
50  
51  import javax.swing.JComponent;
52  import javax.swing.SwingConstants;
53  import javax.swing.UIManager;
54  import javax.swing.plaf.ComponentUI;
55  
56  import com.bric.multislider.MultiThumbSlider.Collision;
57  
58  /**
59   * This is the abstract UI for <code>MultiThumbSliders</code>
60   * @param <T> the type
61   */
62  public abstract class MultiThumbSliderUI<T> extends ComponentUI implements MouseListener, MouseMotionListener
63  {
64  
65      /**
66       * The Swing client property associated with a Thumb.
67       * @see Thumb
68       */
69      public static final String THUMB_SHAPE_PROPERTY = MultiThumbSliderUI.class.getName() + ".thumbShape";
70  
71      PropertyChangeListener thumbShapeListener = new PropertyChangeListener()
72      {
73  
74          @Override
75          public void propertyChange(PropertyChangeEvent evt)
76          {
77              MultiThumbSliderUI.this.slider.repaint();
78          }
79  
80      };
81  
82      /**
83       * A thumb shape.
84       */
85      public static enum Thumb {
86          Circle() {
87              @Override
88              public Shape getShape(float width, float height, boolean leftEdge, boolean rightEdge)
89              {
90                  Ellipse2D e = new Ellipse2D.Float(-width / 2f, -height / 2f, width, height);
91                  return e;
92              }
93          },
94          Triangle() {
95              @Override
96              public Shape getShape(float width, float height, boolean leftEdge, boolean rightEdge)
97              {
98                  float k = width / 2;
99                  GeneralPath p = new GeneralPath();
100                 float r = 5;
101 
102                 if ((leftEdge) && (!rightEdge))
103                 {
104                     k = k * 2;
105                     p.moveTo(0, height / 2);
106                     p.lineTo(-k, height / 2 - k);
107                     p.lineTo(-k, -height / 2 + r);
108                     p.curveTo(-k, -height / 2, -k, -height / 2, -k + r, -height / 2);
109                     p.lineTo(0, -height / 2);
110                     p.closePath();
111                 }
112                 else if ((rightEdge) && (!leftEdge))
113                 {
114                     k = k * 2;
115                     p.moveTo(0, -height / 2);
116                     p.lineTo(k - r, -height / 2);
117                     p.curveTo(k, -height / 2, k, -height / 2, k, -height / 2 + r);
118                     p.lineTo(k, height / 2 - k);
119                     p.lineTo(0, height / 2);
120                     p.closePath();
121                 }
122                 else
123                 {
124                     p.moveTo(0, height / 2);
125                     p.lineTo(-k, height / 2 - k);
126                     p.lineTo(-k, -height / 2 + r);
127                     p.curveTo(-k, -height / 2, -k, -height / 2, -k + r, -height / 2);
128                     p.lineTo(k - r, -height / 2);
129                     p.curveTo(k, -height / 2, k, -height / 2, k, -height / 2 + r);
130                     p.lineTo(k, height / 2 - k);
131                     p.closePath();
132                 }
133                 return p;
134             }
135         },
136         Rectangle() {
137             @Override
138             public Shape getShape(float width, float height, boolean leftEdge, boolean rightEdge)
139             {
140                 if ((leftEdge) && (!rightEdge))
141                 {
142                     return new Rectangle2D.Float(-width, -height / 2, width, height);
143                 }
144                 else if ((rightEdge) && (!leftEdge))
145                 {
146                     return new Rectangle2D.Float(0, -height / 2, width, height);
147                 }
148                 else
149                 {
150                     return new Rectangle2D.Float(-width / 2, -height / 2, width, height);
151                 }
152             }
153         },
154         Hourglass() {
155             @Override
156             public Shape getShape(float width, float height, boolean leftEdge, boolean rightEdge)
157             {
158                 GeneralPath p = new GeneralPath();
159                 if ((leftEdge) && (!rightEdge))
160                 {
161                     float k = width;
162                     p.moveTo(-width, -height / 2);
163                     p.lineTo(0, -height / 2);
164                     p.lineTo(0, height / 2);
165                     p.lineTo(-width, height / 2);
166                     p.lineTo(0, height / 2 - k);
167                     p.lineTo(0, -height / 2 + k);
168                     p.closePath();
169                 }
170                 else if ((rightEdge) && (!leftEdge))
171                 {
172                     float k = width;
173                     p.moveTo(width, -height / 2);
174                     p.lineTo(0, -height / 2);
175                     p.lineTo(0, height / 2);
176                     p.lineTo(width, height / 2);
177                     p.lineTo(0, height / 2 - k);
178                     p.lineTo(0, -height / 2 + k);
179                     p.closePath();
180                 }
181                 else
182                 {
183                     float k = width / 2;
184                     p.moveTo(-width / 2, -height / 2);
185                     p.lineTo(width / 2, -height / 2);
186                     p.lineTo(0, -height / 2 + k);
187                     p.lineTo(0, height / 2 - k);
188                     p.lineTo(width / 2, height / 2);
189                     p.lineTo(-width / 2, height / 2);
190                     p.lineTo(0, height / 2 - k);
191                     p.lineTo(0, -height / 2 + k);
192                     p.closePath();
193                 }
194                 return p;
195             }
196         };
197 
198         /**
199          * Create a thumb that is centered at (0,0) for a horizontally oriented slider.
200          * @param sliderUI the slider UI this thumb relates to.
201          * @param x the x-coordinate where this thumb is centered.
202          * @param y the y-coordinate where this thumb is centered.
203          * @param width the width of the the thumb (assuming this is a horizontal slider)
204          * @param height the height of the the thumb (assuming this is a horizontal slider)
205          * @param leftEdge true if this is the left-most thumb
206          * @param rightEdge true if this is the right-most thumb.
207          * @return the shape of this thumb.
208          */
209         public Shape getShape(MultiThumbSliderUI<?> sliderUI, float x, float y, int width, int height,
210                 boolean leftEdge, boolean rightEdge)
211         {
212 
213             // TODO: reinstate leftEdge and rightEdge once bug related to nudging
214             // adjacent thumbs is resolved.
215 
216             GeneralPath path = new GeneralPath(getShape(width, height, false, false));
217             if (sliderUI.slider.getOrientation() == SwingConstants.VERTICAL)
218             {
219                 path.transform(AffineTransform.getRotateInstance(-Math.PI / 2));
220             }
221             path.transform(AffineTransform.getTranslateInstance(x, y));
222             return path;
223         }
224 
225         /**
226          * Create a thumb that is centered at (0,0) for a horizontally oriented slider.
227          * @param width the width of the the thumb (assuming this is a horizontal slider)
228          * @param height the height of the the thumb (assuming this is a horizontal slider)
229          * @param leftEdge true if this is the left-most thumb
230          * @param rightEdge true if this is the right-most thumb.
231          * @return the shape of this thumb.
232          */
233         public abstract Shape getShape(float width, float height, boolean leftEdge, boolean rightEdge);
234     }
235 
236     protected MultiThumbSlider<T> slider;
237 
238     /**
239      * The maximum width returned by <code>getMaximumSize()</code>. (or if the slider is vertical, this is the maximum
240      * height.)
241      */
242     int MAX_LENGTH = 300;
243 
244     /**
245      * The minimum width returned by <code>getMinimumSize()</code>. (or if the slider is vertical, this is the minimum
246      * height.)
247      */
248     int MIN_LENGTH = 50;
249 
250     /**
251      * The maximum width returned by <code>getPreferredSize()</code>. (or if the slider is vertical, this is the
252      * preferred height.)
253      */
254     int PREF_LENGTH = 140;
255 
256     /**
257      * The height of a horizontal slider -- or width of a vertical slider.
258      */
259     int DEPTH = 15;
260 
261     /**
262      * The pixel position of the thumbs. This may be x or y coordinates, depending on whether this slider is horizontal
263      * or vertical
264      */
265     int[] thumbPositions = new int[0];
266 
267     /**
268      * A float from zero to one, indicating whether that thumb should be highlighted or not.
269      */
270     protected float[] thumbIndications = new float[0];
271 
272     /** This is used by the animating thread. The field indication is updated until it equals this value. */
273     private float indicationGoal = 0;
274 
275     /**
276      * The overall indication of the thumbs. At one they should be opaque, at zero they should be transparent.
277      */
278     float indication = 0;
279 
280     /** The rectangle the track should be painted in. */
281     protected Rectangle trackRect = new Rectangle(0, 0, 0, 0);
282 
283     public MultiThumbSliderUI(MultiThumbSlider<T> slider)
284     {
285         this.slider = slider;
286     }
287 
288     @Override
289     public Dimension getMaximumSize(JComponent s)
290     {
291         MultiThumbSlider<T> mySlider = (MultiThumbSlider<T>) s;
292         int k = Math.max(this.DEPTH, getPreferredComponentDepth());
293         if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL)
294         {
295             return new Dimension(this.MAX_LENGTH, k);
296         }
297         return new Dimension(k, this.MAX_LENGTH);
298     }
299 
300     @Override
301     public Dimension getMinimumSize(JComponent s)
302     {
303         MultiThumbSlider<T> mySlider = (MultiThumbSlider<T>) s;
304         int k = Math.max(this.DEPTH, getPreferredComponentDepth());
305         if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL)
306         {
307             return new Dimension(this.MIN_LENGTH, k);
308         }
309         return new Dimension(k, this.MIN_LENGTH);
310     }
311 
312     @Override
313     public Dimension getPreferredSize(JComponent s)
314     {
315         MultiThumbSlider<T> mySlider = (MultiThumbSlider<T>) s;
316         int k = Math.max(this.DEPTH, getPreferredComponentDepth());
317         if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL)
318         {
319             return new Dimension(this.PREF_LENGTH, k);
320         }
321         return new Dimension(k, this.PREF_LENGTH);
322     }
323 
324     /**
325      * Return the typical height of a horizontally oriented slider, or the width of the vertically oriented slider.
326      * @return the typical height of a horizontally oriented slider, or the width of the vertically oriented slider.
327      */
328     protected abstract int getPreferredComponentDepth();
329 
330     /**
331      * This records the positions/values of each thumb. This is used when the mouse is pressed, so as the mouse is
332      * dragged values can get replaced and rearranged freely. (Including removing and adding thumbs)
333      */
334     class State
335     {
336         T[] values;
337 
338         float[] positions;
339 
340         int selectedThumb;
341 
342         public State()
343         {
344             this.values = MultiThumbSliderUI.this.slider.getValues();
345             this.positions = MultiThumbSliderUI.this.slider.getThumbPositions();
346             this.selectedThumb = MultiThumbSliderUI.this.slider.getSelectedThumb(false);
347         }
348 
349         public State(State s)
350         {
351             this.selectedThumb = s.selectedThumb;
352             this.positions = new float[s.positions.length];
353             this.values = createSimilarArray(s.values, s.values.length);
354             System.arraycopy(s.positions, 0, this.positions, 0, this.positions.length);
355             System.arraycopy(s.values, 0, this.values, 0, this.values.length);
356         }
357 
358         /** Strip values outside of [0,1] */
359         private void polish()
360         {
361             while (this.positions[0] < 0)
362             {
363                 float[] f2 = new float[this.positions.length - 1];
364                 System.arraycopy(this.positions, 1, f2, 0, this.positions.length - 1);
365                 T[] c2 = createSimilarArray(this.values, this.values.length - 1);
366                 System.arraycopy(this.values, 1, c2, 0, this.positions.length - 1);
367                 this.positions = f2;
368                 this.values = c2;
369                 this.selectedThumb++;
370             }
371             while (this.positions[this.positions.length - 1] > 1)
372             {
373                 float[] f2 = new float[this.positions.length - 1];
374                 System.arraycopy(this.positions, 0, f2, 0, this.positions.length - 1);
375                 T[] c2 = createSimilarArray(this.values, this.values.length - 1);
376                 System.arraycopy(this.values, 0, c2, 0, this.positions.length - 1);
377                 this.positions = f2;
378                 this.values = c2;
379                 this.selectedThumb--;
380             }
381             if (this.selectedThumb >= this.positions.length)
382                 this.selectedThumb = -1;
383         }
384 
385         /** Make the slider reflect this object */
386         public void install()
387         {
388             polish();
389 
390             MultiThumbSliderUI.this.slider.setValues(this.positions, this.values);
391             MultiThumbSliderUI.this.slider.setSelectedThumb(this.selectedThumb);
392         }
393 
394         /**
395          * This is a kludgy casting trick to make our arrays mesh with generics.
396          * @param src source array
397          * @param length the length
398          * @return array of type T
399          */
400         private T[] createSimilarArray(T[] src, int length)
401         {
402             Class<?> componentType = src.getClass().getComponentType();
403             return (T[]) Array.newInstance(componentType, length);
404         }
405 
406         public void removeThumb(int index)
407         {
408             float[] f = new float[this.positions.length - 1];
409             T[] c = createSimilarArray(this.values, this.values.length - 1);
410             System.arraycopy(this.positions, 0, f, 0, index);
411             System.arraycopy(this.values, 0, c, 0, index);
412             System.arraycopy(this.positions, index + 1, f, index, f.length - index);
413             System.arraycopy(this.values, index + 1, c, index, f.length - index);
414             this.positions = f;
415             this.values = c;
416             this.selectedThumb = -1;
417         }
418 
419         public boolean setPosition(int thumbIndex, float newPosition)
420         {
421             return setPosition(thumbIndex, newPosition, true);
422         }
423 
424         private boolean isCrossover(int thumbIndexA, int thumbIndexB, float newThumbBPosition)
425         {
426             if (thumbIndexA == thumbIndexB)
427                 return false;
428             int oldState = new Float(this.positions[thumbIndexA]).compareTo(this.positions[thumbIndexB]);
429             int newState = new Float(this.positions[thumbIndexA]).compareTo(newThumbBPosition);
430             if (newState * oldState < 0)
431                 return true;
432             return isOverlap(thumbIndexA, thumbIndexB, newThumbBPosition);
433         }
434 
435         private boolean isOverlap(int thumbIndexA, int thumbIndexB, float newThumbBPosition)
436         {
437             if (thumbIndexA == thumbIndexB)
438                 return false;
439             if (!MultiThumbSliderUI.this.slider.isThumbOverlap())
440             {
441                 Point2D aCenter = getThumbCenter(this.positions[thumbIndexA]);
442                 Point2D bCenter = getThumbCenter(newThumbBPosition);
443                 Rectangle2D aBounds = ShapeBounds.getBounds(getThumbShape(thumbIndexA, aCenter));
444                 Rectangle2D bBounds = ShapeBounds.getBounds(getThumbShape(thumbIndexB, bCenter));
445                 return aBounds.intersects(bBounds) || aBounds.equals(bBounds);
446             }
447             return false;
448         }
449 
450         private boolean setPosition(int thumbIndex, float newPosition, boolean revise)
451         {
452             Collision c = MultiThumbSliderUI.this.slider.getCollisionPolicy();
453             if (Collision.JUMP_OVER_OTHER.equals(c) && (!MultiThumbSliderUI.this.slider.isThumbOverlap()))
454             {
455                 newPosition = Math.max(0, Math.min(1, newPosition));
456                 for (int a = 0; a < this.positions.length; a++)
457                 {
458                     if (isOverlap(a, thumbIndex, newPosition))
459                     {
460                         if (revise)
461                         {
462                             float alternative;
463 
464                             int maxWidth = Math.max(getThumbSize(a).width, getThumbSize(thumbIndex).width);
465                             float trackSize =
466                                     MultiThumbSliderUI.this.slider.getOrientation() == SwingConstants.HORIZONTAL ? MultiThumbSliderUI.this.trackRect.width
467                                             : MultiThumbSliderUI.this.trackRect.height;
468                             newPosition = Math.max(0, Math.min(1, newPosition));
469                             // offset is measured in pixels
470                             for (int offset = 0; offset < 4 * maxWidth; offset++)
471                             {
472                                 alternative = Math.max(0, Math.min(1, newPosition - ((float) offset) / trackSize));
473                                 if (!isOverlap(a, thumbIndex, alternative))
474                                 {
475                                     return setPosition(thumbIndex, alternative, false);
476                                 }
477                                 alternative = Math.max(0, Math.min(1, newPosition + ((float) offset) / trackSize));
478                                 if (!isOverlap(a, thumbIndex, alternative))
479                                 {
480                                     return setPosition(thumbIndex, alternative, false);
481                                 }
482                             }
483                             return false;
484                         }
485                         return false;
486                     }
487                 }
488             }
489             else if (Collision.STOP_AGAINST.equals(c))
490             {
491                 for (int a = 0; a < this.positions.length; a++)
492                 {
493                     if (isCrossover(a, thumbIndex, newPosition))
494                     {
495                         // this move would cross thumbIndex over an existing thumb. This violates the collision policy:
496                         if (revise)
497                         {
498                             float alternative;
499 
500                             int maxWidth = Math.max(getThumbSize(a).width, getThumbSize(thumbIndex).width);
501                             float trackSize =
502                                     MultiThumbSliderUI.this.slider.getOrientation() == SwingConstants.HORIZONTAL ? MultiThumbSliderUI.this.trackRect.width
503                                             : MultiThumbSliderUI.this.trackRect.height;
504                             // offset is measured in pixels
505                             for (int offset = 0; offset < 2 * maxWidth; offset++)
506                             {
507                                 if (this.positions[a] > this.positions[thumbIndex])
508                                 {
509                                     alternative = this.positions[a] - ((float) offset) / trackSize;
510                                 }
511                                 else
512                                 {
513                                     alternative = this.positions[a] + ((float) offset) / trackSize;
514                                 }
515                                 if (!isCrossover(a, thumbIndex, alternative))
516                                 {
517                                     return setPosition(thumbIndex, alternative, false);
518                                 }
519                             }
520                             return false;
521                         }
522 
523                         return false;
524                     }
525                 }
526             }
527             else if (Collision.NUDGE_OTHER.equals(c))
528             {
529                 if (revise)
530                 {
531                     final Set<Integer> processedThumbs = new LinkedHashSet<Integer>();
532                     processedThumbs.add(-1);
533 
534                     class NudgeRequest
535                     {
536                         /** The index of the thumb this request wants to move. */
537                         final int thumbIndex;
538 
539                         /** The original value of this thumb. */
540                         final float startingValue;
541 
542                         /** The amount we're asking to change this value by. */
543                         final float requestedDelta;
544 
545                         NudgeRequest(int thumbIndex, float startingValue, float requestedDelta)
546                         {
547                             this.thumbIndex = thumbIndex;
548                             this.startingValue = startingValue;
549                             this.requestedDelta = requestedDelta;
550                         }
551 
552                         void process()
553                         {
554                             float span;
555                             if (MultiThumbSliderUI.this.slider.isThumbOverlap())
556                             {
557                                 span = 0;
558                             }
559                             else
560                             {
561                                 span = (float) ShapeBounds.getBounds(getThumbShape(this.thumbIndex)).getWidth();
562                                 if (MultiThumbSliderUI.this.slider.getOrientation() == SwingConstants.HORIZONTAL)
563                                 {
564                                     span = span / ((float) MultiThumbSliderUI.this.trackRect.width);
565                                 }
566                                 else
567                                 {
568                                     span = span / ((float) MultiThumbSliderUI.this.trackRect.height);
569                                 }
570                             }
571                             int[] neighbors = getNeighbors(this.thumbIndex);
572                             float newPosition = this.startingValue + this.requestedDelta;
573                             processedThumbs.add(this.thumbIndex);
574 
575                             if (neighbors[0] == -1 && newPosition < 0)
576                             {
577                                 setPosition(this.thumbIndex, 0, false);
578                             }
579                             else if (neighbors[1] == -1 && newPosition > 1)
580                             {
581                                 setPosition(this.thumbIndex, 1, false);
582                             }
583                             else if (processedThumbs.add(neighbors[0])
584                                     && (newPosition < State.this.positions[neighbors[0]] || Math.abs(State.this.positions[neighbors[0]]
585                                             - newPosition) < span - .0001))
586                             {
587                                 NudgeRequest dependsOn =
588                                         new NudgeRequest(neighbors[0], State.this.positions[neighbors[0]], (newPosition - span)
589                                                 - State.this.positions[neighbors[0]]);
590                                 dependsOn.process();
591                                 setPosition(this.thumbIndex, State.this.positions[dependsOn.thumbIndex] + span, false);
592                             }
593                             else if (processedThumbs.add(neighbors[1])
594                                     && (newPosition > State.this.positions[neighbors[1]] || Math.abs(State.this.positions[neighbors[1]]
595                                             - newPosition) < span - .0001))
596                             {
597                                 NudgeRequest dependsOn =
598                                         new NudgeRequest(neighbors[1], State.this.positions[neighbors[1]], (newPosition + span)
599                                                 - State.this.positions[neighbors[1]]);
600                                 dependsOn.process();
601                                 setPosition(this.thumbIndex, State.this.positions[dependsOn.thumbIndex] - span, false);
602                             }
603                             else
604                             {
605                                 setPosition(this.thumbIndex, this.startingValue + this.requestedDelta, false);
606                             }
607                         }
608                     }
609 
610                     float originalValue = this.positions[thumbIndex];
611                     NudgeRequest rootRequest =
612                             new NudgeRequest(thumbIndex, this.positions[thumbIndex], newPosition - this.positions[thumbIndex]);
613                     rootRequest.process();
614                     return this.positions[thumbIndex] != originalValue;
615                 }
616 
617             }
618             this.positions[thumbIndex] = newPosition;
619             return true;
620         }
621 
622         /**
623          * Return the left (lesser) neighbor and the right (greater) neighbor. Either index may be -1 if it is not
624          * available.
625          * @param thumbIndex the index of the thumb to examine.
626          * @return the left (lesser) neighbor and the right (greater) neighbor.
627          */
628         int[] getNeighbors(int thumbIndex)
629         {
630             float leftNeighborDelta = 10;
631             float rightNeighborDelta = 10;
632             int leftNeighbor = -1;
633             int rightNeighbor = -1;
634             for (int a = 0; a < this.positions.length; a++)
635             {
636                 if (a != thumbIndex)
637                 {
638                     if (this.positions[thumbIndex] < this.positions[a])
639                     {
640                         float delta = this.positions[a] - this.positions[thumbIndex];
641                         if (delta < rightNeighborDelta)
642                         {
643                             rightNeighborDelta = delta;
644                             rightNeighbor = a;
645                         }
646                     }
647                     else if (this.positions[thumbIndex] > this.positions[a])
648                     {
649                         float delta = this.positions[thumbIndex] - this.positions[a];
650                         if (delta < leftNeighborDelta)
651                         {
652                             leftNeighborDelta = delta;
653                             leftNeighbor = a;
654                         }
655                     }
656                 }
657             }
658             return new int[]{leftNeighbor, rightNeighbor};
659         }
660     }
661 
662     Thread animatingThread = null;
663 
664     Runnable animatingRunnable = new Runnable()
665     {
666         public void run()
667         {
668             boolean finished = false;
669             while (!finished)
670             {
671                 synchronized (MultiThumbSliderUI.this)
672                 {
673                     finished = true;
674                     for (int a = 0; a < MultiThumbSliderUI.this.thumbIndications.length; a++)
675                     {
676                         if (a != MultiThumbSliderUI.this.slider.getSelectedThumb())
677                         {
678                             if (a == MultiThumbSliderUI.this.currentIndicatedThumb)
679                             {
680                                 if (MultiThumbSliderUI.this.thumbIndications[a] < 1)
681                                 {
682                                     MultiThumbSliderUI.this.thumbIndications[a] = Math.min(1, MultiThumbSliderUI.this.thumbIndications[a] + .025f);
683                                     finished = false;
684                                 }
685                             }
686                             else
687                             {
688                                 if (MultiThumbSliderUI.this.thumbIndications[a] > 0)
689                                 {
690                                     MultiThumbSliderUI.this.thumbIndications[a] = Math.max(0, MultiThumbSliderUI.this.thumbIndications[a] - .025f);
691                                     finished = false;
692                                 }
693                             }
694                         }
695                         else
696                         {
697                             // the selected thumb is painted as selected,
698                             // so there's no indication to animate.
699                             // just set the indication to whatever it should
700                             // be and move on. No repainting.
701                             if (a == MultiThumbSliderUI.this.currentIndicatedThumb)
702                             {
703                                 MultiThumbSliderUI.this.thumbIndications[a] = 1;
704                             }
705                             else
706                             {
707                                 MultiThumbSliderUI.this.thumbIndications[a] = 0;
708                             }
709                         }
710                     }
711                     if (MultiThumbSliderUI.this.indicationGoal > MultiThumbSliderUI.this.indication + .01f)
712                     {
713                         if (MultiThumbSliderUI.this.indication < .99f)
714                         {
715                             MultiThumbSliderUI.this.indication = Math.min(1, MultiThumbSliderUI.this.indication + .1f);
716                             finished = false;
717                         }
718                     }
719                     else if (MultiThumbSliderUI.this.indicationGoal < MultiThumbSliderUI.this.indication - .01f)
720                     {
721                         if (MultiThumbSliderUI.this.indication > .01f)
722                         {
723                             MultiThumbSliderUI.this.indication = Math.max(0, MultiThumbSliderUI.this.indication - .1f);
724                             finished = false;
725                         }
726                     }
727                 }
728                 if (!finished)
729                     MultiThumbSliderUI.this.slider.repaint();
730 
731                 // rest a little bit
732                 long t = System.currentTimeMillis();
733                 while (System.currentTimeMillis() - t < 20)
734                 {
735                     try
736                     {
737                         Thread.sleep(10);
738                     }
739                     catch (Exception e)
740                     {
741                         Thread.yield();
742                     }
743                 }
744             }
745         }
746     };
747 
748     private int currentIndicatedThumb = -1;
749 
750     protected boolean mouseInside = false;
751 
752     protected boolean mouseIsDown = false;
753 
754     private State pressedState;
755 
756     private int dx, dy;
757 
758     public void mousePressed(MouseEvent e)
759     {
760         this.dx = 0;
761         this.dy = 0;
762 
763         if (this.slider.isEnabled() == false)
764             return;
765 
766         if (e.getClickCount() >= 2)
767         {
768             if (this.slider.doDoubleClick(e.getX(), e.getY()))
769             {
770                 e.consume();
771                 return;
772             }
773         }
774         else if (e.isPopupTrigger())
775         {
776             int x = e.getX();
777             int y = e.getY();
778             if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
779             {
780                 if (x < this.trackRect.x || x > this.trackRect.x + this.trackRect.width)
781                     return;
782                 y = this.trackRect.y + this.trackRect.height;
783             }
784             else
785             {
786                 if (y < this.trackRect.y || y > this.trackRect.y + this.trackRect.height)
787                     return;
788                 x = this.trackRect.x + this.trackRect.width;
789             }
790             if (this.slider.doPopup(x, y))
791             {
792                 e.consume();
793                 return;
794             }
795         }
796         this.mouseIsDown = true;
797         mouseMoved(e);
798 
799         if (e.getSource() != this.slider)
800         {
801             throw new RuntimeException("only install this UI on the GradientSlider it was constructed with");
802         }
803         this.slider.requestFocus();
804 
805         int index = getIndex(e);
806         if (index != -1)
807         {
808             if (this.slider.getOrientation() == SwingConstants.HORIZONTAL)
809             {
810                 this.dx = -e.getX() + this.thumbPositions[index];
811             }
812             else
813             {
814                 this.dy = -e.getY() + this.thumbPositions[index];
815             }
816         }
817 
818         if (index != -1)
819         {
820             this.slider.setSelectedThumb(index);
821             e.consume();
822         }
823         else
824         {
825             if (this.slider.isAutoAdding())
826             {
827                 float k;
828 
829                 int v;
830                 if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
831                 {
832                     v = e.getX();
833                 }
834                 else
835                 {
836                     v = e.getY();
837                 }
838 
839                 if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
840                 {
841                     k = ((float) (v - this.trackRect.x)) / ((float) this.trackRect.width);
842                     if (this.slider.isInverted())
843                         k = 1 - k;
844                 }
845                 else
846                 {
847                     k = ((float) (v - this.trackRect.y)) / ((float) this.trackRect.height);
848                     if (this.slider.isInverted() == false)
849                         k = 1 - k;
850                 }
851                 if (k > 0 && k < 1)
852                 {
853                     int added = this.slider.addThumb(k);
854                     this.slider.setSelectedThumb(added);
855                 }
856                 e.consume();
857             }
858             else
859             {
860                 if (this.slider.getSelectedThumb() != -1)
861                 {
862                     this.slider.setSelectedThumb(-1);
863                     e.consume();
864                 }
865             }
866         }
867         this.pressedState = new State();
868     }
869 
870     private int getIndex(MouseEvent e)
871     {
872         int v;
873         Rectangle2D shapeSum = new Rectangle2D.Double(this.trackRect.x, this.trackRect.y, this.trackRect.width, this.trackRect.height);
874         for (int a = 0; a < this.slider.getThumbCount(); a++)
875         {
876             shapeSum.add(ShapeBounds.getBounds(getThumbShape(a)));
877         }
878         if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
879         {
880             v = e.getX();
881             if (v < shapeSum.getMinX() || v > shapeSum.getMaxX())
882             {
883                 return -1; // didn't click in the track;
884             }
885         }
886         else
887         {
888             v = e.getY();
889             if (v < shapeSum.getMinY() || v > shapeSum.getMaxY())
890             {
891                 return -1;
892             }
893         }
894         int min = Math.abs(v - this.thumbPositions[0]);
895         int minIndex = 0;
896         for (int a = 1; a < this.thumbPositions.length; a++)
897         {
898             int distance = Math.abs(v - this.thumbPositions[a]);
899             if (distance < min)
900             {
901                 min = distance;
902                 minIndex = a;
903             }
904             else if (distance == min)
905             {
906                 // two thumbs may perfectly overlap
907                 if (v < this.thumbPositions[a])
908                 {
909                     // you clicked to the left of the fulcrum, so we should side with the smaller index
910                     if (this.slider.isInverted())
911                     {
912                         // ... unless it's inverted:
913                         minIndex = a;
914                     }
915                 }
916                 else
917                 {
918                     if (!this.slider.isInverted())
919                         minIndex = a;
920                 }
921             }
922         }
923         if (min < getThumbSize(minIndex).width / 2)
924         {
925             return minIndex;
926         }
927         return -1;
928     }
929 
930     public void mouseEntered(MouseEvent e)
931     {
932         mouseMoved(e);
933     }
934 
935     public void mouseExited(MouseEvent e)
936     {
937         setCurrentIndicatedThumb(-1);
938         setMouseInside(false);
939     }
940 
941     public void mouseClicked(MouseEvent e)
942     {
943     }
944 
945     public void mouseMoved(MouseEvent e)
946     {
947         if (this.slider.isEnabled() == false)
948             return;
949 
950         int i = getIndex(e);
951         setCurrentIndicatedThumb(i);
952         boolean b = (e.getX() >= 0 && e.getX() < this.slider.getWidth() && e.getY() >= 0 && e.getY() < this.slider.getHeight());
953         if (this.mouseIsDown)
954             b = true;
955         setMouseInside(b);
956     }
957 
958     protected Dimension getThumbSize(int thumbIndex)
959     {
960         return new Dimension(16, 16);
961     }
962 
963     /**
964      * Create the shape used to render a specific thumb.
965      * @param thumbIndex the index of the thumb to render.
966      * @return the shape used to render a specific thumb.
967      * @see #getThumbCenter(int)
968      * @see #getThumb(int)
969      */
970     public Shape getThumbShape(int thumbIndex)
971     {
972         return getThumbShape(thumbIndex, null);
973     }
974 
975     /**
976      * Create the shape used to render a specific thumb.
977      * @param thumbIndex the index of the thumb to render.
978      * @param center an optional center to focus the thumb around. If this is null then the current (real) center is
979      *            used, but this can be supplied manually to consider possible shapes and visual size constraints based
980      *            on the current collision policy.
981      * @return the shape used to render a specific thumb.
982      * @see #getThumbCenter(int)
983      * @see #getThumb(int)
984      */
985     public Shape getThumbShape(int thumbIndex, Point2D center)
986     {
987         Thumb thumb = getThumb(thumbIndex);
988         if (center == null)
989             center = getThumbCenter(thumbIndex);
990         Dimension d = getThumbSize(thumbIndex);
991         return thumb.getShape(this, (float) center.getX(), (float) center.getY(), d.width, d.height, thumbIndex == 0,
992                 thumbIndex == this.slider.getThumbCount() - 1);
993     }
994 
995     /**
996      * Calculate the thumb center
997      * @param thumbIndex the index of the thumb to consult.
998      * @return the center of a given thumb
999      */
1000     public Point2D getThumbCenter(int thumbIndex)
1001     {
1002         float[] values = this.slider.getThumbPositions();
1003         float n = values[thumbIndex];
1004 
1005         return getThumbCenter(n);
1006     }
1007 
1008     /**
1009      * Calculate the thumb center based on a fractional position
1010      * @param position a value from [0,1]
1011      * @return the center of a potential thumbnail for this position.
1012      */
1013     public Point2D getThumbCenter(float position)
1014     {
1015         /*
1016          * I'm on the fence about whether to document this as allowing null or not. Does this occur in the wild? If so:
1017          * is this more an internal error than something we need to document/allow for?
1018          */
1019         if (position < 0 || position > 1)
1020             return null;
1021 
1022         if (this.slider.getOrientation() == MultiThumbSlider.VERTICAL)
1023         {
1024             float y;
1025             float height = (float) this.trackRect.height;
1026             float x = (float) this.trackRect.getCenterX();
1027             if (this.slider.isInverted())
1028             {
1029                 y = (float) (position * height + this.trackRect.y);
1030             }
1031             else
1032             {
1033                 y = (float) ((1 - position) * height + this.trackRect.y);
1034             }
1035             return new Point2D.Float(x, y);
1036         }
1037         else
1038         {
1039             float x;
1040             float width = (float) this.trackRect.width;
1041             float y = (float) this.trackRect.getCenterY();
1042             if (this.slider.isInverted())
1043             {
1044                 x = (float) ((1 - position) * width + this.trackRect.x);
1045             }
1046             else
1047             {
1048                 x = (float) (position * width + this.trackRect.x);
1049             }
1050             return new Point2D.Float(x, y);
1051         }
1052     }
1053 
1054     /**
1055      * Return the Thumb option used to render a specific thumb. The default implementation here consults the client
1056      * property MultiThumbSliderUI.THUMB_SHAPE_PROPERTY, and returns Circle by default.
1057      * @param thumbIndex the index of the thumb to render.
1058      * @return the Thumb option used to render a specific thumb.
1059      */
1060     public Thumb getThumb(int thumbIndex)
1061     {
1062         Thumb thumb = getProperty(this.slider, THUMB_SHAPE_PROPERTY, Thumb.Circle);
1063         return thumb;
1064     }
1065 
1066     private void setCurrentIndicatedThumb(int i)
1067     {
1068         if (getProperty(this.slider, "MultiThumbSlider.indicateThumb", "true").equals("false"))
1069         {
1070             // never activate a specific thumb
1071             i = -1;
1072         }
1073         this.currentIndicatedThumb = i;
1074         boolean finished = true;
1075         for (int a = 0; a < this.thumbIndications.length; a++)
1076         {
1077             if (a == this.currentIndicatedThumb)
1078             {
1079                 if (this.thumbIndications[a] != 1)
1080                 {
1081                     finished = false;
1082                 }
1083             }
1084             else
1085             {
1086                 if (this.thumbIndications[a] != 0)
1087                 {
1088                     finished = false;
1089                 }
1090             }
1091         }
1092         if (!finished)
1093         {
1094             synchronized (MultiThumbSliderUI.this)
1095             {
1096                 if (this.animatingThread == null || this.animatingThread.isAlive() == false)
1097                 {
1098                     this.animatingThread = new Thread(this.animatingRunnable);
1099                     this.animatingThread.start();
1100                 }
1101             }
1102         }
1103     }
1104 
1105     private void setMouseInside(boolean b)
1106     {
1107         this.mouseInside = b;
1108         updateIndication();
1109     }
1110 
1111     public void mouseDragged(MouseEvent e)
1112     {
1113         if (this.slider.isEnabled() == false)
1114             return;
1115 
1116         e.translatePoint(this.dx, this.dy);
1117 
1118         mouseMoved(e);
1119         if (this.pressedState != null && this.pressedState.selectedThumb != -1)
1120         {
1121             this.slider.setValueIsAdjusting(true);
1122 
1123             State newState = new State(this.pressedState);
1124             float v;
1125             boolean outside;
1126             if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
1127             {
1128                 v = ((float) (e.getX() - this.trackRect.x)) / ((float) this.trackRect.width);
1129                 if (this.slider.isInverted())
1130                     v = 1 - v;
1131                 outside = (e.getY() < this.trackRect.y - 10) || (e.getY() > this.trackRect.y + this.trackRect.height + 10);
1132 
1133                 // don't whack the thumb off the slider if you happen to be *near* the edge:
1134                 if (e.getX() > this.trackRect.x - 10 && e.getX() < this.trackRect.x + this.trackRect.width + 10)
1135                 {
1136                     if (v < 0)
1137                         v = 0;
1138                     if (v > 1)
1139                         v = 1;
1140                 }
1141             }
1142             else
1143             {
1144                 v = ((float) (e.getY() - this.trackRect.y)) / ((float) this.trackRect.height);
1145                 if (this.slider.isInverted() == false)
1146                     v = 1 - v;
1147                 outside = (e.getX() < this.trackRect.x - 10) || (e.getX() > this.trackRect.x + this.trackRect.width + 10);
1148 
1149                 if (e.getY() > this.trackRect.y - 10 && e.getY() < this.trackRect.y + this.trackRect.height + 10)
1150                 {
1151                     if (v < 0)
1152                         v = 0;
1153                     if (v > 1)
1154                         v = 1;
1155                 }
1156             }
1157             if (newState.positions.length <= this.slider.getMinimumThumbnailCount())
1158             {
1159                 outside = false; // I don't care if you are outside: no removing!
1160             }
1161             newState.setPosition(newState.selectedThumb, v);
1162 
1163             // because we delegate mouseReleased() to this method:
1164             if (outside && this.slider.isThumbRemovalAllowed())
1165             {
1166                 newState.removeThumb(newState.selectedThumb);
1167             }
1168             if (validatePositions(newState))
1169             {
1170                 newState.install();
1171             }
1172             e.consume();
1173         }
1174     }
1175 
1176     public void mouseReleased(MouseEvent e)
1177     {
1178         if (this.slider.isEnabled() == false)
1179             return;
1180 
1181         this.mouseIsDown = false;
1182         if (this.pressedState != null && this.slider.getThumbCount() <= this.pressedState.positions.length)
1183         {
1184             mouseDragged(e); // go ahead and commit this final location
1185         }
1186         if (this.slider.isValueAdjusting())
1187         {
1188             this.slider.setValueIsAdjusting(false);
1189         }
1190         this.slider.repaint();
1191 
1192         if (e.isPopupTrigger() && this.slider.doPopup(e.getX(), e.getY()))
1193         {
1194             // on windows popuptriggers happen on mouseRelease
1195             e.consume();
1196             return;
1197         }
1198     }
1199 
1200     /**
1201      * This retrieves a property. If the component has this property manually set (by calling
1202      * <code>component.putClientProperty()</code>), then that value will be returned. Otherwise this method refers to
1203      * <code>UIManager.get()</code>. If that value is missing, this returns <code>defaultValue</code>
1204      * @param jc component
1205      * @param propertyName the property name
1206      * @param defaultValue if no other value is found, this is returned
1207      * @return the property value
1208      */
1209     public static <K> K getProperty(JComponent jc, String propertyName, K defaultValue)
1210     {
1211         Object jcValue = jc.getClientProperty(propertyName);
1212         if (jcValue != null)
1213             return (K) jcValue;
1214         Object uiValue = UIManager.get(propertyName);
1215         if (uiValue != null)
1216             return (K) uiValue;
1217         return defaultValue;
1218     }
1219 
1220     /**
1221      * Makes sure the thumbs are in the right order.
1222      * @param state state
1223      * @return true if the thumbs are valid. False if there are two thumbs with the same value (this is not allowed)
1224      */
1225     protected boolean validatePositions(State state)
1226     {
1227         float[] p = state.positions;
1228         Object[] c = state.values;
1229 
1230         /**
1231          * Don't let the user position a thumb outside of [0,1] if there are only 2 colors: colors outside [0,1] are
1232          * deleted, and we can't delete colors so we get less than 2.
1233          */
1234         if (p.length <= this.slider.getMinimumThumbnailCount() || (!this.slider.isThumbRemovalAllowed()))
1235         {
1236             /**
1237              * Since the user can only manipulate 1 thumb at a time, only 1 thumb should be outside the domain of [0,1].
1238              * So we *don't* have to reorganize c when we change p
1239              */
1240             for (int a = 0; a < p.length; a++)
1241             {
1242                 if (p[a] < 0)
1243                 {
1244                     p[a] = 0;
1245                 }
1246                 else if (p[a] > 1)
1247                 {
1248                     p[a] = 1;
1249                 }
1250             }
1251         }
1252 
1253         // validate the new positions:
1254         boolean checkAgain = true;
1255         while (checkAgain)
1256         {
1257             checkAgain = false;
1258             for (int a = 0; a < p.length - 1; a++)
1259             {
1260                 if (p[a] > p[a + 1])
1261                 {
1262                     checkAgain = true;
1263 
1264                     float swap1 = p[a];
1265                     p[a] = p[a + 1];
1266                     p[a + 1] = swap1;
1267                     Object swap2 = c[a];
1268                     c[a] = c[a + 1];
1269                     c[a + 1] = swap2;
1270 
1271                     if (a == state.selectedThumb)
1272                     {
1273                         state.selectedThumb = a + 1;
1274                     }
1275                     else if (a + 1 == state.selectedThumb)
1276                     {
1277                         state.selectedThumb = a;
1278                     }
1279                 }
1280             }
1281         }
1282 
1283         return true;
1284     }
1285 
1286     FocusListener focusListener = new FocusListener()
1287     {
1288         public void focusLost(FocusEvent e)
1289         {
1290             Component c = (Component) e.getSource();
1291             if (getProperty(MultiThumbSliderUI.this.slider, "MultiThumbSlider.indicateComponent", "false").toString().equals("true"))
1292             {
1293                 MultiThumbSliderUI.this.slider.setSelectedThumb(-1);
1294             }
1295             updateIndication();
1296             c.repaint();
1297         }
1298 
1299         public void focusGained(FocusEvent e)
1300         {
1301             Component c = (Component) e.getSource();
1302             int i = MultiThumbSliderUI.this.slider.getSelectedThumb(false);
1303             if (i == -1)
1304             {
1305                 int direction = 1;
1306                 if (MultiThumbSliderUI.this.slider.getOrientation() == MultiThumbSlider.VERTICAL)
1307                     direction *= -1;
1308                 if (MultiThumbSliderUI.this.slider.isInverted())
1309                     direction *= -1;
1310                 MultiThumbSliderUI.this.slider.setSelectedThumb((direction == 1) ? 0 : MultiThumbSliderUI.this.slider.getThumbCount() - 1);
1311             }
1312             updateIndication();
1313             c.repaint();
1314         }
1315     };
1316 
1317     /**
1318      * This will try to add a thumb between index1 and index2.
1319      * <P>
1320      * This method will not add a thumb if there is already a very small distance between these two endpoints
1321      * @param index1 low value
1322      * @param index2 high value
1323      * @return true if a new thumb was added
1324      */
1325     protected boolean addThumb(int index1, int index2)
1326     {
1327         float pos1 = 0;
1328         float pos2 = 1;
1329         int min;
1330         int max;
1331         if (index1 < index2)
1332         {
1333             min = index1;
1334             max = index2;
1335         }
1336         else
1337         {
1338             min = index2;
1339             max = index1;
1340         }
1341         float[] positions = this.slider.getThumbPositions();
1342         if (min >= 0)
1343             pos1 = positions[min];
1344         if (max < positions.length)
1345             pos2 = positions[max];
1346 
1347         if (pos2 - pos1 < .05)
1348             return false;
1349 
1350         float newPosition = (pos1 + pos2) / 2f;
1351         this.slider.setSelectedThumb(this.slider.addThumb(newPosition));
1352 
1353         return true;
1354     }
1355 
1356     KeyListener keyListener = new KeyListener()
1357     {
1358         public void keyPressed(KeyEvent e)
1359         {
1360             if (MultiThumbSliderUI.this.slider.isEnabled() == false)
1361                 return;
1362 
1363             if (e.getSource() != MultiThumbSliderUI.this.slider)
1364                 throw new RuntimeException("only install this UI on the GradientSlider it was constructed with");
1365             int i = MultiThumbSliderUI.this.slider.getSelectedThumb();
1366             int code = e.getKeyCode();
1367             int orientation = MultiThumbSliderUI.this.slider.getOrientation();
1368             if (i != -1 && (code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_LEFT)
1369                     && orientation == MultiThumbSlider.HORIZONTAL
1370                     && e.getModifiers() == Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())
1371             {
1372                 // insert a new thumb
1373                 int i2;
1374                 if ((code == KeyEvent.VK_RIGHT && MultiThumbSliderUI.this.slider.isInverted() == false)
1375                         || (code == KeyEvent.VK_LEFT && MultiThumbSliderUI.this.slider.isInverted() == true))
1376                 {
1377                     i2 = i + 1;
1378                 }
1379                 else
1380                 {
1381                     i2 = i - 1;
1382                 }
1383                 addThumb(i, i2);
1384                 e.consume();
1385                 return;
1386             }
1387             else if (i != -1 && (code == KeyEvent.VK_UP || code == KeyEvent.VK_DOWN)
1388                     && orientation == MultiThumbSlider.VERTICAL
1389                     && e.getModifiers() == Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())
1390             {
1391                 // insert a new thumb
1392                 int i2;
1393                 if ((code == KeyEvent.VK_UP && MultiThumbSliderUI.this.slider.isInverted() == false)
1394                         || (code == KeyEvent.VK_DOWN && MultiThumbSliderUI.this.slider.isInverted() == true))
1395                 {
1396                     i2 = i + 1;
1397                 }
1398                 else
1399                 {
1400                     i2 = i - 1;
1401                 }
1402                 addThumb(i, i2);
1403                 e.consume();
1404                 return;
1405             }
1406             else if (code == KeyEvent.VK_DOWN && orientation == MultiThumbSlider.HORIZONTAL && i != -1)
1407             {
1408                 // popup up!
1409                 int x =
1410                         MultiThumbSliderUI.this.slider.isInverted() ? (int) (MultiThumbSliderUI.this.trackRect.x + MultiThumbSliderUI.this.trackRect.width
1411                                 * (1 - MultiThumbSliderUI.this.slider.getThumbPositions()[i])) : (int) (MultiThumbSliderUI.this.trackRect.x + MultiThumbSliderUI.this.trackRect.width
1412                                 * MultiThumbSliderUI.this.slider.getThumbPositions()[i]);
1413                 int y = MultiThumbSliderUI.this.trackRect.y + MultiThumbSliderUI.this.trackRect.height;
1414                 if (MultiThumbSliderUI.this.slider.doPopup(x, y))
1415                 {
1416                     e.consume();
1417                     return;
1418                 }
1419             }
1420             else if (code == KeyEvent.VK_RIGHT && orientation == MultiThumbSlider.VERTICAL && i != -1)
1421             {
1422                 // popup up!
1423                 int y =
1424                         MultiThumbSliderUI.this.slider.isInverted() ? (int) (MultiThumbSliderUI.this.trackRect.y + MultiThumbSliderUI.this.trackRect.height * MultiThumbSliderUI.this.slider.getThumbPositions()[i])
1425                                 : (int) (MultiThumbSliderUI.this.trackRect.y + MultiThumbSliderUI.this.trackRect.height * (1 - MultiThumbSliderUI.this.slider.getThumbPositions()[i]));
1426                 int x = MultiThumbSliderUI.this.trackRect.x + MultiThumbSliderUI.this.trackRect.width;
1427                 if (MultiThumbSliderUI.this.slider.doPopup(x, y))
1428                 {
1429                     e.consume();
1430                     return;
1431                 }
1432             }
1433             if (i != -1)
1434             {
1435                 // move the selected thumb
1436                 if (code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_DOWN)
1437                 {
1438                     nudge(i, 1);
1439                     e.consume();
1440                 }
1441                 else if (code == KeyEvent.VK_LEFT || code == KeyEvent.VK_UP)
1442                 {
1443                     nudge(i, -1);
1444                     e.consume();
1445                 }
1446                 else if (code == KeyEvent.VK_DELETE || code == KeyEvent.VK_BACK_SPACE)
1447                 {
1448                     if (MultiThumbSliderUI.this.slider.getThumbCount() > MultiThumbSliderUI.this.slider.getMinimumThumbnailCount() && MultiThumbSliderUI.this.slider.isThumbRemovalAllowed())
1449                     {
1450                         MultiThumbSliderUI.this.slider.removeThumb(i);
1451                         e.consume();
1452                     }
1453                 }
1454                 else if (code == KeyEvent.VK_SPACE || code == KeyEvent.VK_ENTER)
1455                 {
1456                     MultiThumbSliderUI.this.slider.doDoubleClick(-1, -1);
1457                 }
1458             }
1459         }
1460 
1461         public void keyReleased(KeyEvent e)
1462         {
1463         }
1464 
1465         public void keyTyped(KeyEvent e)
1466         {
1467         }
1468     };
1469 
1470     PropertyChangeListener propertyListener = new PropertyChangeListener()
1471     {
1472 
1473         public void propertyChange(PropertyChangeEvent e)
1474         {
1475             String name = e.getPropertyName();
1476             if (name.equals(MultiThumbSlider.VALUES_PROPERTY) || name.equals(MultiThumbSlider.ORIENTATION_PROPERTY)
1477                     || name.equals(MultiThumbSlider.INVERTED_PROPERTY))
1478             {
1479                 calculateGeometry();
1480                 MultiThumbSliderUI.this.slider.repaint();
1481             }
1482             else if (name.equals(MultiThumbSlider.SELECTED_THUMB_PROPERTY)
1483                     || name.equals(MultiThumbSlider.PAINT_TICKS_PROPERTY))
1484             {
1485                 MultiThumbSliderUI.this.slider.repaint();
1486             }
1487             else if (name.equals("MultiThumbSlider.indicateComponent"))
1488             {
1489                 setMouseInside(MultiThumbSliderUI.this.mouseInside);
1490                 MultiThumbSliderUI.this.slider.repaint();
1491             }
1492         }
1493 
1494     };
1495 
1496     ComponentListener compListener = new ComponentListener()
1497     {
1498 
1499         public void componentHidden(ComponentEvent e)
1500         {
1501         }
1502 
1503         public void componentMoved(ComponentEvent e)
1504         {
1505         }
1506 
1507         public void componentResized(ComponentEvent e)
1508         {
1509             calculateGeometry();
1510             Component c = (Component) e.getSource();
1511             c.repaint();
1512         }
1513 
1514         public void componentShown(ComponentEvent e)
1515         {
1516         }
1517     };
1518 
1519     protected void updateIndication()
1520     {
1521         synchronized (MultiThumbSliderUI.this)
1522         {
1523             if (this.slider.isEnabled() && (this.slider.hasFocus() || this.mouseInside))
1524             {
1525                 this.indicationGoal = 1;
1526             }
1527             else
1528             {
1529                 this.indicationGoal = 0;
1530             }
1531 
1532             if (getProperty(this.slider, "MultiThumbSlider.indicateComponent", "false").equals("false"))
1533             {
1534                 // always turn on the "indication", so controls are always visible
1535                 this.indicationGoal = 1;
1536                 if (this.slider.isVisible() == false)
1537                 { // when the component isn't yet initialized
1538                     this.indication = 1; // initialize it to fully indicated
1539                 }
1540             }
1541 
1542             if (this.indication != this.indicationGoal)
1543             {
1544                 if (this.animatingThread == null || this.animatingThread.isAlive() == false)
1545                 {
1546                     this.animatingThread = new Thread(this.animatingRunnable);
1547                     this.animatingThread.start();
1548                 }
1549             }
1550         }
1551     }
1552 
1553     protected synchronized void calculateGeometry()
1554     {
1555         this.trackRect = calculateTrackRect();
1556 
1557         float[] pos = this.slider.getThumbPositions();
1558 
1559         if (this.thumbPositions.length != pos.length)
1560         {
1561             this.thumbPositions = new int[pos.length];
1562             this.thumbIndications = new float[pos.length];
1563         }
1564         if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
1565         {
1566             for (int a = 0; a < this.thumbPositions.length; a++)
1567             {
1568                 if (this.slider.isInverted() == false)
1569                 {
1570                     this.thumbPositions[a] = this.trackRect.x + (int) (this.trackRect.width * pos[a]);
1571                 }
1572                 else
1573                 {
1574                     this.thumbPositions[a] = this.trackRect.x + (int) (this.trackRect.width * (1 - pos[a]));
1575                 }
1576                 this.thumbIndications[a] = 0;
1577             }
1578         }
1579         else
1580         {
1581             for (int a = 0; a < this.thumbPositions.length; a++)
1582             {
1583                 if (this.slider.isInverted())
1584                 {
1585                     this.thumbPositions[a] = this.trackRect.y + (int) (this.trackRect.height * pos[a]);
1586                 }
1587                 else
1588                 {
1589                     this.thumbPositions[a] = this.trackRect.y + (int) (this.trackRect.height * (1 - pos[a]));
1590                 }
1591                 this.thumbIndications[a] = 0;
1592             }
1593         }
1594     }
1595 
1596     protected Rectangle calculateTrackRect()
1597     {
1598         Insets i = new Insets(5, 5, 5, 5);
1599         int w, h;
1600         if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
1601         {
1602             w = this.slider.getWidth() - i.left - i.right;
1603             h = Math.min(this.DEPTH, this.slider.getHeight() - i.top - i.bottom);
1604         }
1605         else
1606         {
1607             h = this.slider.getHeight() - i.top - i.bottom;
1608             w = Math.min(this.DEPTH, this.slider.getWidth() - i.left - i.right);
1609         }
1610         return new Rectangle(this.slider.getWidth() / 2 - w / 2, this.slider.getHeight() / 2 - h / 2, w, h);
1611     }
1612 
1613     private void nudge(int thumbIndex, int direction)
1614     {
1615         float pixelFraction;
1616         if (this.slider.getOrientation() == MultiThumbSlider.HORIZONTAL)
1617         {
1618             pixelFraction = 1f / (this.trackRect.width);
1619         }
1620         else
1621         {
1622             pixelFraction = 1f / (this.trackRect.height);
1623         }
1624         if (direction < 0)
1625             pixelFraction *= -1;
1626         if (this.slider.isInverted())
1627             pixelFraction *= -1;
1628         if (this.slider.getOrientation() == MultiThumbSlider.VERTICAL)
1629             pixelFraction *= -1;
1630 
1631         // repeat a couple of times: it's possible we'll nudge two values
1632         // so they're exactly equal, which will make validate() fail.
1633         // in that case: move the value ANOTHER nudge to the left/right
1634         // to really make a change. But make sure we still respect the [0,1] limits.
1635         State state = new State();
1636         int a = 0;
1637         while (a < 10 && state.positions[thumbIndex] >= 0 && state.positions[thumbIndex] <= 1)
1638         {
1639             state.setPosition(thumbIndex, state.positions[thumbIndex] + pixelFraction);
1640             if (validatePositions(state))
1641             {
1642                 state.install();
1643                 return;
1644             }
1645             a++;
1646         }
1647     }
1648 
1649     @Override
1650     public void installUI(JComponent slider)
1651     {
1652         slider.addMouseListener(this);
1653         slider.addMouseMotionListener(this);
1654         slider.addFocusListener(this.focusListener);
1655         slider.addKeyListener(this.keyListener);
1656         slider.addComponentListener(this.compListener);
1657         slider.addPropertyChangeListener(this.propertyListener);
1658         slider.addPropertyChangeListener(THUMB_SHAPE_PROPERTY, this.thumbShapeListener);
1659         calculateGeometry();
1660     }
1661 
1662     @Override
1663     public void paint(Graphics g, JComponent slider2)
1664     {
1665         if (slider2 != this.slider)
1666             throw new RuntimeException("only use this UI on the GradientSlider it was constructed with");
1667 
1668         Graphics2D g2 = (Graphics2D) g;
1669         int w = this.slider.getWidth();
1670         int h = this.slider.getHeight();
1671 
1672         if (this.slider.isOpaque())
1673         {
1674             g.setColor(this.slider.getBackground());
1675             g.fillRect(0, 0, w, h);
1676         }
1677 
1678         if (slider2.hasFocus())
1679         {
1680             g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1681             paintFocus(g2);
1682         }
1683         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
1684         paintTrack(g2);
1685         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
1686         paintThumbs(g2);
1687     }
1688 
1689     protected abstract void paintTrack(Graphics2D g);
1690 
1691     protected abstract void paintFocus(Graphics2D g);
1692 
1693     protected abstract void paintThumbs(Graphics2D g);
1694 
1695     @Override
1696     public void uninstallUI(JComponent slider)
1697     {
1698         slider.removeMouseListener(this);
1699         slider.removeMouseMotionListener(this);
1700         slider.removeFocusListener(this.focusListener);
1701         slider.removeKeyListener(this.keyListener);
1702         slider.removeComponentListener(this.compListener);
1703         slider.removePropertyChangeListener(this.propertyListener);
1704         slider.removePropertyChangeListener(THUMB_SHAPE_PROPERTY, this.thumbShapeListener);
1705         super.uninstallUI(slider);
1706     }
1707 
1708 }