001/*
002 * ============================================================================
003 *  Copyright © 2002-2024 by Thomas Thrien.
004 *  All Rights Reserved.
005 * ============================================================================
006 *  Licensed to the public under the agreements of the GNU Lesser General Public
007 *  License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *       http://www.gnu.org/licenses/lgpl.html
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 *  License for the specific language governing permissions and limitations
015 *  under the License.
016 */
017package org.tquadrat.foundation.fx.control.skin;
018
019import static java.lang.Double.max;
020import static javafx.geometry.Orientation.HORIZONTAL;
021import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
022import static org.apiguardian.api.API.Status.INTERNAL;
023import static org.apiguardian.api.API.Status.STABLE;
024import static org.tquadrat.foundation.fx.FXUtils.clamp;
025import static org.tquadrat.foundation.fx.FXUtils.nearest;
026import static org.tquadrat.foundation.fx.control.skin.RangeSliderSkin.FocusedChild.HIGH_THUMB;
027import static org.tquadrat.foundation.fx.control.skin.RangeSliderSkin.FocusedChild.LOW_THUMB;
028import static org.tquadrat.foundation.fx.control.skin.RangeSliderSkin.FocusedChild.NONE;
029import static org.tquadrat.foundation.fx.control.skin.RangeSliderSkin.FocusedChild.RANGE_BAR;
030import static org.tquadrat.foundation.fx.internal.ControlUtils.focusNextSibling;
031import static org.tquadrat.foundation.fx.internal.ControlUtils.focusPreviousSibling;
032import static org.tquadrat.foundation.lang.Objects.isNull;
033import static org.tquadrat.foundation.lang.Objects.nonNull;
034import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
035
036import org.apiguardian.api.API;
037import org.tquadrat.foundation.annotation.ClassVersion;
038import org.tquadrat.foundation.fx.control.RangeSlider;
039import javafx.beans.binding.ObjectBinding;
040import javafx.event.EventHandler;
041import javafx.geometry.Orientation;
042import javafx.geometry.Point2D;
043import javafx.geometry.Side;
044import javafx.scene.Cursor;
045import javafx.scene.chart.NumberAxis;
046import javafx.scene.control.Skin;
047import javafx.scene.control.SkinBase;
048import javafx.scene.input.KeyEvent;
049import javafx.scene.input.MouseEvent;
050import javafx.scene.layout.StackPane;
051import javafx.util.Callback;
052
053/**
054 *  The skin for instances of
055 *  {@link RangeSlider}.
056 *
057 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
058 *  @inspired  {@href https://controlsfx.github.io/ ControlsFX Project}
059 *  @version $Id: RangeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $
060 *  @since 0.4.6
061 *
062 *  @UMLGraph.link
063 */
064@SuppressWarnings( {"ClassWithTooManyFields", "ClassWithTooManyMethods", "OverlyComplexClass"} )
065@ClassVersion( sourceVersion = "$Id: RangeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $" )
066@API( status = STABLE, since = "0.4.6" )
067public class RangeSliderSkin extends SkinBase<RangeSlider>
068{
069        /*---------------*\
070    ====** Inner Classes **====================================================
071        \*---------------*/
072    /**
073     *  The indicators for the focus owners.
074     *
075     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
076     *  @inspired  {@href https://controlsfx.github.io/ ControlsFX Project}
077     *  @version $Id: RangeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $
078     *  @since 0.4.6
079     *
080     *  @UMLGraph.link
081     */
082    @SuppressWarnings( "ProtectedInnerClass" )
083    @ClassVersion( sourceVersion = "$Id: RangeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $" )
084    @API( status = INTERNAL, since = "0.4.6" )
085    protected static enum FocusedChild
086    {
087            /*------------------*\
088        ====** Enum Declaration **=============================================
089            \*------------------*/
090        /**
091         *  The focus is on the low thumb.
092         */
093        LOW_THUMB,
094
095        /**
096         *  The focus is on the high thump.
097         */
098        HIGH_THUMB,
099
100        /**
101         *  The focus is on the range bar.
102         */
103        RANGE_BAR,
104
105        /**
106         *  None of the elements of the
107         *  {@link RangeSlider}
108         *  does currently have the focus.
109         */
110        NONE
111    }
112    //  enum FocusedChild
113
114    /**
115     *  The implementation of
116     *  {@link StackPane}
117     *  that is used for the thumbs of a
118     *  {@link RangeSlider}
119     *  instance.
120     *
121     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
122     *  @inspired  {@href https://controlsfx.github.io/ ControlsFX Project}
123     *  @version $Id: RangeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $
124     *  @since 0.4.6
125     *
126     *  @UMLGraph.link
127     */
128    @ClassVersion( sourceVersion = "$Id: RangeSliderSkin.java 1121 2024-03-16 16:51:23Z tquadrat $" )
129    @API( status = INTERNAL, since = "0.4.6" )
130    private static final class ThumbPane extends StackPane
131    {
132            /*--------------*\
133        ====** Constructors **=================================================
134            \*--------------*/
135        /**
136         *  Creates a new instance of {@code ThumbPane}.
137         */
138        public ThumbPane() { super(); }
139
140            /*---------*\
141        ====** Methods **======================================================
142            \*---------*/
143        /**
144         *  Sets the focus.
145         *
146         *  @param  flag    {@code true} if this instance has the focus,
147         *      {@code false} if not.
148         */
149        public final void setFocus( final boolean flag ) { setFocused( flag ); }
150    }
151    //  class ThumbPane
152
153        /*------------*\
154    ====** Attributes **=======================================================
155        \*------------*/
156    /**
157     *  The current focus owner.
158     */
159    private FocusedChild m_CurrentFocus = LOW_THUMB;
160
161    /**
162     *  The high thumb itself.
163     */
164    private ThumbPane m_HighThumb;
165
166    /**
167     *  The low thumb itself.
168     */
169    private ThumbPane m_LowThumb;
170
171    /**
172     *  The position for the low thumb.
173     */
174    private double m_LowThumbPos;
175
176    /**
177     *  The orientation for the {@code RangeSlider}.
178     */
179    private Orientation m_Orientation;
180
181    /**
182     *  Used as a temp value for low and high thumbs.
183     */
184    private double m_PreDragPos;
185
186    /**
187     *  The
188     *  {@link #m_PreDragPos}
189     *  in skin coordinates.
190     */
191    private Point2D m_PreDragThumbPoint;
192
193    /**
194     *  The bar between the two thumbs, can be dragged.
195     */
196    private StackPane m_RangeBar;
197
198    /**
199     *  The end of the range.
200     */
201    private double m_RangeEnd;
202
203    /**
204     *  The start of the range.
205     */
206    private double m_RangeStart;
207
208    /**
209     *  The callback for the value selection.
210     */
211    private Callback<Void,FocusedChild> m_SelectedValue;
212
213    /**
214     *  The flag that indicates whether the tick marks are shown or not.
215     */
216    private boolean m_ShowTickMarks;
217
218    /**
219     *  The height of the thumbs.
220     */
221    @SuppressWarnings( "FieldCanBeLocal" )
222    private double m_ThumbHeight;
223
224    /**
225     *  The width of the thumbs.
226     */
227    private double m_ThumbWidth;
228
229    /**
230     *  The tick line.
231     */
232    private NumberAxis m_TickLine = null;
233
234    /**
235     *  The container that represents the slider track.
236     */
237    private StackPane m_Track;
238
239    /**
240     *  The length of the track.
241     */
242    private double m_TrackLength;
243
244    /**
245     *  The start of the track.
246     */
247    private double m_TrackStart;
248
249    /**
250     *  The width of the gap between the slider track and the tick line.
251     */
252    @SuppressWarnings( {"MagicNumber", "FieldMayBeFinal"} )
253    private double m_TrackToTickGap = 2.0;
254
255        /*--------------*\
256    ====** Constructors **=====================================================
257        \*--------------*/
258    /**
259     *  Creates a new instance of {@code RangeSliderSkin}.
260     *
261     *  @param  control The control for which this Skin should attach to.
262     */
263    @SuppressWarnings( {"OverlyLongMethod", "OverlyComplexMethod"} )
264    public RangeSliderSkin( final RangeSlider control )
265    {
266        super( requireNonNullArgument( control, "control" ) );
267
268        m_Orientation = getSkinnable().getOrientation();
269
270        initLowThumb();
271        initHighThumb();
272        initRangeBar();
273
274        registerChangeListener( control.lowValueProperty(), $ ->
275        {
276            positionLowThumb();
277            if( isHorizontal() )
278            {
279                m_RangeBar.resizeRelocate( m_RangeStart, m_RangeBar.getLayoutY(), m_RangeEnd - m_RangeStart, m_RangeBar.getHeight() );
280            }
281            else
282            {
283                m_RangeBar.resize( m_RangeBar.getWidth(), m_RangeEnd - m_RangeStart );
284            }
285        } );
286        registerChangeListener( control.highValueProperty(), $ ->
287        {
288            positionHighThumb();
289            if( isHorizontal() )
290            {
291                m_RangeBar.resize( m_RangeEnd - m_RangeStart, m_RangeBar.getHeight() );
292            }
293            else
294            {
295                m_RangeBar.resizeRelocate( m_RangeBar.getLayoutX(), m_RangeStart, m_RangeBar.getWidth(), m_RangeEnd - m_RangeStart );
296            }
297        } );
298        registerChangeListener( control.minProperty(), $ ->
299        {
300            if( m_ShowTickMarks && nonNull( m_TickLine ) )
301            {
302                m_TickLine.setLowerBound( getSkinnable().getMin() );
303            }
304            getSkinnable().requestLayout();
305        } );
306        registerChangeListener( control.maxProperty(), $ ->
307        {
308            if( m_ShowTickMarks && nonNull( m_TickLine ) )
309            {
310                m_TickLine.setUpperBound( getSkinnable().getMax() );
311            }
312            getSkinnable().requestLayout();
313        } );
314        registerChangeListener( control.orientationProperty(), $ ->
315        {
316            m_Orientation = getSkinnable().getOrientation();
317            if( m_ShowTickMarks && nonNull( m_TickLine ) )
318            {
319                m_TickLine.setSide( isHorizontal() ? Side.BOTTOM : Side.RIGHT );
320            }
321            getSkinnable().requestLayout();
322        } );
323        registerChangeListener( control.showTickMarksProperty(),
324            $ -> setShowTickMarks( getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels() ) );
325        registerChangeListener( control.showTickLabelsProperty(),
326            $ -> setShowTickMarks( getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels() ) );
327        registerChangeListener( control.majorTickUnitProperty(), $ ->
328        {
329            if( nonNull( m_TickLine ) )
330            {
331                m_TickLine.setTickUnit( getSkinnable().getMajorTickUnit() );
332                getSkinnable().requestLayout();
333            }
334        } );
335        registerChangeListener( control.minorTickCountProperty(), $ ->
336        {
337            if( nonNull( m_TickLine ) )
338            {
339                m_TickLine.setMinorTickCount( Integer.max( getSkinnable().getMinorTickCount(),0 ) + 1 );
340                getSkinnable().requestLayout();
341            }
342        } );
343
344        //noinspection LambdaParameterNamingConvention
345        m_LowThumb.focusedProperty().addListener( ($1,$2,hasFocus) ->
346        {
347            if( hasFocus ) m_CurrentFocus = LOW_THUMB;
348        } );
349        //noinspection LambdaParameterNamingConvention
350        m_HighThumb.focusedProperty().addListener( ($1,$2,hasFocus) ->
351        {
352            if( hasFocus ) m_CurrentFocus = HIGH_THUMB;
353        } );
354        //noinspection LambdaParameterNamingConvention
355        m_RangeBar.focusedProperty().addListener( ($1,$2,hasFocus) ->
356        {
357            if( hasFocus ) m_CurrentFocus = RANGE_BAR;
358        } );
359        //noinspection LambdaParameterNamingConvention
360        control.focusedProperty().addListener( ($1,$2,hasFocus) ->
361        {
362            if( hasFocus )
363            {
364                m_LowThumb.setFocus( true );
365            }
366            else
367            {
368                m_LowThumb.setFocus( false );
369                m_HighThumb.setFocus( false );
370                m_CurrentFocus = NONE;
371            }
372        } );
373
374        @SuppressWarnings( "OverlyLongLambda" )
375        final EventHandler<KeyEvent> keyPressEventHandler = event ->
376        {
377            switch( event.getCode() )
378            {
379                case TAB ->
380                {
381                    if( m_LowThumb.isFocused() )
382                    {
383                        if( event.isShiftDown() )
384                        {
385                            focusPreviousSibling( getSkinnable() );
386                        }
387                        else
388                        {
389                            m_LowThumb.setFocus( false );
390                            m_HighThumb.setFocus( true );
391                        }
392                        event.consume();
393                    }
394                    else if( m_HighThumb.isFocused() )
395                    {
396                        if( event.isShiftDown() )
397                        {
398                            m_HighThumb.setFocus( false );
399                            m_LowThumb.setFocus( true );
400                        }
401                        else
402                        {
403                            focusNextSibling( getSkinnable() );
404                        }
405                        event.consume();
406                    }
407                }   //  case TAB
408
409                case LEFT, KP_LEFT ->
410                {
411                    if( getSkinnable().getOrientation() == HORIZONTAL )
412                    {
413                        rtl( getSkinnable(), this::incrementValue, this::decrementValue );
414                    }
415                }
416
417                case RIGHT, KP_RIGHT ->
418                {
419                    if( getSkinnable().getOrientation() == HORIZONTAL )
420                    {
421                        rtl( getSkinnable(), this::decrementValue, this::incrementValue );
422                    }
423                }
424
425                case DOWN, KP_DOWN ->
426                {
427                    if( getSkinnable().getOrientation() == Orientation.VERTICAL )
428                    {
429                        decrementValue();
430                    }
431                }
432
433                case UP, KP_UP ->
434                {
435                    if( getSkinnable().getOrientation() == Orientation.VERTICAL )
436                    {
437                        incrementValue();
438                    }
439                }
440
441                default -> {}
442            }
443            event.consume();
444        };
445
446        @SuppressWarnings( "OverlyLongLambda" )
447        final EventHandler<KeyEvent> keyReleaseEventHandler = event ->
448        {
449            switch( event.getCode() )
450            {
451                case HOME -> home();
452                case END -> end();
453                default -> {}
454            }
455            event.consume();
456        };
457
458        getSkinnable().addEventHandler( KeyEvent.KEY_PRESSED, keyPressEventHandler );
459        getSkinnable().addEventHandler( KeyEvent.KEY_RELEASED, keyReleaseEventHandler );
460
461        /*
462         * Set up a callback to indicate which thumb is currently selected
463         * (via enum).
464         */
465        setSelectedValue( $ -> m_CurrentFocus );
466    }   //  RangeSliderSkin()
467
468        /*---------*\
469    ====** Methods **==========================================================
470        \*---------*/
471    /**
472     *  Calculates the increment/decrement value that is used by
473     *  {@link #incrementValue()}
474     *  and
475     *  {@link #decrementValue()}
476     *  to move the thumps.
477     *
478     *  @return The increment value (that is also used decrement the position
479     *      values for the thumbs).
480     */
481    private final double computeIncrement()
482    {
483        final var rangeSlider = getSkinnable();
484        final double increment;
485        if( rangeSlider.getMinorTickCount() != 0 )
486        {
487            increment = rangeSlider.getMajorTickUnit() / (max( (double) rangeSlider.getMinorTickCount(), 0.0 ) + 1);
488        }
489        else
490        {
491            increment = rangeSlider.getMajorTickUnit();
492        }
493        final var retValue = (rangeSlider.getBlockIncrement() > 0.0D) && (rangeSlider.getBlockIncrement() < increment)
494            ? increment
495            : rangeSlider.getBlockIncrement();
496
497        //---* Done *----------------------------------------------------------
498        return retValue;
499    }   //  computeIncrement()
500
501    /**
502     *  {@inheritDoc}
503     */
504    @Override
505    protected final double computeMaxHeight( final double width, final double topInset, final double rightInset, final double bottomInset, final double leftInset)
506    {
507        final var retValue = isHorizontal() ? getSkinnable().prefHeight( width ) : Double.MAX_VALUE;
508
509        //---* Done *----------------------------------------------------------
510        return retValue;
511    }   //  computeMaxHeight()
512
513    /**
514     *  {@inheritDoc}
515     */
516    @Override
517    protected final double computeMaxWidth( final double height, final double topInset, final double rightInset, final double bottomInset, final double leftInset )
518    {
519        final var retValue = isHorizontal() ? Double.MAX_VALUE : getSkinnable().prefWidth( USE_COMPUTED_SIZE );
520
521        //---* Done *----------------------------------------------------------
522        return retValue;
523    }   //  computeMaxWidth()
524
525    /**
526     *  {@inheritDoc}
527     */
528    @Override
529    protected final double computeMinHeight( final double width, final double topInset, final double rightInset, final double bottomInset, final double leftInset )
530    {
531        final var retValue = isHorizontal()
532                             ? topInset + m_LowThumb.prefHeight( USE_COMPUTED_SIZE ) + bottomInset
533                             : topInset + minTrackLength() + m_LowThumb.prefHeight( USE_COMPUTED_SIZE ) + bottomInset;
534
535        //---* Done *----------------------------------------------------------
536        return retValue;
537    }   //  computeMinHeight()
538
539    /**
540     *  {@inheritDoc}
541     */
542    @Override
543    protected final double computeMinWidth( final double height, final double topInset, final double rightInset, final double bottomInset, final double leftInset )
544    {
545        final var retValue = isHorizontal()
546                             ? leftInset + minTrackLength() + m_LowThumb.minWidth( USE_COMPUTED_SIZE ) + rightInset
547                             : leftInset + m_LowThumb.prefWidth( USE_COMPUTED_SIZE ) + rightInset;
548
549        //---* Done *----------------------------------------------------------
550        return retValue;
551    }   //  computeMinWidth()
552
553    /**
554     *  {@inheritDoc}
555     */
556    @Override
557    protected final double computePrefHeight( final double width, final double topInset, final double rightInset, final double bottomInset, final double leftInset )
558    {
559        final double retValue;
560        if( isHorizontal() )
561        {
562            retValue = getSkinnable().getInsets().getTop()
563                + max( m_LowThumb.prefHeight( USE_COMPUTED_SIZE ), m_Track.prefHeight( USE_COMPUTED_SIZE ) )
564                + (m_ShowTickMarks ? m_TrackToTickGap + m_TickLine.prefHeight( USE_COMPUTED_SIZE ) : 0.0)
565                + bottomInset;
566        }
567        else
568        {
569            retValue = m_ShowTickMarks ? max(140.0, m_TickLine.prefHeight(USE_COMPUTED_SIZE) ) : 140.0;
570        }
571
572        //---* Done *----------------------------------------------------------
573        return retValue;
574    }   //  computePrefHeight()
575
576    /**
577     *  {@inheritDoc}
578     */
579    @Override
580    protected final double computePrefWidth( final double height, final double topInset, final double rightInset, final double bottomInset, final double leftInset )
581    {
582        final double retValue;
583        if( isHorizontal() )
584        {
585            retValue = m_ShowTickMarks ? max( 140.0, m_TickLine.prefWidth( USE_COMPUTED_SIZE ) ) : 140.0;
586        }
587        else
588        {
589            retValue = leftInset
590                + max( m_LowThumb.prefWidth( USE_COMPUTED_SIZE ), m_Track.prefWidth( USE_COMPUTED_SIZE ) )
591                + (m_ShowTickMarks ? m_TrackToTickGap + m_TickLine.prefWidth( USE_COMPUTED_SIZE ) : 0.0)
592                + rightInset;
593        }
594
595        //---* Done *----------------------------------------------------------
596        return retValue;
597    }   //  computePrefWidth()
598
599    /**
600     *  Adjusts the range bar's position after it was released.
601     */
602    private void confirmRange()
603    {
604        final var rangeSlider = getSkinnable();
605
606        if( rangeSlider.isSnapToTicks() )
607        {
608            rangeSlider.setLowValue( snapValueToTicks( rangeSlider.getLowValue() ) );
609        }
610        rangeSlider.setLowValueChanging( false );
611        if( rangeSlider.isSnapToTicks() )
612        {
613            rangeSlider.setHighValue( snapValueToTicks( rangeSlider.getHighValue() ) );
614        }
615        rangeSlider.setHighValueChanging( false );
616    }   //  confirmRange()
617
618    /**
619     *  Moves the selected thumb in the direction to the
620     *  {@link RangeSlider#getMin() min}
621     *  value.
622     */
623    private final void decrementValue()
624    {
625        final var rangeSlider = getSkinnable();
626        if( nonNull( m_SelectedValue ) )
627        {
628            if ( m_SelectedValue.call(null) == HIGH_THUMB)
629            {
630                if( rangeSlider.isSnapToTicks() )
631                {
632                    rangeSlider.adjustHighValue( rangeSlider.getHighValue() - computeIncrement() );
633                }
634                else
635                {
636                    rangeSlider.decrementHighValue();
637                }
638            }
639            else
640            {
641                if( rangeSlider.isSnapToTicks() )
642                {
643                    rangeSlider.adjustLowValue( rangeSlider.getLowValue() - computeIncrement() );
644                }
645                else
646                {
647                    rangeSlider.decrementLowValue();
648                }
649            }
650        }
651    }   //  decrementValue()
652
653    /**
654     *  Responds to the END key.
655     */
656    private final void end()
657    {
658        final var rangeSlider = getSkinnable();
659        rangeSlider.adjustHighValue( rangeSlider.getMax() );
660    }   //  end()
661
662    /**
663     *  Returns the difference between
664     *  {@link RangeSlider#getMax()}
665     *  and
666     *  {@link RangeSlider#getMin()},
667     *  but if they have the same value, 1.0 is returned instead of 0.0 because
668     *  otherwise the division where the result value can be used will return
669     *  {@link Double#NaN}.
670     *
671     *  @return The difference.
672     */
673    private final double getMaxMinusMinNoZero()
674    {
675        final var rangeSlider = getSkinnable();
676        final var retValue = Double.compare( rangeSlider.getMin(), rangeSlider.getMax() ) == 0 ? 1.0 : rangeSlider.getMax() - rangeSlider.getMin();
677
678        //---* Done *----------------------------------------------------------
679        return retValue;
680    }   //  getMaxMinusMinNoZero()
681
682    /**
683     *  Updates the
684     *  {@linkplain RangeSlider#highValueProperty() high value}
685     *  based on the new position of the high thumb after it was dragged with
686     *  the mouse.
687     *
688     *  @param  ignoredMouseEvent   The mouse event.
689     *  @param  position    The mouse position on the track with 0.0 being the
690     *      start of the track and 1.0 being the end.
691     */
692    private final void highThumbDragged( final MouseEvent ignoredMouseEvent, final double position )
693    {
694        final var rangeSliderlider = getSkinnable();
695        rangeSliderlider.setHighValue( clamp( rangeSliderlider.getMin(), position * (rangeSliderlider.getMax() - rangeSliderlider.getMin()) + rangeSliderlider.getMin(), rangeSliderlider.getMax() ) );
696    }   //  highThumbDragged()
697
698    /**
699     *  <p>{@summary Prepares the dragging of the high thumb.}</p>
700     *  <p>When the high thumb is released,
701     *  {@link RangeSlider#highValueChangingProperty()}
702     *  is set to {@code false}.</p>
703     *
704     *  @param  ignoredMouseEvent   The mouse event.
705     *  @param  ignoredPosition The new position.
706     */
707    @SuppressWarnings( "SameParameterValue" )
708    private final void highThumbPressed( final MouseEvent ignoredMouseEvent, final double ignoredPosition )
709    {
710        final var rangeSlider = getSkinnable();
711        if( !rangeSlider.isFocused() ) rangeSlider.requestFocus();
712        rangeSlider.setHighValueChanging( true );
713    }   //  highThumbPressed()
714
715    /**
716     *  Adjusts the
717     *  {@linkplain RangeSlider#highValueProperty() high value}
718     *  when the high thumb is released.
719     *
720     *  @param  mouseEvent  The mouse event.
721     */
722    private final void highThumbReleased( final MouseEvent mouseEvent )
723    {
724        final var rangeSlider = getSkinnable();
725        if( rangeSlider.isSnapToTicks() )
726        {
727            rangeSlider.setHighValue( snapValueToTicks( rangeSlider.getHighValue() ) );
728        }
729        rangeSlider.setHighValueChanging( false );
730    }   //  highThumbReleased()
731
732    /**
733     *  Responds to the HOME key.
734     */
735    private final void home()
736    {
737        final var rangeSlider = getSkinnable();
738        rangeSlider.adjustHighValue( rangeSlider.getMin() );
739    }   //  home()
740
741    /**
742     *  Moves the selected thumb in the direction to the
743     *  {@link RangeSlider#getMax() max}
744     *  value.
745     */
746    private final void incrementValue()
747    {
748        final var rangeSlider = getSkinnable();
749        if( nonNull( m_SelectedValue ) )
750        {
751            if( m_SelectedValue.call(null) == HIGH_THUMB )
752            {
753                if( rangeSlider.isSnapToTicks() )
754                {
755                    rangeSlider.adjustHighValue( rangeSlider.getHighValue() + computeIncrement() );
756                }
757                else
758                {
759                    rangeSlider.incrementHighValue();
760                }
761            }
762            else
763            {
764                if( rangeSlider.isSnapToTicks() )
765                {
766                    rangeSlider.adjustLowValue( rangeSlider.getLowValue() + computeIncrement() );
767                }
768                else
769                {
770                    rangeSlider.incrementLowValue();
771                }
772            }
773        }
774    }   //  incrementValue()
775
776    /**
777     *  <p>{@summary Initialises the high thumb.}</p>
778     *  <p>Needs to be called after
779     *  {@link #initLowThumb()}
780     *  and before
781     *  {@link #initRangeBar()}.</p>
782     */
783    private final void initHighThumb()
784    {
785        m_HighThumb = new ThumbPane();
786        m_HighThumb.getStyleClass().setAll( "high-thumb" );
787        if( !getChildren().contains( m_HighThumb ) ) getChildren().add( m_HighThumb );
788
789        m_HighThumb.setOnMousePressed( e ->
790        {
791            m_LowThumb.setFocus( false );
792            m_HighThumb.setFocus( true );
793            highThumbPressed( e, 0.0D );
794            m_PreDragThumbPoint = m_HighThumb.localToParent( e.getX(), e.getY() );
795            m_PreDragPos = (getSkinnable().getHighValue() - getSkinnable().getMin()) / (getMaxMinusMinNoZero());
796        } );
797        m_HighThumb.setOnMouseReleased( this::highThumbReleased );
798        //noinspection OverlyLongLambda
799        m_HighThumb.setOnMouseDragged( mouseEvent ->
800        {
801            final var orientation = getSkinnable().getOrientation() == HORIZONTAL;
802            final var trackLength = orientation ? m_Track.getWidth() : m_Track.getHeight();
803
804            final var point2d = m_HighThumb.localToParent( mouseEvent.getX(), mouseEvent.getY() );
805            if( isNull( m_PreDragThumbPoint ) )  m_PreDragThumbPoint = point2d;
806            final var relativePosition = getSkinnable().getOrientation() == HORIZONTAL
807                ? point2d.getX() - m_PreDragThumbPoint.getX()
808                : -(point2d.getY() - m_PreDragThumbPoint.getY());
809            highThumbDragged( mouseEvent, m_PreDragPos + relativePosition / trackLength );
810        } );
811    }   //  initHighThumb()
812
813    /**
814     *  <p>{@summary Initialises the low thumb.}</p>
815     *  <p>Needs to be called before
816     *  {@link #initHighThumb()}.</p>
817     */
818    private final void initLowThumb()
819    {
820        m_LowThumb = new ThumbPane();
821        m_LowThumb.getStyleClass().setAll( "low-thumb" );
822        m_LowThumb.setFocusTraversable( true );
823        m_Track = new StackPane();
824        m_Track.setFocusTraversable( false );
825        m_Track.getStyleClass().setAll( "track" );
826
827        getChildren().clear();
828        getChildren().addAll( m_Track, m_LowThumb );
829        setShowTickMarks( getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels() );
830
831        m_Track.setOnMousePressed( me ->
832        {
833            if( !m_LowThumb.isPressed() && !m_HighThumb.isPressed() )
834            {
835                if( isHorizontal() )
836                {
837                    trackPress( me, (me.getX() / m_TrackLength) );
838                }
839                else
840                {
841                    trackPress( me, (me.getY() / m_TrackLength) );
842                }
843            }
844        } );
845
846        m_LowThumb.setOnMousePressed( me ->
847        {
848            m_HighThumb.setFocus( false );
849            m_LowThumb.setFocus( true );
850            lowThumbPressed( me, 0.0f );
851            m_PreDragThumbPoint = m_LowThumb.localToParent( me.getX(), me.getY() );
852            m_PreDragPos = (getSkinnable().getLowValue() - getSkinnable().getMin()) / getMaxMinusMinNoZero();
853        } );
854
855        m_LowThumb.setOnMouseReleased( this::lowThumbReleased );
856
857        m_LowThumb.setOnMouseDragged( mouseEvent ->
858        {
859            final var cur = m_LowThumb.localToParent( mouseEvent.getX(), mouseEvent.getY() );
860            if( isNull( m_PreDragThumbPoint ) )  m_PreDragThumbPoint = cur;
861            final var dragPos = isHorizontal() ? cur.getX() - m_PreDragThumbPoint.getX() : -(cur.getY() - m_PreDragThumbPoint.getY());
862            lowThumbDragged( mouseEvent, m_PreDragPos + dragPos / m_TrackLength );
863        } );
864    }   //  initLowThumb()
865
866    /**
867     *  <p>{@summary Initialises the range bar.}</p>
868     *  <p>Needs to be called after
869     *  {@link #initHighThumb()}.</p>
870     */
871    private final void initRangeBar()
872    {
873        m_RangeBar = new StackPane();
874        m_RangeBar.setFocusTraversable( false );
875        //noinspection AnonymousInnerClass
876        m_RangeBar.cursorProperty().bind( new ObjectBinding<>()
877        {
878            {
879                bind( m_RangeBar.hoverProperty() );
880            }
881
882            /**
883             *  {@inheritDoc}
884             */
885            @Override
886            protected final Cursor computeValue()
887            {
888                return m_RangeBar.isHover() ? Cursor.HAND : Cursor.DEFAULT;
889            }
890        } );
891        m_RangeBar.getStyleClass().setAll( "range-bar" );
892
893        m_RangeBar.setOnMousePressed( e ->
894        {
895            m_RangeBar.requestFocus();
896            m_PreDragPos = isHorizontal() ? e.getX() : -e.getY();
897        } );
898
899        m_RangeBar.setOnMouseDragged( e ->
900        {
901            final var delta = (isHorizontal() ? e.getX() : -e.getY()) - m_PreDragPos;
902            moveRange( delta );
903        } );
904
905        m_RangeBar.setOnMouseReleased( $ -> confirmRange() );
906        getChildren().add( m_RangeBar );
907    }   //  initRangeBar()
908
909    /**
910     *  Checks whether the orientation of the
911     *  {@link RangeSlider}
912     *  is
913     *  {@linkplain Orientation#HORIZONTAL horizontal}.
914     *
915     *  @return {@code true} if the orientation is
916     *      {@link Orientation#HORIZONTAL},
917     *      {@code false} when it is
918     *      {@link Orientation#VERTICAL}.
919     */
920    private final boolean isHorizontal() { return isNull( m_Orientation ) || m_Orientation == HORIZONTAL; }
921
922    /**
923     *  {@inheritDoc}
924     */
925    @SuppressWarnings( {"OverlyComplexMethod", "MagicNumber"} )
926    @Override
927    protected final void layoutChildren( final double contentX, final double contentY, final double contentWidth, final double contentHeight )
928    {
929        //---* Resize thumb to preferred size *--------------------------------
930        m_ThumbWidth = m_LowThumb.prefWidth( USE_COMPUTED_SIZE );
931        m_ThumbHeight = m_LowThumb.prefHeight( USE_COMPUTED_SIZE );
932        m_LowThumb.resize( m_ThumbWidth, m_ThumbHeight );
933
934        /*
935         * We assume there is a common radius for all corners on the track.
936         */
937        final var trackRadius = isNull( m_Track.getBackground() )
938            ? 0.0
939            : m_Track.getBackground().getFills().isEmpty()
940                ? 0.0
941                : m_Track.getBackground().getFills().getFirst().getRadii().getTopLeftHorizontalRadius();
942
943        if( isHorizontal() )
944        {
945            final var tickLineHeight = m_ShowTickMarks ? m_TickLine.prefHeight( USE_COMPUTED_SIZE ) : 0.0;
946            final var trackHeight = m_Track.prefHeight( USE_COMPUTED_SIZE );
947            final var trackAreaHeight = max( trackHeight, m_ThumbHeight );
948            final var totalHeightNeeded = trackAreaHeight  + ((m_ShowTickMarks) ? m_TrackToTickGap + tickLineHeight : 0.0);
949
950            //---* Vertically center slider in available height *--------------
951            final var startY = contentY + ((contentHeight - totalHeightNeeded) / 2.0);
952
953            m_TrackLength = contentWidth - m_ThumbWidth;
954            m_TrackStart = contentX + (m_ThumbWidth / 2.0);
955            @SuppressWarnings( "NumericCastThatLosesPrecision" )
956            final var trackTop = (double) ((int) (startY + ((trackAreaHeight - trackHeight) / 2.0)));
957            //noinspection NumericCastThatLosesPrecision
958            m_LowThumbPos = (double) ((int) (startY + ((trackAreaHeight - m_ThumbHeight) / 2.0)));
959
960            positionLowThumb();
961
962            //---* Now do the layout for the track *---------------------------
963            m_Track.resizeRelocate( m_TrackStart - trackRadius, trackTop , m_TrackLength + trackRadius + trackRadius, trackHeight );
964
965            positionHighThumb();
966
967            //---* Do the range bar layout *-----------------------------------
968            m_RangeBar.resizeRelocate( m_RangeStart, trackTop, m_RangeEnd - m_RangeStart, trackHeight );
969
970            //---* Do the layout for the tick line *---------------------------
971            if( m_ShowTickMarks )
972            {
973                m_TickLine.setLayoutX( m_TrackStart );
974                m_TickLine.setLayoutY( trackTop + trackHeight + m_TrackToTickGap );
975                m_TickLine.resize( m_TrackLength, tickLineHeight );
976                m_TickLine.requestAxisLayout();
977            }
978            else
979            {
980                if( nonNull( m_TickLine ) )
981                {
982                    m_TickLine.resize(0,0 );
983                    m_TickLine.requestAxisLayout();
984                }
985                m_TickLine = null;
986            }
987        }
988        else
989        {
990            final var tickLineWidth = m_ShowTickMarks ? m_TickLine.prefWidth( USE_COMPUTED_SIZE ) : 0.0;
991            final var trackWidth = m_Track.prefWidth( USE_COMPUTED_SIZE );
992            final var trackAreaWidth = max( trackWidth, m_ThumbWidth );
993            final var totalWidthNeeded = trackAreaWidth  + (m_ShowTickMarks ? m_TrackToTickGap + tickLineWidth : 0.0) ;
994
995            //---* Horizontally center the slider in available width *---------
996            final var startX = contentX + ((contentWidth - totalWidthNeeded) / 2.0);
997            m_TrackLength = contentHeight - m_ThumbHeight;
998            m_TrackStart = contentY + (m_ThumbHeight / 2.0);
999            @SuppressWarnings( "NumericCastThatLosesPrecision" )
1000            final var trackLeft = (double) ((int) (startX + ((trackAreaWidth - trackWidth) / 2.0)));
1001            //noinspection NumericCastThatLosesPrecision
1002            m_LowThumbPos = (double) ((int) (startX + ((trackAreaWidth - m_ThumbWidth) / 2.0)));
1003
1004            positionLowThumb();
1005
1006            //---* Now do the layout for the track *---------------------------
1007            m_Track.resizeRelocate( trackLeft, m_TrackStart - trackRadius, trackWidth, m_TrackLength + trackRadius + trackRadius );
1008
1009            positionHighThumb();
1010
1011            //---* Do the range bar layout *-----------------------------------
1012            m_RangeBar.resizeRelocate( trackLeft, m_RangeStart, trackWidth, m_RangeEnd - m_RangeStart );
1013
1014            //---* Do the layout for the tick line *---------------------------
1015            if( m_ShowTickMarks )
1016            {
1017                m_TickLine.setLayoutX( trackLeft + trackWidth + m_TrackToTickGap );
1018                m_TickLine.setLayoutY( m_TrackStart );
1019                m_TickLine.resize( tickLineWidth, m_TrackLength );
1020                m_TickLine.requestAxisLayout();
1021            }
1022            else
1023            {
1024                if( nonNull( m_TickLine ) )
1025                {
1026                    m_TickLine.resize( 0,0 );
1027                    m_TickLine.requestAxisLayout();
1028                }
1029                m_TickLine = null;
1030            }
1031        }
1032    }   //  layoutChildren()
1033
1034    /**
1035     *  Updates the
1036     *  {@linkplain RangeSlider#lowValueProperty() low value}
1037     *  based on the new position of the low thumb after it was dragged with
1038     *  the mouse.
1039     *
1040     *  @param  ignoredMouseEvent   The mouse event.
1041     *  @param  position    The mouse position on the track with 0.0 being the
1042     *      start of the track and 1.0 being the end.
1043     */
1044    public final void lowThumbDragged( final MouseEvent ignoredMouseEvent, final double position )
1045    {
1046        final var rangeSlider = getSkinnable();
1047        final var newValue = clamp
1048            (
1049                rangeSlider.getMin(),
1050                (position * (rangeSlider.getMax() - rangeSlider.getMin())) + rangeSlider.getMin(),
1051                rangeSlider.getMax()
1052            );
1053        rangeSlider.setLowValue( newValue );
1054    }   //  lowThumbDragged()
1055
1056    /**
1057     *  <p>{@summary Prepares the dragging of the low thumb.}</p>
1058     *  <p>When the low thumb is released,
1059     *  {@link RangeSlider#lowValueChangingProperty()}
1060     *  is set to {@code false}.</p>
1061     *
1062     *  @param  ignoredMouseEvent   The mouse event.
1063     *  @param  ignoredPosition The new position.
1064     */
1065    public void lowThumbPressed( final MouseEvent ignoredMouseEvent, final double ignoredPosition )
1066    {
1067        //---* If not already focused, request focus *-------------------------
1068        final var rangeSlider = getSkinnable();
1069        if( !rangeSlider.isFocused() )  rangeSlider.requestFocus();
1070
1071        rangeSlider.setLowValueChanging( true );
1072    }   //  lowThumbPressed()
1073
1074    /**
1075     *  Adjusts the
1076     *  {@linkplain RangeSlider#lowValueProperty() low value}
1077     *  when the low thumb is released.
1078     *
1079     *  @param  mouseEvent  The mouse event.
1080     */
1081    public final void lowThumbReleased( final MouseEvent mouseEvent )
1082    {
1083        final var rangeSlider = getSkinnable();
1084        if( rangeSlider.isSnapToTicks() )
1085        {
1086            rangeSlider.setLowValue( snapValueToTicks( rangeSlider.getLowValue() ) );
1087        }
1088        rangeSlider.setLowValueChanging( false );
1089    }   //  lowThumbReleased()
1090
1091    /**
1092     *  Calculates the minimum length for the track.
1093     *
1094     *  @return The minimum track length.
1095     */
1096    private final double minTrackLength()
1097    {
1098        final var retValue = 2.0 * m_LowThumb.prefWidth( USE_COMPUTED_SIZE );
1099
1100        //---* Done *----------------------------------------------------------
1101        return retValue;
1102    }   //  minTrackLength()
1103
1104    /**
1105     *  Sets the new positions after a move of the range bar.
1106     *
1107     *  @param  position    The new position.
1108     */
1109    private final void moveRange( final double position )
1110    {
1111        final var rangeSlider = getSkinnable();
1112        final var min = rangeSlider.getMin();
1113        final var max = rangeSlider.getMax();
1114        final var lowValue = rangeSlider.getLowValue();
1115        final var newLowValue = clamp
1116            (
1117                min,
1118                lowValue + position * (max-min) / (isHorizontal() ? rangeSlider.getWidth(): rangeSlider.getHeight()),
1119                max
1120            );
1121        final var highValue = rangeSlider.getHighValue();
1122        final var newHighValue = clamp
1123            (
1124                min,
1125                highValue + position * (max-min) / (isHorizontal() ? rangeSlider.getWidth(): rangeSlider.getHeight() ),
1126                max
1127            );
1128
1129        if( (newLowValue > min) && (newHighValue < max) )
1130        {
1131            rangeSlider.setLowValueChanging( true );
1132            rangeSlider.setHighValueChanging( true );
1133            rangeSlider.setLowValue( newLowValue);
1134            rangeSlider.setHighValue( newHighValue);
1135        }
1136    }   //  moveRange()
1137
1138    /**
1139     *  Called whenever either
1140     *  {@link RangeSlider#getMin() min},
1141     *  {@link RangeSlider#getMax() max}
1142     *  or
1143     *  {@link RangeSlider#getHighValue() highValue}
1144     *  has changed, so that high thumb's
1145     *  {@link ThumbPane#setLayoutX(double)}
1146     *  and
1147     *  {@link ThumbPane#setLayoutY(double)}
1148     *  is recomputed.
1149     */
1150    private final void positionHighThumb()
1151    {
1152        final var rangeSlider = getSkinnable();
1153        final var horizontal = getSkinnable().getOrientation() == HORIZONTAL;
1154
1155        final var thumbWidth = m_LowThumb.getWidth();
1156        final var thumbHeight = m_LowThumb.getHeight();
1157        m_HighThumb.resize( thumbWidth, thumbHeight );
1158
1159        final var pad = 0.0d;
1160        var trackStart = horizontal ? m_Track.getLayoutX() : m_Track.getLayoutY();
1161        trackStart += pad;
1162        var trackLength = horizontal ? m_Track.getWidth() : m_Track.getHeight();
1163        trackLength -= 2 * pad;
1164
1165        @SuppressWarnings( "OverlyComplexArithmeticExpression" )
1166        final var lx = horizontal
1167            ? trackStart + (trackLength * ((rangeSlider.getHighValue() - rangeSlider.getMin()) / (getMaxMinusMinNoZero())) - thumbWidth / 2.0 )
1168            : m_LowThumb.getLayoutX();
1169        final var ly = horizontal
1170            ? m_LowThumb.getLayoutY()
1171            : (getSkinnable().getInsets().getTop() + trackLength) - trackLength * ((rangeSlider.getHighValue() - rangeSlider.getMin()) / (getMaxMinusMinNoZero()));
1172        m_HighThumb.setLayoutX( lx );
1173        m_HighThumb.setLayoutY( ly );
1174        if( horizontal )
1175        {
1176            m_RangeEnd = lx;
1177        }
1178        else
1179        {
1180            m_RangeStart = ly + thumbHeight;
1181        }
1182    }   //  positionHighThumb()
1183
1184    /**
1185     *  Called whenever either
1186     *  {@link RangeSlider#getMin() min},
1187     *  {@link RangeSlider#getMax() max}
1188     *  or
1189     *  {@link RangeSlider#getLowValue() lowValue}
1190     *  has changed, so that low thumb's
1191     *  {@link ThumbPane#setLayoutX(double)}
1192     *  and
1193     *  {@link ThumbPane#setLayoutY(double)}
1194     *  is recomputed.
1195     */
1196    private final void positionLowThumb()
1197    {
1198        final var rangeSlider = getSkinnable();
1199        final var horizontal = isHorizontal();
1200        @SuppressWarnings( "OverlyComplexArithmeticExpression" )
1201        final var lx = horizontal
1202            ? m_TrackStart + (((m_TrackLength * ((rangeSlider.getLowValue() - rangeSlider.getMin()) / (getMaxMinusMinNoZero()))) - m_ThumbWidth / 2.0))
1203            : m_LowThumbPos;
1204        final var ly = horizontal
1205            ? m_LowThumbPos
1206            : getSkinnable().getInsets().getTop() + m_TrackLength - (m_TrackLength * ((rangeSlider.getLowValue() - rangeSlider.getMin()) / getMaxMinusMinNoZero()));
1207        m_LowThumb.setLayoutX( lx );
1208        m_LowThumb.setLayoutY( ly );
1209        if( horizontal )
1210        {
1211            m_RangeStart = lx + m_ThumbWidth;
1212        }
1213        else
1214        {
1215            m_RangeEnd = ly;
1216        }
1217    }   //  positionLowThumb()
1218
1219    /**
1220     *  Implements the inverted orientation.
1221     *
1222     *  @param  node    A reference to the node.
1223     *  @param  rtlMethod   The function that has to be used for an orientation
1224     *      from right to left.
1225     *  @param  nonRtlMethod    The function that has to be used for an
1226     *      orientation from left to right.
1227     */
1228    private final void rtl( final RangeSlider node, final Runnable rtlMethod, final Runnable nonRtlMethod )
1229    {
1230        switch( requireNonNullArgument( node, "node" ).getEffectiveNodeOrientation() )
1231        {
1232            case null -> throw new IllegalStateException( "Effective node orientation is null" );
1233            case RIGHT_TO_LEFT -> requireNonNullArgument( rtlMethod, "rtlMethod" ).run();
1234            case LEFT_TO_RIGHT -> requireNonNullArgument( nonRtlMethod, "nonRtlMethod" ).run();
1235            default -> throw new IllegalArgumentException( "Unexpected node orientation: %s".formatted( node.getEffectiveNodeOrientation().name() ) );
1236        }
1237    }   //  rtl()
1238
1239    /**
1240     *  Set up a callback to indicate which thumb is currently selected
1241     *  (via enum).
1242     *
1243     *  @param  callback    The callback.
1244     */
1245    private void setSelectedValue( final Callback<Void,FocusedChild> callback ) { m_SelectedValue = callback; }
1246
1247    /**
1248     *  <p>{@summary Shows tick marks and their labels.}</p>
1249     *  <p>When ticks or labels change their visibility, we have to compute the
1250     *  new visibility and to add the necessary objects. After this method
1251     *  returns, we must be sure to add the high thumb and the range bar.</p>
1252     *
1253     *  @param  ticksVisible    {@code true} if the tick marks are visible,
1254     *      {@code false} if not.
1255     *  @param  labelsVisible   {@code true} if the tick labels are visible,
1256     *      {@code false} if not.
1257     */
1258    private void setShowTickMarks( final boolean ticksVisible, final boolean labelsVisible)
1259    {
1260        m_ShowTickMarks = (ticksVisible || labelsVisible);
1261        final var rangeSlider = getSkinnable();
1262        if( m_ShowTickMarks )
1263        {
1264            if( isNull( m_TickLine ) )
1265            {
1266                m_TickLine = new NumberAxis();
1267                m_TickLine.setFocusTraversable( false );
1268                m_TickLine.tickLabelFormatterProperty().bind( getSkinnable().labelFormatterProperty() );
1269                m_TickLine.setAnimated( false );
1270                m_TickLine.setAutoRanging( false );
1271                m_TickLine.setSide( isHorizontal() ? Side.BOTTOM : Side.RIGHT );
1272                m_TickLine.setUpperBound( rangeSlider.getMax() );
1273                m_TickLine.setLowerBound( rangeSlider.getMin() );
1274                m_TickLine.setTickUnit( rangeSlider.getMajorTickUnit() );
1275                m_TickLine.setTickMarkVisible( ticksVisible );
1276                m_TickLine.setTickLabelsVisible( labelsVisible );
1277                m_TickLine.setMinorTickVisible( ticksVisible );
1278
1279                /*
1280                 * We add 1 to the slider minor tick count since the axis draws
1281                 * one less minor ticks than the number given.
1282                 */
1283                m_TickLine.setMinorTickCount( Integer.max( rangeSlider.getMinorTickCount(), 0 ) + 1) ;
1284                getChildren().clear();
1285                getChildren().addAll( m_TickLine, m_Track, m_LowThumb );
1286            }
1287            else
1288            {
1289                m_TickLine.setTickLabelsVisible( labelsVisible );
1290                m_TickLine.setTickMarkVisible( ticksVisible );
1291                m_TickLine.setMinorTickVisible( ticksVisible );
1292            }
1293        }
1294        else
1295        {
1296            getChildren().clear();
1297            getChildren().addAll( m_Track, m_LowThumb );
1298            // m_TickLine = null;
1299        }
1300
1301        getSkinnable().requestLayout();
1302    }   //  setShowTickMarks()
1303
1304    /**
1305     *  Adjusts the position of a thumb to the nearest tick mark.
1306     *
1307     *  @param  calculatedPosition  The calculated raw position.
1308     *  @return The adjusted position.
1309     */
1310    private final double snapValueToTicks( final double calculatedPosition )
1311    {
1312        final var rangeSlider = getSkinnable();
1313        var input = calculatedPosition;
1314        final var d2 = (rangeSlider.getMinorTickCount() != 0)
1315            ? rangeSlider.getMajorTickUnit() / (max( (double) rangeSlider.getMinorTickCount(), 0.0 ) + 1)
1316            : rangeSlider.getMajorTickUnit();
1317        @SuppressWarnings( "NumericCastThatLosesPrecision" )
1318        final var leftTicks = (int) ((input - rangeSlider.getMin()) / d2);
1319        final var d3 = (double) leftTicks * d2 + rangeSlider.getMin();
1320        final var d4 = (double) (leftTicks + 1) * d2 + rangeSlider.getMin();
1321        input = nearest( d3, input, d4) ;
1322        final var retValue = clamp( rangeSlider.getMin(), input, rangeSlider.getMax() );
1323
1324        //---* Done *----------------------------------------------------------
1325        return retValue;
1326    }   //  snapValueToTicks(
1327
1328    /**
1329     *  Invoked by the
1330     *  {@link RangeSlider}'s
1331     *  {@link Skin}
1332     *  implementation whenever a mouse press occurs on the &quot;track&quot;
1333     *  of the slider. This will cause the thumb to be moved by some amount.
1334     *
1335     *  @param  ignoredMouseEvent   The mouse event.
1336     *  @param  position    The relative mouse position on the track, with 0.0
1337     *      being the start of the track and 1.0 being the end.
1338     */
1339    private final void trackPress( final MouseEvent ignoredMouseEvent, final double position )
1340    {
1341
1342        final var rangeSlider = getSkinnable();
1343        //---* If not already focused, request focus *-------------------------
1344        if( !rangeSlider.isFocused() )  rangeSlider.requestFocus();
1345
1346        if( nonNull( m_SelectedValue ) )
1347        {
1348            /*
1349             * Determine the percentage of the way between min and max
1350             * represented by this mouse event.
1351             */
1352            final double newPosition;
1353            if( isHorizontal() )
1354            {
1355                newPosition = position * (rangeSlider.getMax() - rangeSlider.getMin()) + rangeSlider.getMin();
1356            }
1357            else
1358            {
1359                newPosition = (1 - position) * (rangeSlider.getMax() - rangeSlider.getMin()) + rangeSlider.getMin();
1360            }
1361
1362            /*
1363             * If the position is inferior to the current LowValue, this means
1364             * the user clicked on the track to move the low thumb. If not,
1365             * then it means the user wanted to move the high thumb.
1366             */
1367            if( newPosition < rangeSlider.getLowValue() )
1368            {
1369                rangeSlider.adjustLowValue( newPosition );
1370            }
1371            else
1372            {
1373                rangeSlider.adjustHighValue(newPosition);
1374            }
1375        }
1376    }   //  trackPress()
1377}
1378//  class RangeSliderSkin
1379
1380/*
1381 *  End of File
1382 */