UIForSlider.java

package swingtree;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.Action;
import sprouts.From;
import sprouts.Val;
import sprouts.Var;

import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 *  A SwingTree builder node designed for configuring {@link JSlider} instances.
 * 	<p>
 * 	<b>Please take a look at the <a href="https://globaltcad.github.io/swing-tree/">living swing-tree documentation</a>
 * 	where you can browse a large collection of examples demonstrating how to use the API of this class.</b>
 *
 * @param <S> The type of {@link JSlider} that this {@link UIForSlider} is configuring.
 */
public final class UIForSlider<S extends JSlider> extends UIForAnySwing<UIForSlider<S>, S>
{
    private static final int PREFERRED_STEPS = 256;
    private static final Logger log = LoggerFactory.getLogger(UIForSlider.class);

    private final BuilderState<S> _state;

    UIForSlider( BuilderState<S> state ) {
        Objects.requireNonNull(state);
        _state = state;
    }

    @Override
    protected BuilderState<S> _state() {
        return _state;
    }
    
    @Override
    protected UIForSlider<S> _newBuilderWithState(BuilderState<S> newState ) {
        return new UIForSlider<>(newState);
    }

    /**
     *  Sets the orientation of the slider.
     *  @param align The orientation of the slider.
     *  @return This builder node.
     */
    public final UIForSlider<S> withOrientation( UI.Align align ) {
        NullUtil.nullArgCheck( align, "align", UI.Align.class );
        return _with( thisComponent -> {
                   _setOrientation( thisComponent, align );
               })
               ._this();
    }

    private void _setOrientation( S thisComponent, UI.Align align ) {
        _doWithoutListeners(thisComponent,
            () -> thisComponent.setOrientation(align.forSlider())
        );
    }

    /**
     *  Dynamically sets the orientation of the slider.
     *  @param align The orientation of the slider.
     *  @return This builder node.
     */
    public final UIForSlider<S> withOrientation( Val<UI.Align> align ) {
        NullUtil.nullArgCheck( align, "align", Val.class );
        NullUtil.nullPropertyCheck( align, "align", "Null is not a valid alignment" );
        return _withOnShow( align, (thisComponent,v) -> {
                    _setOrientation(thisComponent, align.orElseThrowUnchecked());
               })
                ._with( thisComponent -> {
                    _setOrientation(thisComponent, align.orElseThrowUnchecked());
                })
               ._this();
    }

    /**
     * Adds an {@link Action} to the underlying {@link JSlider}
     * through an {@link javax.swing.event.ChangeListener},
     * which will be called when the state of the slider changes.
     * For more information see {@link JSlider#addChangeListener(javax.swing.event.ChangeListener)}.
     *
     * @param action The {@link Action} that will be called through the underlying change event.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code action} is {@code null}.
     */
    public final UIForSlider<S> onChange( Action<ComponentDelegate<JSlider, ChangeEvent>> action ) {
        NullUtil.nullArgCheck( action, "action", Action.class );
        return _with( thisComponent -> {
                    _onChange(thisComponent,
                        e -> _runInApp(()->{
                            try {
                                action.accept(new ComponentDelegate<>(thisComponent, e));
                            } catch (Exception ex) {
                                log.error("Error while executing action on slider change!", ex);
                            }
                        })
                    );
                })
                ._this();
    }

    private void _onChange( S thisComponent, Consumer<ChangeEvent> action ) {
        thisComponent.addChangeListener(action::accept);
    }

    /**
     * Sets the minimum value of the slider.
     * For more information see {@link JSlider#setMinimum(int)}.
     *
     * @param min The minimum value of the slider.
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSlider<S> withMin( int min ) {
        return _with( thisComponent -> {
                    _setMin( thisComponent, min );
                })
                ._this();
    }

    private void _setMin( S thisComponent, int min ) {
        _doWithoutListeners(thisComponent, ()->thisComponent.setMinimum( min ));
    }

    /**
     *  Binds the supplied {@link Val} property to the min value of the slider
     *  so that when the value of the property changes, the min value of the slider will be updated accordingly.
     *  For more information about the underlying value in the component, see {@link JSlider#setMinimum(int)}.
     *
     * @param min The min property used to dynamically update the min value of the slider.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code min} is {@code null}.
     */
    public final UIForSlider<S> withMin( Val<Integer> min ) {
        NullUtil.nullArgCheck( min, "min", Val.class );
        return _withOnShow( min, (thisComponent,v) -> {
                    _setMin(thisComponent, min.orElseThrowUnchecked());
                })
                ._with( thisComponent -> {
                    _setMin(thisComponent, min.orElseThrowUnchecked());
                })
                ._this();
    }

    /**
     * Sets the maximum value of the slider.
     * For more information see {@link JSlider#setMaximum(int)} (int)}.
     *
     * @param max The maximum value of the slider.
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSlider<S> withMax( int max ) {
        return _with( thisComponent -> {
                    _setMax( thisComponent, max );
                })
                ._this();
    }

    private void _setMax( S thisComponent, int max ) {
        _doWithoutListeners(thisComponent, ()->thisComponent.setMaximum( max ));
    }

    /**
     *  Binds the supplied {@link Val} property to the max value of the slider.
     *  When the value of the property changes, the max value of the slider will be updated accordingly.
     *  For more information about the underlying value in the component, see {@link JSlider#setMaximum(int)}.
     *
     * @param max An integer property used to dynamically update the max value of the slider.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code max} is {@code null}.
     */
    public final UIForSlider<S> withMax( Val<Integer> max ) {
        NullUtil.nullArgCheck( max, "max", Val.class );
        return _withOnShow( max, (thisComponent,v) -> {
                    _setMax(thisComponent, max.orElseThrowUnchecked());
                })
                ._with( thisComponent -> {
                    _setMax(thisComponent, max.orElseThrowUnchecked());
                })
                ._this();
    }

    /**
     * Sets the current value of the slider.
     * For more information see {@link JSlider#setValue(int)}.
     *
     * @param value The current value of the slider.
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSlider<S> withValue( int value ) {
        return _with( thisComponent -> {
                    _setValue( thisComponent, value );
                })
                ._this();
    }

    private void _setValue( S thisComponent, int value ) {
        _doWithoutListeners(thisComponent, ()->thisComponent.setValue( value ));
    }

    /**
     *  Binds the supplied {@link Val} property to the value of the slider,
     *  which causes the knob of the slider to move when the value of the property changes.
     *  But note that the supplied property is a read only, so when the user updates
     *  the value of the slider, the property will not be updated.
     *  Use {@link #withValue(Var)} if you want to bind a property bidirectionally.
     *  For more information about the underlying value in the component, see {@link JSlider#setValue(int)}.
     *
     * @param val An integer property used to dynamically update the value of the slider.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code value} is {@code null}.
     */
    public final UIForSlider<S> withValue( Val<Integer> val ) {
        NullUtil.nullArgCheck( val, "val", Val.class );
        return _withOnShow( val, (thisComponent,v) -> {
                    _setValue(thisComponent, val.orElseThrowUnchecked());
                })
                ._with( thisComponent -> {
                    _setValue(thisComponent, val.orElseThrowUnchecked());
                })
                ._this();
    }

    final <N extends Number> UIForSlider<S> _withBinding(
        Val<N> userMin, Val<N> userMax, Val<N> userCurrent, boolean biDirectional
    ) {
        Objects.requireNonNull(userMin);
        Objects.requireNonNull(userMax);
        Objects.requireNonNull(userCurrent);
        boolean allOfTheSameType = userMin.type() == userMax.type() &&
                                   userMax.type() == userCurrent.type();

        if ( !allOfTheSameType )
            throw new IllegalArgumentException("Min, max and current slider values must all be of the same type.");

        Class<N> userType = userMin.type();
        boolean isWholeNumber = userType == Integer.class || userType == Long.class || userType == Short.class || userType == Byte.class;
        if ( !isWholeNumber ) {
            Function<N,Integer> scaleToSliderInt = n -> _scale(Integer.class, n, userMin.orElseThrowUnchecked(), userMax.orElseThrowUnchecked(), false);
            Val<Integer> sliderMin     = userMin.viewAsInt( scaleToSliderInt );
            Val<Integer> sliderMax     = userMax.viewAsInt( scaleToSliderInt );
            Val<Integer> sliderCurrent = userCurrent.viewAsInt( scaleToSliderInt );
            return _withBindingInternal(
                        sliderMin, sliderMax, sliderCurrent, biDirectional ? (Var) userCurrent : null,
                        n -> {
                            if ( sliderMin.is(n) )
                                return userMin.orElseThrowUnchecked();
                            if ( sliderMax.is(n) )
                                return userMax.orElseThrowUnchecked();
                            return _scale(userType, n, userMin.orElseThrowUnchecked(), userMax.orElseThrowUnchecked(), true);
                        }
                    );
        }
        if ( userType != Integer.class )
            return _withBindingInternal(
                    userMin.viewAsInt( n -> _convertTo(Integer.class, n) ),
                    userMax.viewAsInt( n -> _convertTo(Integer.class, n) ),
                    userCurrent.viewAsInt( n -> _convertTo(Integer.class, n) ),
                    biDirectional ? (Var<N>) userCurrent : null,
                    n->_convertTo(userType, n)
                );
        else
            return _withBindingInternal(
                    userMin,
                    userMax,
                    userCurrent,
                    biDirectional ? (Var<N>) userCurrent : null,
                    n->_convertTo(userType, n)
                );
    }

    final <N extends Number, T extends Number> UIForSlider<S> _withBindingInternal(
        Val<N> min, Val<N> max, Val<N> current, @Nullable Var<T> target, Function<Integer,T> scaling
    ) {
        return _withOnShow( min, (thisComponent,v) -> {
                    _setMin(thisComponent, min.orElseThrowUnchecked().intValue());
                })
                ._withOnShow( max, (thisComponent,v) -> {
                    _setMax(thisComponent, max.orElseThrowUnchecked().intValue());
                })
                ._withOnShow( current, (thisComponent,v) -> {
                    _setValue(thisComponent, current.orElseThrowUnchecked().intValue());
                })
                ._with( thisComponent -> {
                    _setMin(thisComponent, min.orElseThrowUnchecked().intValue());
                    _setMax(thisComponent, max.orElseThrowUnchecked().intValue());
                    _setValue(thisComponent, current.orElseThrowUnchecked().intValue());
                    if ( target != null ) {
                        _onChange(thisComponent,
                            e -> _runInApp(thisComponent.getValue(), newItem -> {
                                T targetItem = scaling.apply(newItem);
                                target.set(From.VIEW, targetItem);
                            })
                        );
                    }
                })
                ._this();
    }

    private <N extends Number> N _scale( Class<N> target, Number in, Number min, Number max, boolean inverse ) {
        double scale = _goodScaleFor(min, max);
        if ( inverse )
            scale = 1.0 / scale;
        double newValue = in.doubleValue() * scale;
        // No we convert the new value to the target type.
        return _convertTo(target, newValue);
    }

    private <N extends Number> N _convertTo( Class<N> target, Number in ) {
        if ( target == Integer.class )
            return target.cast(in.intValue());
        if ( target == Long.class )
            return target.cast(in.longValue());
        if ( target == Short.class )
            return target.cast(in.shortValue());
        if ( target == Byte.class )
            return target.cast(in.byteValue());
        if ( target == Float.class )
            return target.cast(in.floatValue());
        if ( target == Double.class )
            return target.cast(in.doubleValue());
        throw new IllegalArgumentException("Unsupported number type: " + target);
    }


    private <N extends Number> double _goodScaleFor( N min, N max ) {
        double minVal = min.doubleValue();
        double maxVal = max.doubleValue();
        double diff = maxVal - minVal;
        // The scale should ensure that we have at least PREFERRED_STEPS steps in integer values.
        return PREFERRED_STEPS / diff;
    }

    /**
     *  Use this to bind the supplied {@link Var} property to the value of the slider.
     *  When the value of the slider changes, the value of the {@link Var} will be updated
     *  and when the item of the {@link Var} is changed as part of the application logic,
     *  the value of the slider will be updated accordingly.
     *
     * @param var An integer property used to dynamically update the value of the slider.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code value} is {@code null}.
     */
    public final UIForSlider<S> withValue( Var<Integer> var ) {
        NullUtil.nullArgCheck( var, "var", Var.class );
        return _withOnShow( var, (thisComponent,v) -> {
                    _setValue(thisComponent, v);
                })
                ._with( thisComponent -> {
                    _onChange(thisComponent,
                        e -> _runInApp(thisComponent.getValue(), newItem -> var.set(From.VIEW, newItem) )
                    );
                    _setValue(thisComponent, var.orElseThrowUnchecked());
                })
                ._this();
    }

    /**
     * Sets the major tick spacing of the slider.
     * For more information see {@link JSlider#setMajorTickSpacing(int)}.
     *
     * @param spacing The major tick spacing of the slider.
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSlider<S> withMajorTickSpacing( int spacing ) {
        return _with( thisComponent -> {
                    thisComponent.setMajorTickSpacing( spacing );
                })
                ._this();
    }

    /**
     * Sets the minor tick spacing of the slider.
     * For more information see {@link JSlider#setMinorTickSpacing(int)}.
     *
     * @param spacing The minor tick spacing of the slider.
     * @return This very instance, which enables builder-style method chaining.
     */
    public final UIForSlider<S> withMinorTickSpacing( int spacing ) {
        return _with( thisComponent -> {
                    thisComponent.setMinorTickSpacing( spacing );
                })
                ._this();
    }

    /**
     * Dynamically sets the major tick spacing of the slider.
     * For more information see {@link JSlider#setMajorTickSpacing(int)}.
     * @param spacing The major tick spacing of the slider.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code spacing} is {@code null}.
     */
    public final UIForSlider<S> withMajorTickSpacing( Val<Integer> spacing ) {
        NullUtil.nullArgCheck( spacing, "spacing", Val.class );
        NullUtil.nullPropertyCheck( spacing, "spacing" );
        return _withOnShow( spacing, (thisComponent,v) -> {
                    thisComponent.setMajorTickSpacing(v);
                })
                ._with( thisComponent -> {
                    thisComponent.setMajorTickSpacing( spacing.orElseThrowUnchecked() );
                })
                ._this();
    }

    /**
     * Dynamically sets the minor tick spacing of the slider.
     * For more information see {@link JSlider#setMinorTickSpacing(int)}.
     * @param spacing The minor tick spacing of the slider.
     * @return This very instance, which enables builder-style method chaining.
     * @throws IllegalArgumentException if {@code spacing} is {@code null}.
     */
    public final UIForSlider<S> withMinorTickSpacing( Val<Integer> spacing ) {
        NullUtil.nullArgCheck( spacing, "spacing", Val.class );
        NullUtil.nullPropertyCheck( spacing, "spacing" );
        return _withOnShow( spacing, (thisComponent,v) -> {
                    thisComponent.setMinorTickSpacing(v);
                })
                ._with( thisComponent -> {
                    thisComponent.setMinorTickSpacing( spacing.orElseThrowUnchecked() );
                })
                ._this();
    }


    private void _doWithoutListeners( S thisComponent, Runnable someTask ) {
        // We need to first remove the change listener, otherwise we might trigger unwanted events.
        ChangeListener[] listeners = thisComponent.getChangeListeners();
        for ( ChangeListener listener : listeners )
            thisComponent.removeChangeListener( listener );

        someTask.run();

        // Now we can add the listeners back.
        for ( ChangeListener listener : listeners )
            thisComponent.addChangeListener( listener );
    }

}