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