View Javadoc
1   package org.djutils.swing.multislider;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Component;
5   import java.awt.Dimension;
6   import java.awt.Font;
7   import java.awt.Graphics;
8   import java.awt.Point;
9   import java.awt.event.ComponentAdapter;
10  import java.awt.event.ComponentEvent;
11  import java.awt.event.MouseEvent;
12  import java.awt.event.MouseListener;
13  import java.awt.event.MouseMotionAdapter;
14  import java.util.Dictionary;
15  import java.util.HashMap;
16  import java.util.Hashtable;
17  import java.util.Map;
18  
19  import javax.swing.BoxLayout;
20  import javax.swing.JComponent;
21  import javax.swing.JLabel;
22  import javax.swing.JPanel;
23  import javax.swing.JSlider;
24  import javax.swing.OverlayLayout;
25  import javax.swing.SwingConstants;
26  import javax.swing.SwingUtilities;
27  import javax.swing.event.ChangeEvent;
28  import javax.swing.event.ChangeListener;
29  import javax.swing.plaf.SliderUI;
30  import javax.swing.plaf.basic.BasicSliderUI;
31  
32  import org.djutils.exceptions.Throw;
33  import org.djutils.logger.CategoryLogger;
34  
35  /**
36   * The {@code AbstractMultiSlider} forms the base of implementing a slider with multiple thumbs. The AbstractMultiSlider is
37   * implemented by drawing a number of sliders on top of each other using an Swing {@code OverlayManager}, and passing the mouse
38   * events from a glass pane on top to the correct slider(s). The class is a {@code ChangeListener} to listen to the changes of
39   * individual sliders underneath.
40   * <p>
41   * Several models exist to indicate whether thumbs can pass each other or not, or be on top of each other or not.
42   * </p>
43   * <p>
44   * The {@code AbstractMultiSlider} stores all values internally as int. Only when getting or setting values (or, e.g., the
45   * minimum or maximum), the generic type T is used.
46   * </p>
47   * <p>
48   * Copyright (c) 2024-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
49   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
50   * distributed under a three-clause BSD-style license, which can be found at
51   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>.
52   * </p>
53   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
54   * @param <T> the type of values that the {@code AbstractMultiSlider} stores and returns
55   */
56  public abstract class AbstractMultiSlider<T> extends JComponent
57  {
58      /** */
59      private static final long serialVersionUID = 1L;
60  
61      /** the sliders that are stacked on top of each other. */
62      private JSlider[] sliders;
63  
64      /** the current slider number in process (e.g., drag operation, mouse down). */
65      private transient int busySlider = -1;
66  
67      /** whether an operation is busy or not. */
68      private transient boolean busy = false;
69  
70      /** the 'glass pane' on top of the sliders to dispatch the mouse clicks and drags to the correct slider. */
71      private final DispatcherPane dispatcherPane;
72  
73      /** the panel in which the thumb labels can be drawn if this is wanted. */
74      private final LabelPanel labelPanel;
75  
76      /** the initial index values of the labels for the reset function. */
77      private final int[] initialIndexValues;
78  
79      /** the last known index values of the labels for the state change function. */
80      private final int[] lastIndexValues;
81  
82      /** the labels per thumb (per underlying slider). */
83      private final Map<Integer, String> thumbLabels = new HashMap<>();
84  
85      /** whether we draw thumb labels or not. */
86      private boolean drawThumbLabels = false;
87  
88      /** the track size lowest pixel (to calculate width for horizontal slider; height for vertical slider). */
89      private int trackSizeLoPx;
90  
91      /** the track size highest pixel (to calculate width for horizontal slider; height for vertical slider). */
92      private int trackSizeHiPx;
93  
94      /** MultiSlider restriction on passing. */
95      private boolean passing = false;
96  
97      /** MultiSlider restriction on overlap. */
98      private boolean overlap = true;
99  
100     /** busy testing whether the state change of an underlying slider is okay, to avoid stack overflow. */
101     private boolean testingStateChange = false;
102 
103     /**
104      * Only one <code>ChangeEvent</code> is needed for the {@code MultiSlider} since the event's only (read-only) state is the
105      * source property. The source of events generated here is always "this". The event is created the first time that an event
106      * notification is fired.
107      */
108     private transient ChangeEvent changeEvent = null;
109 
110     /**
111      * Creates a slider with the specified orientation and the specified minimum, maximum, and initial values. The orientation
112      * can be either horizontal or vertical.
113      * @param minIndex the minimum index value of the slider
114      * @param maxIndex the maximum index value of the slider
115      * @param horizontal the orientation of the slider; true for horizontal, false for vertical
116      * @param initialIndexValues the initial index values of the thumbs of the slider
117      * @throws IllegalArgumentException if initial values are outside the min-max range, or if the number of thumbs is 0, or
118      *             when the values are not in increasing order (which is important for restricting passing and overlap)
119      */
120     public AbstractMultiSlider(final int minIndex, final int maxIndex, final boolean horizontal,
121             final int... initialIndexValues)
122     {
123         Throw.when(initialIndexValues.length == 0, IllegalArgumentException.class, "the number of thumbs cannot be zero");
124         Throw.when(minIndex >= maxIndex, IllegalArgumentException.class, "min should be less than max");
125         for (int i = 0; i < initialIndexValues.length; i++)
126         {
127             Throw.when(initialIndexValues[i] < minIndex || initialIndexValues[i] > maxIndex, IllegalArgumentException.class,
128                     "all initial value should be between min and max (inclusive)");
129             Throw.when(i > 0 && initialIndexValues[i] < initialIndexValues[i - 1], IllegalArgumentException.class,
130                     "all initial value should be in increasing order or overlap");
131         }
132 
133         // store the initial value array in a safe copy
134         this.initialIndexValues = new int[initialIndexValues.length];
135         this.lastIndexValues = new int[initialIndexValues.length];
136 
137         // based on the orientation, add a JPanel with two subpanels: one for the labels, and one for the sliders
138         setLayout(new BorderLayout());
139         JPanel topPanel = new JPanel();
140         topPanel.setLayout(new BoxLayout(topPanel, horizontal ? BoxLayout.Y_AXIS : BoxLayout.X_AXIS));
141         topPanel.setOpaque(false);
142         add(topPanel, horizontal ? BorderLayout.NORTH : BorderLayout.WEST);
143 
144         // create the label panel
145         this.labelPanel = new LabelPanel(this);
146         this.labelPanel.setLayout(new BorderLayout());
147         this.labelPanel.setPreferredSize(new Dimension(1000, 0));
148         this.labelPanel.setOpaque(false);
149         topPanel.add(this.labelPanel);
150 
151         // create the slider panel
152         JPanel sliderPanel = new JPanel();
153         sliderPanel.setOpaque(false);
154         topPanel.add(sliderPanel);
155 
156         // put a glass pane on top that dispatches the mouse event to all panes
157         this.dispatcherPane = new DispatcherPane(this);
158         sliderPanel.add(this.dispatcherPane);
159 
160         // make the sliders and show them. Slider 0 at the bottom. This one will get ticks, etc.
161         sliderPanel.setLayout(new OverlayLayout(sliderPanel));
162         this.sliders = new JSlider[initialIndexValues.length];
163         for (int i = initialIndexValues.length - 1; i >= 0; i--)
164         {
165             this.initialIndexValues[i] = initialIndexValues[i]; // create copy
166             this.lastIndexValues[i] = initialIndexValues[i];
167             var slider = new JSlider(horizontal ? SwingConstants.HORIZONTAL : SwingConstants.VERTICAL, minIndex, maxIndex,
168                     initialIndexValues[i]);
169             this.sliders[i] = slider;
170             slider.setOpaque(false);
171             slider.setPaintTrack(i == 0);
172             sliderPanel.add(slider);
173 
174             // ensure movability of the slider by setting it again
175             slider.setValue(initialIndexValues[i]);
176 
177             // set the initial labels (issue #71)
178             this.thumbLabels.put(i, "");
179         }
180 
181         this.dispatcherPane.setPreferredSize(new Dimension(this.sliders[0].getSize()));
182         calculateTrackSize();
183 
184         // listen to resize events to (re)set the track width or height
185         addComponentListener(new ComponentAdapter()
186         {
187             @Override
188             public void componentResized(final ComponentEvent e)
189             {
190                 super.componentResized(e);
191                 AbstractMultiSlider.this.setUI(getUI());
192                 calculateTrackSize();
193                 AbstractMultiSlider.this.dispatcherPane.revalidate();
194                 AbstractMultiSlider.this.labelPanel.revalidate();
195                 AbstractMultiSlider.this.labelPanel.repaint();
196             }
197         });
198 
199         // listen to state changes of underlying sliders and check if restrictions are met
200         for (int i = 0; i < this.sliders.length; i++)
201         {
202             final int index = i;
203             this.sliders[index].addChangeListener(new ChangeListener()
204             {
205                 @Override
206                 public void stateChanged(final ChangeEvent e)
207                 {
208                     if (!AbstractMultiSlider.this.testingStateChange)
209                     {
210                         AbstractMultiSlider.this.testingStateChange = true;
211                         checkRestrictions(index);
212                         AbstractMultiSlider.this.testingStateChange = false;
213                     }
214                     AbstractMultiSlider.this.fireStateChanged();
215                 }
216             });
217         }
218 
219         invalidate();
220     }
221 
222     /**
223      * Return the individual sliders, where slider[0] contains the formatting.
224      * @return the individual sliders
225      */
226     protected JSlider[] getSliders()
227     {
228         return this.sliders;
229     }
230 
231     /**
232      * Return an individual slider with index i.
233      * @param i the index for which to retrieve the slider
234      * @return the individual slider with index i
235      */
236     public JSlider getSlider(final int i)
237     {
238         return this.sliders[i];
239     }
240 
241     /**
242      * Return the number of thumbs on this multislider.
243      * @return the number of thumbs on this multislider
244      */
245     public int getNumberOfThumbs()
246     {
247         return this.sliders.length;
248     }
249 
250     /**
251      * Indicate that slider i is busy (e.g., mouse-down or a drag operation).
252      * @param i the slider number that is busy
253      */
254     protected void setBusySlider(final int i)
255     {
256         this.busySlider = i;
257     }
258 
259     /**
260      * Return whether slider i is busy (e.g., mouse-down or a drag operation).
261      * @param i the slider number to check
262      * @return whether slier i is busy or not
263      */
264     protected boolean isBusySlider(final int i)
265     {
266         return this.busySlider == i;
267     }
268 
269     /**
270      * Return which slider is busy (e.g., mouse-down or a drag operation). The function returns -1 if no slider is busy.
271      * @return the slider number of the busy slider, or -1 if no slider is busy with an action
272      */
273     protected int getBusySlider()
274     {
275         return this.busySlider;
276     }
277 
278     /**
279      * Return whether one of the sliders is busy (e.g., mouse-down or a drag operation). Note that the 'busy' flag is set BEFORE
280      * the mouse event (e.g., mouse released, mouse exited) is handled. This means that when 'busy' is true, no operation is
281      * taking place.
282      * @return whether one of the sliders is busy or not
283      */
284     public boolean isBusy()
285     {
286         return this.busy;
287     }
288 
289     /**
290      * Set whether one of the sliders is busy (e.g., mouse-down or a drag operation). Note that the 'busy' flag has to be set
291      * BEFORE the mouse event (e.g., mouse released, mouse exited) is handled. This means that when 'busy' is true, no operation
292      * is taking place.
293      * @param busy set whether one of the sliders is busy or not
294      */
295     protected void setBusy(final boolean busy)
296     {
297         this.busy = busy;
298     }
299 
300     /**
301      * Reset the slider values to the initial values.
302      */
303     public void resetToInitialValues()
304     {
305         for (int i = 0; i < this.sliders.length; i++)
306         {
307             this.sliders[i].setValue(this.initialIndexValues[i]);
308             this.sliders[i].invalidate();
309             this.sliders[i].repaint();
310         }
311         fireFinalValueChanged();
312         invalidate();
313         repaint();
314     }
315 
316     /**
317      * Return the label panel in which thumb labels can be drawn. The labels move with the thumbs.
318      * @return the label panel in which thumb labels can be drawn
319      */
320     protected LabelPanel getLabelPanel()
321     {
322         return this.labelPanel;
323     }
324 
325     /**
326      * Set the thumb label for thumb i to the given label.
327      * @param i the thumb number
328      * @param label the label to display
329      * @throws IndexOutOfBoundsException when thumb number is out of bounds
330      */
331     public void setThumbLabel(final int i, final String label)
332     {
333         Throw.when(i < 0 || i >= getNumberOfThumbs(), IndexOutOfBoundsException.class, "thumb number %d is out of bounds", i);
334         this.thumbLabels.put(i, label);
335         invalidate();
336     }
337 
338     /**
339      * Get the thumb label for thumb i.
340      * @param i the thumb number
341      * @return the label to display
342      * @throws IndexOutOfBoundsException when thumb number is out of bounds
343      */
344     public String getThumbLabel(final int i)
345     {
346         Throw.when(i < 0 || i >= getNumberOfThumbs(), IndexOutOfBoundsException.class, "thumb number %d is out of bounds", i);
347         return this.thumbLabels.get(i);
348     }
349 
350     /**
351      * Turn the thumb label display on or off.
352      * @param b whether the thumbs are displayed or not
353      * @param sizePx the height (for a horizontal slider) or width (for a vertical slider) of the label panel in pixels
354      */
355     public void setDrawThumbLabels(final boolean b, final int sizePx)
356     {
357         calculateTrackSize();
358         JSlider js = getSlider(0);
359         this.drawThumbLabels = b;
360         if (b)
361         {
362             if (isHorizontal())
363             {
364                 this.labelPanel.setPreferredSize(new Dimension(js.getWidth(), sizePx));
365             }
366             else
367             {
368                 this.labelPanel.setPreferredSize(new Dimension(sizePx, js.getHeight()));
369             }
370         }
371         else
372         {
373             if (isHorizontal())
374             {
375                 this.labelPanel.setPreferredSize(new Dimension(js.getWidth(), 0));
376             }
377             else
378             {
379                 this.labelPanel.setPreferredSize(new Dimension(0, js.getHeight()));
380             }
381         }
382         this.labelPanel.revalidate();
383         revalidate();
384     }
385 
386     /**
387      * Return whether thumb label display on or off.
388      * @return whether the thumbs are displayed or not
389      */
390     public boolean isDrawThumbLabels()
391     {
392         return this.drawThumbLabels;
393     }
394 
395     /**
396      * Recalculate the track size (width for horizontal slider; height for vertical slider) after a resize operation.
397      */
398     protected void calculateTrackSize()
399     {
400         JSlider js = getSlider(0);
401         BasicSliderUI ui = (BasicSliderUI) js.getUI();
402         int loValue = getInverted() ? js.getMaximum() : js.getMinimum();
403         int hiValue = getInverted() ? js.getMinimum() : js.getMaximum();
404         if (isHorizontal())
405         {
406             this.trackSizeLoPx = 0;
407             this.trackSizeHiPx = js.getWidth();
408             int i = 0;
409             while (i < js.getWidth() && ui.valueForXPosition(i) == loValue)
410             {
411                 this.trackSizeLoPx = i++;
412             }
413             i = js.getWidth();
414             while (i >= 0 && ui.valueForXPosition(i) == hiValue)
415             {
416                 this.trackSizeHiPx = i--;
417             }
418         }
419         else
420         {
421             this.trackSizeLoPx = 0;
422             this.trackSizeHiPx = js.getHeight();
423             int i = 0;
424             while (i < js.getHeight() && ui.valueForYPosition(i) == hiValue)
425             {
426                 this.trackSizeLoPx = i++;
427             }
428             i = js.getHeight();
429             while (i >= 0 && ui.valueForYPosition(i) == loValue)
430             {
431                 this.trackSizeHiPx = i--;
432             }
433         }
434 
435         // Adjust based on the number of values between minimum and maximum
436         int nr = getIndexMaximum() - getIndexMinimum();
437         int pxPerNr = (this.trackSizeHiPx - this.trackSizeLoPx) / nr;
438         this.trackSizeLoPx = isHorizontal() ? this.trackSizeLoPx - pxPerNr / 2 : this.trackSizeLoPx + pxPerNr / 2;
439         this.trackSizeHiPx = isHorizontal() ? this.trackSizeHiPx + pxPerNr / 2 : this.trackSizeHiPx - pxPerNr / 2;
440     }
441 
442     /**
443      * Calculate the track size (width for a horizontal slider; height for a vertical slider).
444      * @return the track size (width for a horizontal slider; height for a vertical slider
445      */
446     protected int trackSize()
447     {
448         // recalculate the track size of the previous calculation was carried out before 'pack' or 'setSize'.
449         if (this.trackSizeHiPx - this.trackSizeLoPx < 2)
450         {
451             calculateTrackSize();
452         }
453         return this.trackSizeHiPx - this.trackSizeLoPx;
454     }
455 
456     /**
457      * Return the track size lowest pixel (to calculate width for horizontal slider; height for vertical slider).
458      * @return the track size lowest pixel
459      */
460     public int getTrackSizeLoPx()
461     {
462         return this.trackSizeLoPx;
463     }
464 
465     /**
466      * Return the track size highest pixel (to calculate width for horizontal slider; height for vertical slider).
467      * @return the track size highest pixel
468      */
469     public int getTrackSizeHiPx()
470     {
471         return this.trackSizeHiPx;
472     }
473 
474     /**
475      * Calculate x pixel (horizontal) or y pixel (vertical) of thumb[i], relative to the panel of the JSlider.
476      * @param i the slider number
477      * @return the x pixel (horizontal) or y pixel (vertical) of thumb[i], relative to the panel of the JSlider
478      */
479     protected int thumbPositionPx(final int i)
480     {
481         JSlider slider = getSlider(i);
482         int value = slider.getValue();
483         int min = slider.getMinimum();
484         int max = slider.getMaximum();
485         int ts = trackSize();
486         if (getInverted())
487         {
488             return this.trackSizeLoPx + (int) (1.0 * ts * (max - value) / (max - min));
489         }
490         return this.trackSizeLoPx + (int) (1.0 * ts * (value - min) / (max - min));
491     }
492 
493     /**
494      * Return the glass pane on top of the multislider.
495      * @return the glass pane on top of the multislider
496      */
497     protected DispatcherPane getDispatcherPane()
498     {
499         return this.dispatcherPane;
500     }
501 
502     /**
503      * Gets the UI object which implements the L&amp;F for this component.
504      * @return the SliderUI object that implements the Slider L&amp;F
505      */
506     @Override
507     public SliderUI getUI()
508     {
509         return this.sliders[0].getUI();
510     }
511 
512     /**
513      * Sets the UI object which implements the L&amp;F for all underlying sliders.
514      * @param ui the SliderUI L&amp;F object
515      */
516     public void setUI(final SliderUI ui)
517     {
518         for (var slider : this.sliders)
519         {
520             try
521             {
522                 slider.setUI(ui.getClass().getDeclaredConstructor().newInstance());
523             }
524             catch (Exception exception)
525             {
526                 // silently fail
527             }
528         }
529         invalidate();
530         calculateTrackSize();
531     }
532 
533     /**
534      * Resets the UI property to a value from the current look and feel for all underlying sliders.
535      */
536     @Override
537     public void updateUI()
538     {
539         for (var slider : this.sliders)
540         {
541             slider.updateUI();
542         }
543         invalidate();
544         calculateTrackSize();
545     }
546 
547     /**
548      * Returns the name of the L&amp;F class that renders this component.
549      * @return the string "SliderUI"
550      */
551     @Override
552     public String getUIClassID()
553     {
554         return this.sliders[0].getUIClassID();
555     }
556 
557     /**
558      * Adds a ChangeListener to the multislider.
559      * @param listener the ChangeListener to add
560      */
561     public void addChangeListener(final ChangeListener listener)
562     {
563         this.listenerList.add(ChangeListener.class, listener);
564     }
565 
566     /**
567      * Removes a ChangeListener from the multislider.
568      * @param listener the ChangeListener to remove
569      */
570     public void removeChangeListener(final ChangeListener listener)
571     {
572         this.listenerList.remove(ChangeListener.class, listener);
573     }
574 
575     /**
576      * Returns an array of all the <code>ChangeListener</code>s added to this MultiSlider with addChangeListener().
577      * @return all of the <code>ChangeListener</code>s added or an empty array if no listeners have been added
578      */
579     public ChangeListener[] getChangeListeners()
580     {
581         return this.listenerList.getListeners(ChangeListener.class);
582     }
583 
584     /**
585      * Send a {@code ChangeEvent}, whose source is this {@code MultiSlider}, to all {@code ChangeListener}s that have registered
586      * interest in {@code ChangeEvent}s. This method is called each time a {@code ChangeEvent} is received from the model of one
587      * of the underlying sliders.
588      * <p>
589      * The event instance is created if necessary, and stored in {@code changeEvent}.
590      * </p>
591      */
592     protected void fireStateChanged()
593     {
594         // See if an actual state change has occurred
595         boolean changed = false;
596         for (int i = 0; i < getNumberOfThumbs(); i++)
597         {
598             if (this.lastIndexValues[i] != this.sliders[i].getValue())
599             {
600                 changed = true;
601                 this.lastIndexValues[i] = this.sliders[i].getValue();
602             }
603         }
604 
605         if (changed)
606         {
607             // Note that the listener array has the classes at the even places, and the listeners at the odd places (yuck).
608             Object[] listeners = this.listenerList.getListenerList();
609             for (int i = listeners.length - 2; i >= 0; i -= 2)
610             {
611                 if (listeners[i] == ChangeListener.class)
612                 {
613                     if (this.changeEvent == null)
614                     {
615                         this.changeEvent = new ChangeEvent(this);
616                     }
617                     ((ChangeListener) listeners[i + 1]).stateChanged(this.changeEvent);
618                 }
619             }
620         }
621     }
622 
623     /**
624      * Adds a FinalValueChangeListener to the multislider.
625      * @param listener the FinalValueChangeListener to add
626      */
627     public void addFinalValueChangeListener(final FinalValueChangeListener listener)
628     {
629         this.listenerList.add(FinalValueChangeListener.class, listener);
630     }
631 
632     /**
633      * Removes a FinalValueChangeListener from the multislider.
634      * @param listener the FinalValueChangeListener to remove
635      */
636     public void removeFinalValueChangeListener(final FinalValueChangeListener listener)
637     {
638         this.listenerList.remove(FinalValueChangeListener.class, listener);
639     }
640 
641     /**
642      * Returns an array of all the <code>FinalValueChangeListener</code>s added to this MultiSlider with
643      * addFinalValueChangeListener().
644      * @return all of the <code>FinalValueChangeListener</code>s added or an empty array if no listeners have been added
645      */
646     public FinalValueChangeListener[] getFinalValueChangeListeners()
647     {
648         return this.listenerList.getListeners(FinalValueChangeListener.class);
649     }
650 
651     /**
652      * Send a {@code ChangeEvent}, whose source is this {@code MultiSlider}, to all {@code FinalValueChangeListener}s that have
653      * registered interest in {@code ChangeEvent}s. This method is called when a change is final, e.g., after setValue(...),
654      * setInitialValues(), mouse up, and leaving the slider window after a drag event. Note that the {@code ChangeEvent}s are
655      * NOT fired when a value of an underlying slider is changed directly. The regular ChangeListener does fire these changes.
656      * <p>
657      * The event instance is created if necessary, and stored in {@code changeEvent}.
658      * </p>
659      */
660     protected void fireFinalValueChanged()
661     {
662         // Note that the listener array has the classes at the even places, and the listeners at the odd places (yuck).
663         Object[] listeners = this.listenerList.getListenerList();
664         for (int i = listeners.length - 2; i >= 0; i -= 2)
665         {
666             if (listeners[i] == FinalValueChangeListener.class)
667             {
668                 if (this.changeEvent == null)
669                 {
670                     this.changeEvent = new ChangeEvent(this);
671                 }
672                 ((FinalValueChangeListener) listeners[i + 1]).stateChanged(this.changeEvent);
673             }
674         }
675     }
676 
677     /**
678      * Translate an index to a value.
679      * @param index the index on the slider scale to convert
680      * @return the corresponding value
681      */
682     protected abstract T mapIndexToValue(int index);
683 
684     /**
685      * Translate a value to an index.
686      * @param value the value to convert to an index
687      * @return the corresponding index
688      * @throws IllegalArgumentException when value cannot be mapped onto an index
689      */
690     protected abstract int mapValueToIndex(T value);
691 
692     /**
693      * Returns the slider's current value for slider[i].
694      * @param i the thumb to retrieve the value from
695      * @return the current value of slider[i]
696      * @throws IllegalArgumentException when no value is present for the index
697      */
698     public T getValue(final int i)
699     {
700         return mapIndexToValue(getIndexValue(i));
701     }
702 
703     /**
704      * Sets the slider's current value to {@code value}. This method forwards the new value to the model. If the new value is
705      * different from the previous value, all change listeners are notified.
706      * @param i the thumb to set the value for
707      * @param value the new value
708      */
709     public void setValue(final int i, final T value)
710     {
711         int n = mapValueToIndex(value);
712         setIndexValue(i, n);
713     }
714 
715     /**
716      * Returns the slider's current index value for slider[i] from the {@code BoundedRangeModel}.
717      * @param i the thumb to retrieve the value from
718      * @return the current index value of slider[i]
719      */
720     public int getIndexValue(final int i)
721     {
722         return this.sliders[i].getModel().getValue();
723     }
724 
725     /**
726      * Sets the slider's current index value to {@code n}. This method forwards the new value to the model.
727      * <p>
728      * The data model (an instance of {@code BoundedRangeModel}) handles any mathematical issues arising from assigning faulty
729      * values. See the {@code BoundedRangeModel} documentation for details.
730      * </p>
731      * If the new value is different from the previous value, all change listeners are notified.
732      * @param i the thumb to set the value for
733      * @param n the new index value
734      */
735     protected void setIndexValue(final int i, final int n)
736     {
737         Throw.when(n < getIndexMinimum() || n > getIndexMaximum(), IllegalArgumentException.class,
738                 "setValue(%d) not in range [%d, %d]", n, getIndexMinimum(), getIndexMaximum());
739         this.sliders[i].setValue(n);
740         checkRestrictions(i);
741         this.sliders[i].invalidate();
742         this.sliders[i].repaint();
743         fireFinalValueChanged();
744     }
745 
746     /**
747      * Returns the minimum value supported by the slider from the <code>BoundedRangeModel</code>.
748      * @return the value of the model's minimum property
749      */
750     protected int getIndexMinimum()
751     {
752         return this.sliders[0].getMinimum();
753     }
754 
755     /**
756      * Sets the slider's minimum value to {@code minimum}. This method forwards the new minimum value to the models of all
757      * underlying sliders.
758      * <p>
759      * The data model (an instance of {@code BoundedRangeModel}) handles any mathematical issues arising from assigning faulty
760      * values. See the {@code BoundedRangeModel} documentation for details.
761      * </p>
762      * If the new minimum value is different from the previous minimum value, all change listeners are notified.
763      * @param minimum the new minimum
764      */
765     protected void setIndexMinimum(final int minimum)
766     {
767         Throw.when(minimum >= getIndexMaximum(), IllegalArgumentException.class, "setMinimum(%d) >= maximum %d", minimum,
768                 getIndexMaximum());
769         int oldMin = getIndexMinimum();
770         for (var slider : this.sliders)
771         {
772             slider.setMinimum(minimum);
773         }
774         checkRestrictions();
775         for (var slider : this.sliders)
776         {
777             slider.invalidate();
778         }
779         invalidate();
780         fireFinalValueChanged();
781         firePropertyChange("minimum", Integer.valueOf(oldMin), Integer.valueOf(minimum));
782         calculateTrackSize();
783     }
784 
785     /**
786      * Return the minimum value supported by the multislider.
787      * @return the minimum typed value of the multislider
788      */
789     public T getMinimum()
790     {
791         return mapIndexToValue(getIndexMinimum());
792     }
793 
794     /**
795      * Set the minimum value supported by the multislider.
796      * @param minimum the new minimum typed value of the multislider
797      */
798     public void setMinimum(final T minimum)
799     {
800         setIndexMinimum(mapValueToIndex(minimum));
801     }
802 
803     /**
804      * Returns the maximum value supported by the slider from the <code>BoundedRangeModel</code>.
805      * @return the value of the model's maximum property
806      */
807     protected int getIndexMaximum()
808     {
809         return this.sliders[0].getMaximum();
810     }
811 
812     /**
813      * Sets the slider's maximum value to {@code maximum}. This method forwards the new maximum value to the models of all
814      * underlying sliders.
815      * <p>
816      * The data model (an instance of {@code BoundedRangeModel}) handles any mathematical issues arising from assigning faulty
817      * values. See the {@code BoundedRangeModel} documentation for details.
818      * </p>
819      * If the new maximum value is different from the previous maximum value, all change listeners are notified.
820      * @param maximum the new maximum
821      */
822     protected void setIndexMaximum(final int maximum)
823     {
824         Throw.when(maximum <= getIndexMinimum(), IllegalArgumentException.class, "setMaximum(%d) >= minimum %d", maximum,
825                 getIndexMinimum());
826         int oldMax = getIndexMaximum();
827         for (var slider : this.sliders)
828         {
829             slider.setMaximum(maximum);
830         }
831         checkRestrictions();
832         for (var slider : this.sliders)
833         {
834             slider.invalidate();
835         }
836         fireFinalValueChanged();
837         firePropertyChange("maximum", Integer.valueOf(oldMax), Integer.valueOf(maximum));
838         invalidate();
839         calculateTrackSize();
840     }
841 
842     /**
843      * Return the maximum value supported by the multislider.
844      * @return the maximum typed value of the multislider
845      */
846     public T getMaximum()
847     {
848         return mapIndexToValue(getIndexMaximum());
849     }
850 
851     /**
852      * Set the maximum value supported by the multislider.
853      * @param maximum the new maximum typed value of the multislider
854      */
855     public void setMaximum(final T maximum)
856     {
857         setIndexMaximum(mapValueToIndex(maximum));
858     }
859 
860     /**
861      * Returns the "extent" from the <code>BoundedRangeModel</code>. This represents the range of values "covered" by the thumb.
862      * @return an int representing the extent
863      */
864     public int getExtent()
865     {
866         return this.sliders[0].getExtent();
867     }
868 
869     /**
870      * Sets the size of the range "covered" by the thumb for all underlying slider objects. Most look and feel implementations
871      * will change the value by this amount if the user clicks on either side of the thumb. This method just forwards the new
872      * extent value to the model.
873      * <p>
874      * The data model (an instance of {@code BoundedRangeModel}) handles any mathematical issues arising from assigning faulty
875      * values. See the {@code BoundedRangeModel} documentation for details.
876      * </p>
877      * If the new extent value is different from the previous extent value, all change listeners are notified.
878      * @param extent the new extent
879      */
880     public void setExtent(final int extent)
881     {
882         for (var slider : this.sliders)
883         {
884             slider.setExtent(extent);
885         }
886     }
887 
888     /**
889      * Return this multislider's vertical or horizontal orientation.
890      * @return {@code SwingConstants.VERTICAL} or {@code SwingConstants.HORIZONTAL}
891      */
892     public int getOrientation()
893     {
894         return this.sliders[0].getOrientation();
895     }
896 
897     /**
898      * Return whether the orientation of the multislider is horizontal or not.
899      * @return true if the orientation of the multislider is horizontal, false when not.
900      */
901     public boolean isHorizontal()
902     {
903         return getOrientation() == SwingConstants.HORIZONTAL;
904     }
905 
906     /**
907      * Return whether the orientation of the multislider is vertical or not.
908      * @return true if the orientation of the multislider is vertical , false when not.
909      */
910     public boolean isVertical()
911     {
912         return !isHorizontal();
913     }
914 
915     /**
916      * Set the slider's orientation to either {@code SwingConstants.VERTICAL} or {@code SwingConstants.HORIZONTAL}.
917      * @param orientation {@code HORIZONTAL} or {@code VERTICAL}
918      * @throws IllegalArgumentException if orientation is not one of {@code VERTICAL}, {@code HORIZONTAL}
919      */
920     public void setOrientation(final int orientation)
921     {
922         for (var slider : this.sliders)
923         {
924             slider.setOrientation(orientation);
925             slider.invalidate();
926         }
927         this.dispatcherPane.setPreferredSize(new Dimension(this.sliders[0].getSize()));
928         invalidate();
929         calculateTrackSize();
930     }
931 
932     @Override
933     public void setFont(final Font font)
934     {
935         for (var slider : this.sliders)
936         {
937             slider.setFont(font);
938             slider.invalidate();
939         }
940         invalidate();
941         calculateTrackSize();
942     }
943 
944     /**
945      * Returns the dictionary of what labels to draw at which values.
946      * @return the <code>Dictionary</code> containing labels and where to draw them
947      */
948     @SuppressWarnings("rawtypes")
949     public Dictionary getLabelTable()
950     {
951         return this.sliders[0].getLabelTable();
952     }
953 
954     /**
955      * Specify what label will be drawn at any given value. The key-value pairs are of this format:
956      * <code>{ Integer value, java.swing.JComponent label }</code>. An easy way to generate a standard table of value labels is
957      * by using the {@code createStandardLabels} method.
958      * @param labels new {@code Dictionary} of labels, or {@code null} to remove all labels
959      */
960     @SuppressWarnings("rawtypes")
961     public void setLabelTable(final Dictionary labels)
962     {
963         for (var slider : this.sliders)
964         {
965             slider.setLabelTable(labels);
966             slider.invalidate();
967         }
968         invalidate();
969         calculateTrackSize();
970     }
971 
972     /**
973      * Creates a {@code Hashtable} of numerical text labels, starting at the slider minimum, and using the increment specified.
974      * For example, if you call <code>createStandardLabels( 10 )</code> and the slider minimum is zero, then labels will be
975      * created for the values 0, 10, 20, 30, and so on.
976      * <p>
977      * For the labels to be drawn on the slider, the returned {@code Hashtable} must be passed into {@code setLabelTable}, and
978      * {@code setPaintLabels} must be set to {@code true}.
979      * <p>
980      * For further details on the makeup of the returned {@code Hashtable}, see the {@code setLabelTable} documentation.
981      * @param increment distance between labels in the generated hashtable
982      * @return a new {@code Hashtable} of labels
983      * @throws IllegalArgumentException if {@code increment} is less than or equal to zero
984      */
985     public Hashtable<Integer, JComponent> createStandardLabels(final int increment)
986     {
987         return createStandardLabels(increment, getIndexMinimum());
988     }
989 
990     /**
991      * Creates a {@code Hashtable} of text labels, starting at the starting point specified, and using the increment specified.
992      * For example, if you call <code>createStandardLabels( 10, 2 )</code>, then labels will be created for the index values 2,
993      * 12, 22, 32, and so on.
994      * <p>
995      * For the labels to be drawn on the slider, the returned {@code Hashtable} must be passed into {@code setLabelTable}, and
996      * {@code setPaintLabels} must be set to {@code true}.
997      * <p>
998      * For further details on the makeup of the returned {@code Hashtable}, see the {@code setLabelTable} documentation.
999      * @param increment distance between labels in the generated hashtable
1000      * @param startIndex value at which the labels will begin
1001      * @return a new {@code Hashtable} of labels
1002      * @exception IllegalArgumentException if {@code start} is out of range, or if {@code increment} is less than or equal to
1003      *                zero
1004      */
1005     public Hashtable<Integer, JComponent> createStandardLabels(final int increment, final int startIndex)
1006     {
1007         Throw.when(increment <= 0, IllegalArgumentException.class, "increment should be > 0");
1008         Throw.when(startIndex < getIndexMinimum() || startIndex > getIndexMaximum(), IllegalArgumentException.class,
1009                 "startIndex should be between minimum index and maximum index");
1010         Hashtable<Integer, JComponent> labels = new Hashtable<>();
1011         for (int i = startIndex; i <= getIndexMaximum(); i += increment)
1012         {
1013             labels.put(i, new JLabel(format(mapIndexToValue(i))));
1014         }
1015         return labels;
1016     }
1017 
1018     /**
1019      * Format a value for e.g., the labels of the slider. By default, the formatting is done with {@code toString()}, but this
1020      * can be overridden.
1021      * @param value the value to format
1022      * @return a formatted string representation of the value
1023      */
1024     protected String format(final T value)
1025     {
1026         return value.toString();
1027     }
1028 
1029     /**
1030      * Returns true if the value-range shown for the slider is reversed.
1031      * @return true if the slider values are reversed from their normal order
1032      */
1033     public boolean getInverted()
1034     {
1035         return this.sliders[0].getInverted();
1036     }
1037 
1038     /**
1039      * Specify true to reverse the value-range shown for the slider and false to put the value range in the normal order. The
1040      * order depends on the slider's <code>ComponentOrientation</code> property. Normal (non-inverted) horizontal sliders with a
1041      * <code>ComponentOrientation</code> value of <code>LEFT_TO_RIGHT</code> have their maximum on the right. Normal horizontal
1042      * sliders with a <code>ComponentOrientation</code> value of <code>RIGHT_TO_LEFT</code> have their maximum on the left.
1043      * Normal vertical sliders have their maximum on the top. These labels are reversed when the slider is inverted.
1044      * <p>
1045      * By default, the value of this property is {@code false}.
1046      * @param b true to reverse the slider values from their normal order
1047      */
1048     public void setInverted(final boolean b)
1049     {
1050         for (var slider : this.sliders)
1051         {
1052             slider.setInverted(b);
1053             slider.invalidate();
1054         }
1055         invalidate();
1056         calculateTrackSize();
1057     }
1058 
1059     /**
1060      * This method returns the major tick spacing. The number that is returned represents the distance, measured in values,
1061      * between each major tick mark. If you have a slider with a range from 0 to 50 and the major tick spacing is set to 10, you
1062      * will get major ticks next to the following values: 0, 10, 20, 30, 40, 50.
1063      * @return the number of values between major ticks
1064      */
1065     public int getMajorTickSpacing()
1066     {
1067         return this.sliders[0].getMajorTickSpacing();
1068     }
1069 
1070     /**
1071      * This method sets the major tick spacing. The number that is passed in represents the distance, measured in values,
1072      * between each major tick mark. If you have a slider with a range from 0 to 50 and the major tick spacing is set to 10, you
1073      * will get major ticks next to the following values: 0, 10, 20, 30, 40, 50.
1074      * <p>
1075      * In order for major ticks to be painted, {@code setPaintTicks} must be set to {@code true}.
1076      * </p>
1077      * This method will also set up a label table for you. If there is not already a label table, and the major tick spacing is
1078      * {@code > 0}, and {@code getPaintLabels} returns {@code true}, a standard label table will be generated (by calling
1079      * {@code createStandardLabels}) with labels at the major tick marks. For the example above, you would get text labels: "0",
1080      * "10", "20", "30", "40", "50". The label table is then set on the slider by calling {@code setLabelTable}.
1081      * @param n new value for the {@code majorTickSpacing} property
1082      */
1083     public void setMajorTickSpacing(final int n)
1084     {
1085         int oldValue = getMajorTickSpacing();
1086         for (var slider : this.sliders)
1087         {
1088             slider.setMajorTickSpacing(n);
1089             slider.invalidate();
1090         }
1091         firePropertyChange("majorTickSpacing", oldValue, n);
1092         invalidate();
1093     }
1094 
1095     /**
1096      * This method returns the minor tick spacing. The number that is returned represents the distance, measured in values,
1097      * between each minor tick mark. If you have a slider with a range from 0 to 50 and the minor tick spacing is set to 10, you
1098      * will get minor ticks next to the following values: 0, 10, 20, 30, 40, 50.
1099      * @return the number of values between minor ticks
1100      */
1101     public int getMinorTickSpacing()
1102     {
1103         return this.sliders[0].getMinorTickSpacing();
1104     }
1105 
1106     /**
1107      * This method sets the minor tick spacing. The number that is passed in represents the distance, measured in values,
1108      * between each minor tick mark. If you have a slider with a range from 0 to 50 and the minor tick spacing is set to 10, you
1109      * will get minor ticks next to the following values: 0, 10, 20, 30, 40, 50.
1110      * <p>
1111      * In order for minor ticks to be painted, {@code setPaintTicks} must be set to {@code true}.
1112      * @param n new value for the {@code minorTickSpacing} property
1113      * @see #getMinorTickSpacing
1114      * @see #setPaintTicks
1115      */
1116     public void setMinorTickSpacing(final int n)
1117     {
1118         int oldValue = getMinorTickSpacing();
1119         for (var slider : this.sliders)
1120         {
1121             slider.setMinorTickSpacing(n);
1122             slider.invalidate();
1123         }
1124         firePropertyChange("minorTickSpacing", oldValue, n);
1125         invalidate();
1126     }
1127 
1128     /**
1129      * Returns true if the thumb (and the data value it represents) resolve to the closest tick mark next to where the user
1130      * positioned the thumb.
1131      * @return true if the value snaps to the nearest tick mark, else false
1132      */
1133     public boolean getSnapToTicks()
1134     {
1135         return this.sliders[0].getSnapToTicks();
1136     }
1137 
1138     /**
1139      * Specifying true makes the thumb (and the data value it represents) resolve to the closest tick mark next to where the
1140      * user positioned the thumb. By default, this property is {@code false}.
1141      * @param b true to snap the thumb to the nearest tick mark
1142      */
1143     public void setSnapToTicks(final boolean b)
1144     {
1145         boolean oldValue = getSnapToTicks();
1146         for (var slider : this.sliders)
1147         {
1148             slider.setSnapToTicks(b);
1149         }
1150         firePropertyChange("snapToTicks", oldValue, b);
1151     }
1152 
1153     /**
1154      * Tells if tick marks are to be painted.
1155      * @return true if tick marks are painted, else false
1156      */
1157     public boolean getPaintTicks()
1158     {
1159         return this.sliders[0].getPaintTicks();
1160     }
1161 
1162     /**
1163      * Determines whether tick marks are painted on the slider. By default, this property is {@code false}.
1164      * @param b whether or not tick marks should be painted
1165      */
1166     public void setPaintTicks(final boolean b)
1167     {
1168         boolean oldValue = getPaintTicks();
1169         for (var slider : this.sliders)
1170         {
1171             slider.setPaintTicks(b);
1172             slider.invalidate();
1173         }
1174         firePropertyChange("paintTicks", oldValue, b);
1175         invalidate();
1176         calculateTrackSize();
1177     }
1178 
1179     /**
1180      * Tells if the track (area the slider slides in) is to be painted.
1181      * @return true if track is painted, else false
1182      */
1183     public boolean getPaintTrack()
1184     {
1185         return this.sliders[0].getPaintTrack();
1186     }
1187 
1188     /**
1189      * Determines whether the track is painted on the slider. By default, this property is {@code true}. It is up to the look
1190      * and feel to honor this property, some may choose to ignore it.
1191      * @param b whether or not to paint the slider track
1192      * @see #getPaintTrack
1193      */
1194     public void setPaintTrack(final boolean b)
1195     {
1196         boolean oldValue = getPaintTrack();
1197         this.sliders[0].setPaintTrack(b);
1198         firePropertyChange("paintTrack", oldValue, b);
1199         calculateTrackSize();
1200     }
1201 
1202     /**
1203      * Tells if labels are to be painted.
1204      * @return true if labels are painted, else false
1205      */
1206     public boolean getPaintLabels()
1207     {
1208         return this.sliders[0].getPaintLabels();
1209     }
1210 
1211     /**
1212      * Determines whether labels are painted on the slider.
1213      * <p>
1214      * This method will also set up a label table for you. If there is not already a label table, and the major tick spacing is
1215      * {@code > 0}, a standard label table will be generated (by calling {@code createStandardLabels}) with labels at the major
1216      * tick marks. The label table is then set on the slider by calling {@code setLabelTable}.
1217      * </p>
1218      * By default, this property is {@code false}.
1219      * @param b whether or not to paint labels
1220      */
1221     public void setPaintLabels(final boolean b)
1222     {
1223         boolean oldValue = getPaintLabels();
1224         for (var slider : this.sliders)
1225         {
1226             slider.setPaintLabels(b);
1227             slider.invalidate();
1228         }
1229         firePropertyChange("paintLabels", oldValue, b);
1230         invalidate();
1231         calculateTrackSize();
1232     }
1233 
1234     /**
1235      * Set whether passing of the thumbs is allowed, and check whether thumb values are in line with restrictions.
1236      * @param b whether passing of the thumbs is allowed or not
1237      */
1238     public void setPassing(final boolean b)
1239     {
1240         this.passing = b;
1241         if (!this.passing)
1242         {
1243             checkRestrictions();
1244         }
1245     }
1246 
1247     /**
1248      * Return whether passing of the thumbs is allowed.
1249      * @return whether passing of the thumbs is allowed or not
1250      */
1251     public boolean getPassing()
1252     {
1253         return this.passing;
1254     }
1255 
1256     /**
1257      * Set whether overlap of the thumbs is allowed, and check whether thumb values are in line with restrictions.
1258      * @param b whether overlap of the thumbs is allowed or not
1259      */
1260     public void setOverlap(final boolean b)
1261     {
1262         this.overlap = b;
1263         if (!this.overlap)
1264         {
1265             checkRestrictions();
1266         }
1267     }
1268 
1269     /**
1270      * Return whether overlap of the thumbs is allowed.
1271      * @return whether overlap of the thumbs is allowed or not
1272      */
1273     public boolean getOverlap()
1274     {
1275         return this.overlap;
1276     }
1277 
1278     /**
1279      * Check restrictions on all thumb values and correct values where necessary.
1280      * @return whether compliance with the restrictions is ok; false means violation
1281      */
1282     protected boolean checkRestrictions()
1283     {
1284         boolean ret = true;
1285         if (!getPassing())
1286         {
1287             for (int i = 1; i < getNumberOfThumbs(); i++)
1288             {
1289                 // see if we need to push values 'up'
1290                 if (getIndexValue(i) <= getIndexValue(i - 1))
1291                 {
1292                     getSlider(i).setValue(getIndexValue(i - 1));
1293                     ret = false;
1294                 }
1295             }
1296             for (int i = getNumberOfThumbs() - 1; i >= 1; i--)
1297             {
1298                 // see if we need to push values 'down'
1299                 if (getIndexValue(i) <= getIndexValue(i - 1))
1300                 {
1301                     getSlider(i - 1).setValue(getIndexValue(i));
1302                     ret = false;
1303                 }
1304             }
1305         }
1306         if (!getOverlap())
1307         {
1308             for (int i = 1; i < getNumberOfThumbs(); i++)
1309             {
1310                 // see if we need to push values 'up'
1311                 if (getIndexValue(i) <= getIndexValue(i - 1))
1312                 {
1313                     getSlider(i).setValue(getIndexValue(i - 1) + 1);
1314                     ret = false;
1315                 }
1316             }
1317             for (int i = getNumberOfThumbs() - 1; i >= 1; i--)
1318             {
1319                 // see if we need to push values 'down'
1320                 if (getIndexValue(i) <= getIndexValue(i - 1))
1321                 {
1322                     getSlider(i - 1).setValue(getIndexValue(i) - 1);
1323                     ret = false;
1324                 }
1325             }
1326         }
1327         if (!ret)
1328         {
1329             invalidate();
1330             repaint();
1331         }
1332         return ret;
1333     }
1334 
1335     /**
1336      * Check restrictions on the thumb values of thumb 'index' and correct values where necessary.
1337      * @param index the slider for which to check (the only one whose value should change)
1338      * @return whether compliance with the restrictions is ok; false means violation
1339      */
1340     protected boolean checkRestrictions(final int index)
1341     {
1342         boolean ret = true;
1343         if (!getPassing())
1344         {
1345             if (index > 0 && getIndexValue(index) <= getIndexValue(index - 1))
1346             {
1347                 getSlider(index).setValue(getIndexValue(index - 1));
1348                 ret = false;
1349             }
1350             if (index < getNumberOfThumbs() - 1 && getIndexValue(index) >= getIndexValue(index + 1))
1351             {
1352                 getSlider(index).setValue(getIndexValue(index + 1));
1353                 ret = false;
1354             }
1355         }
1356         if (!getOverlap())
1357         {
1358             if (index > 0 && getIndexValue(index) <= getIndexValue(index - 1))
1359             {
1360                 getSlider(index).setValue(getIndexValue(index - 1) + 1);
1361                 ret = false;
1362             }
1363             if (index < getNumberOfThumbs() - 1 && getIndexValue(index) >= getIndexValue(index + 1))
1364             {
1365                 getSlider(index).setValue(getIndexValue(index + 1) - 1);
1366                 ret = false;
1367             }
1368         }
1369         if (!ret)
1370         {
1371             getSlider(index).invalidate();
1372             getSlider(index).repaint();
1373         }
1374         return ret;
1375     }
1376 
1377     /**
1378      * The DispatcherPane class, which is a glass pane sitting on top of the sliders to dispatch the mouse event to the correct
1379      * slider class. Note that the mouse coordinates are relative to the component itself, so a translation might be needed. The
1380      * <code>SwingUtilities.convertPoint()</code> method can make the conversion.
1381      */
1382     protected static class DispatcherPane extends JComponent
1383     {
1384         /** */
1385         private static final long serialVersionUID = 1L;
1386 
1387         /** the pointer to the multislider object; protected to access it by the mouse handlers. */
1388         @SuppressWarnings("checkstyle:visibilitymodifier")
1389         protected final AbstractMultiSlider<?> multiSlider;
1390 
1391         /**
1392          * Return the closest slider number based on x (horizontal) or y (vertical) locations of the thumbs. When two or more
1393          * thumbs are at the exact same distance (e.g., because they overlap), the first slider found that has a movement option
1394          * in the direction of the mouse will be returned.
1395          * @param p the point (e.g., of a mouse position)
1396          * @return the index(es) of the closest slider(s)
1397          */
1398         int closestSliderIndex(final Point p)
1399         {
1400             if (this.multiSlider.getBusySlider() >= 0)
1401             {
1402                 return this.multiSlider.getBusySlider();
1403             }
1404 
1405             int mindist = Integer.MAX_VALUE; // non-absolute lowest distance of (mouse) position in pixels
1406             int minIndex = -1; // int scale value of thumb at closest position
1407             int mini = -1; // thumb index of closest position
1408             int loi = Integer.MAX_VALUE; // lowest thumb number on closest position
1409             int hii = -1; // highest thumb number on closest position
1410             for (int i = 0; i < this.multiSlider.getNumberOfThumbs(); i++)
1411             {
1412                 int posPx = this.multiSlider.thumbPositionPx(i);
1413                 int dist = this.multiSlider.isHorizontal() ? posPx - p.x : posPx - (getHeight() - p.y);
1414                 if (Math.abs(dist) == Math.abs(mindist))
1415                 {
1416                     hii = i;
1417                 }
1418                 else if (Math.abs(dist) < Math.abs(mindist))
1419                 {
1420                     mindist = dist;
1421                     mini = i;
1422                     minIndex = this.multiSlider.getIndexValue(i);
1423                     loi = i;
1424                     hii = i;
1425                 }
1426             }
1427 
1428             // if only one closest slider (loi == hii), or no passing restrictions: move any slider!
1429             if (loi == hii || this.multiSlider.getPassing())
1430             {
1431                 return mini;
1432             }
1433             if (minIndex == this.multiSlider.getIndexMinimum()) // hi movement only
1434             {
1435                 return hii;
1436             }
1437             if (minIndex == this.multiSlider.getIndexMaximum()) // lo movement only
1438             {
1439                 return loi;
1440             }
1441             if (mindist > 0) // mouse to the left
1442             {
1443                 return loi;
1444             }
1445             return hii; // mouse to the right
1446         }
1447 
1448         /**
1449          * @param e the MouseEvent to dispatch to the sliders.
1450          * @param always indicates whether we always need to send the event
1451          * @return the slider index to which the event was dispatched; -1 if none
1452          */
1453         int dispatch(final MouseEvent e, final boolean always)
1454         {
1455             var slider = DispatcherPane.this.multiSlider.getSlider(0);
1456             Point pSlider = SwingUtilities.convertPoint(DispatcherPane.this, e.getPoint(), slider);
1457             if (always || (pSlider.x >= 0 && pSlider.x <= slider.getSize().width && pSlider.y >= 0
1458                     && pSlider.y <= slider.getSize().height))
1459             {
1460                 int index = closestSliderIndex(pSlider);
1461                 MouseEvent meSlider = new MouseEvent((Component) e.getSource(), e.getID(), e.getWhen(), e.getModifiersEx(),
1462                         pSlider.x, pSlider.y, e.getClickCount(), e.isPopupTrigger(), e.getButton());
1463                 try
1464                 {
1465                     DispatcherPane.this.multiSlider.getSlider(index).dispatchEvent(meSlider);
1466                 }
1467                 catch (Exception exception)
1468                 {
1469                     CategoryLogger.always().error(exception, "error dispatching mouseEvent {}", meSlider);
1470                 }
1471                 setBusySlider(index);
1472                 return index;
1473             }
1474             return -1;
1475         }
1476 
1477         /**
1478          * Reset the busy slider -- action over. Call this method BEFORE processing the MouseEvent. In that way, the
1479          * ChangeListener will fire a StateChange while the busy slider is -1 -- indicating that there is a final value.
1480          * @param index the slider number that is busy, or -1 if none
1481          */
1482         void setBusySlider(final int index)
1483         {
1484             this.multiSlider.setBusySlider(index);
1485         }
1486 
1487         /**
1488          * Indicate whether the multislider is busy with handling an action (e.g., drag, mouse down). Note that the busy flag
1489          * has to be set BEFORE the mouse event is handled, to allow a listener to the ChangeEvent to only react when an action
1490          * is completed.
1491          * @param b whether the multislider is busy with handling an action or not
1492          */
1493         void setBusy(final boolean b)
1494         {
1495             this.multiSlider.setBusy(b);
1496         }
1497 
1498         /**
1499          * Check whether minimum, maximum, passing or overlap restrictions were violated, and if so, adjust.
1500          * @param index the slider number for which to check
1501          */
1502         void checkRestrictions(final int index)
1503         {
1504             this.multiSlider.checkRestrictions(index);
1505         }
1506 
1507         /**
1508          * Create a glass pane on top of the sliders.
1509          * @param multiSlider the multislider for which this is the glass pane
1510          */
1511         public DispatcherPane(final AbstractMultiSlider<?> multiSlider)
1512         {
1513             this.multiSlider = multiSlider;
1514             setOpaque(false);
1515 
1516             addMouseListener(new MouseListener()
1517             {
1518                 @Override
1519                 public void mouseReleased(final MouseEvent e)
1520                 {
1521                     setBusy(false);
1522                     int index = dispatch(e, false);
1523                     if (index >= 0)
1524                     {
1525                         checkRestrictions(index);
1526                         DispatcherPane.this.multiSlider.fireFinalValueChanged();
1527                     }
1528                     setBusySlider(-1);
1529                 }
1530 
1531                 @Override
1532                 public void mousePressed(final MouseEvent e)
1533                 {
1534                     setBusy(true);
1535                     int index = dispatch(e, false);
1536                     setBusySlider(index);
1537                     if (index < 0)
1538                     {
1539                         setBusy(false);
1540                     }
1541                 }
1542 
1543                 @Override
1544                 public void mouseExited(final MouseEvent e)
1545                 {
1546                     setBusy(false);
1547                     // emulate a mouse release somewhere on the current slider to force correct update
1548                     var ms = DispatcherPane.this.multiSlider;
1549                     if (ms.getBusySlider() >= 0)
1550                     {
1551                         JSlider js = ms.getSlider(ms.getBusySlider());
1552                         checkRestrictions(ms.getBusySlider());
1553                         MouseEvent meSlider = new MouseEvent(js, MouseEvent.MOUSE_RELEASED, e.getWhen(), 0, 10, 10, 1, false,
1554                                 MouseEvent.BUTTON1);
1555                         js.dispatchEvent(meSlider);
1556                         ms.fireFinalValueChanged();
1557                     }
1558                     setBusySlider(-1);
1559                 }
1560 
1561                 @Override
1562                 public void mouseEntered(final MouseEvent e)
1563                 {
1564                     // no action
1565                 }
1566 
1567                 @Override
1568                 public void mouseClicked(final MouseEvent e)
1569                 {
1570                     // completely caught by pressed + released (or pressed + dragged + released)
1571                 }
1572             });
1573 
1574             addMouseMotionListener(new MouseMotionAdapter()
1575             {
1576                 @Override
1577                 public void mouseDragged(final MouseEvent e)
1578                 {
1579                     setBusy(true);
1580                     int index = dispatch(e, false);
1581                     setBusySlider(index);
1582                     if (index < 0)
1583                     {
1584                         setBusy(false);
1585                     }
1586                     else
1587                     {
1588                         checkRestrictions(index);
1589                     }
1590                 }
1591 
1592                 @Override
1593                 public void mouseMoved(final MouseEvent e)
1594                 {
1595                     // Note: possibly we can trigger an 'mouse entered' when the mouse enters the slider pane,
1596                     // and a 'mouse exited' when the mouse exits the slider pane.
1597                 }
1598             });
1599         }
1600     }
1601 
1602     /**
1603      * The LabelPanel is draw above a horizontal slider or left of a vertical slider and displays labels for the thumbs of the
1604      * slider, so one can see which one is which.
1605      */
1606     protected static class LabelPanel extends JPanel
1607     {
1608         /** */
1609         private static final long serialVersionUID = 1L;
1610 
1611         /** a pointer to the multislider. */
1612         private final AbstractMultiSlider<?> multiSlider;
1613 
1614         /**
1615          * Default constructor.
1616          * @param multiSlider the multislider for which this is the LabelPanel.
1617          */
1618         public LabelPanel(final AbstractMultiSlider<?> multiSlider)
1619         {
1620             this.multiSlider = multiSlider;
1621             repaint();
1622             this.multiSlider.addChangeListener(new ChangeListener()
1623             {
1624                 @Override
1625                 public void stateChanged(final ChangeEvent e)
1626                 {
1627                     repaint();
1628                 }
1629             });
1630         }
1631 
1632         @Override
1633         public void paintComponent(final Graphics g)
1634         {
1635             super.paintComponent(g);
1636             if (LabelPanel.this.multiSlider.isDrawThumbLabels())
1637             {
1638                 for (int i = 0; i < this.multiSlider.getNumberOfThumbs(); i++)
1639                 {
1640                     int pos = this.multiSlider.thumbPositionPx(i);
1641                     String s = this.multiSlider.getThumbLabel(i);
1642                     int sw = g.getFontMetrics().stringWidth(s);
1643                     int sh = g.getFontMetrics().getHeight();
1644                     if (this.multiSlider.isHorizontal())
1645                     {
1646                         g.drawString(s, pos - sw / 2, 12);
1647                     }
1648                     else
1649                     {
1650                         g.drawString(s, getWidth() - sw - 10, getHeight() - pos + sh / 2 - 3);
1651                     }
1652                 }
1653             }
1654         }
1655     }
1656 
1657     /**
1658      * The FinalValueChangeListener sends a final value to the listeners after mouse-up, leave focus, setValue(...), and
1659      * setInitialValues().
1660      */
1661     public interface FinalValueChangeListener extends ChangeListener
1662     {
1663         // no extra events
1664     }
1665 }