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