UIForSlider.java
package swingtree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.Action;
import sprouts.Val;
import sprouts.Var;
import swingtree.api.model.SliderTicks;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import java.util.Objects;
/**
* 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>
* <p>
* A slider in SwingTree works with numbers of the type {@code N}, which is
* {@link Integer} unless you created the slider through one of the factory methods
* taking properties of another number type, like
* {@link UI#slider(UI.Axis, Number, Number, Var)} with a {@code Var<Double>}.
* The minimum, the maximum, the value and the tick marks of the slider are all
* expressed in that type, even though a plain {@link JSlider} only knows whole numbers.
* <p>
* On a slider for whole numbers, a minimum or maximum set directly through
* {@link JSlider#setMinimum(int)} or {@link JSlider#setMaximum(int)} becomes the range
* of the slider, and its tick marks and labels are laid out again for that range.
* On a slider for {@link Float} or {@link Double} numbers, the whole numbers of the
* {@link JSlider} are an internal detail, so such a call is undone right away.
*
* @param <S> The type of {@link JSlider} that this {@link UIForSlider} is configuring.
* @param <N> The type of the numbers the slider works with.
*/
public final class UIForSlider<S extends JSlider, N extends Number> extends UIForAnySwing<UIForSlider<S, N>, S>
{
private static final Logger log = LoggerFactory.getLogger(UIForSlider.class);
private final BuilderState<S> _state;
private final Class<N> _numberType;
UIForSlider( BuilderState<S> state, Class<N> numberType ) {
Objects.requireNonNull(state);
Objects.requireNonNull(numberType);
_state = state;
_numberType = numberType;
}
@Override
protected BuilderState<S> _state() {
return _state;
}
@Override
protected UIForSlider<S, N> _newBuilderWithState(BuilderState<S> newState ) {
return new UIForSlider<>(newState, _numberType);
}
private SliderState _sliderStateOf( S thisComponent ) {
return SliderState.of(thisComponent, _numberType, _state().eventProcessor());
}
/**
* Sets the orientation of the slider.
* @param axis The orientation of the slider.
* @return This builder node.
*/
public final UIForSlider<S, N> withOrientation( UI.Axis axis ) {
NullUtil.nullArgCheck( axis, "axis", UI.Axis.class );
return _with( thisComponent -> {
_setOrientation( thisComponent, axis );
})
._this();
}
private void _setOrientation( S thisComponent, UI.Axis axis ) {
thisComponent.setOrientation(axis.forSlider());
}
/**
* Dynamically sets the orientation of the slider.
* @param axis The orientation of the slider.
* @return This builder node.
*/
public final UIForSlider<S, N> withOrientation( Val<UI.Axis> axis ) {
NullUtil.nullArgCheck( axis, "axis", Val.class );
NullUtil.nullPropertyCheck( axis, "axis", "Null is not a valid axis" );
return _withOnShow( axis, (thisComponent,v) -> {
_setOrientation(thisComponent, v);
})
._with( thisComponent -> {
_setOrientation(thisComponent, axis.orElseThrowUnchecked());
})
._this();
}
/**
* Adds an {@link Action} which is called when the user changes the state of the slider,
* for example by moving its knob, or by pressing or releasing the mouse button on it.
* Changes which your application makes through the properties or values given to this
* builder do not call the action.
* <p>
* Every change calls each action once, in the order in which the actions were added.
* When the knob snaps to a tick mark (see {@link SliderTicks#withSnapToTicks(boolean)}),
* the actions are called after the knob has snapped, so they read the number at that tick mark.
*
* @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, N> onChange( Action<ComponentDelegate<JSlider, ChangeEvent>> action ) {
NullUtil.nullArgCheck( action, "action", Action.class );
return _with( thisComponent -> {
_sliderStateOf(thisComponent).onChange(
e -> _runInApp(()->{
try {
action.accept(new ComponentDelegate<>(thisComponent, e));
} catch (Exception ex) {
log.error(SwingTree.get().logMarker(), "Error while executing action on slider change!", ex);
}
})
);
})
._this();
}
/**
* Sets the minimum value of the slider, which is the number at the start of its track.
*
* @param min The minimum 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, N> withMin( N min ) {
NullUtil.nullArgCheck( min, "min", Number.class );
return _with( thisComponent -> {
_sliderStateOf(thisComponent).setMin(min);
})
._this();
}
/**
* Binds the supplied {@link Val} property to the minimum value of the slider,
* so that when the value of the property changes, the minimum of the slider,
* and with it the position of the knob and of the tick marks, is updated accordingly.
* When the user moves the knob, the value written back by the slider is never
* smaller than the item of this property.
*
* @param min The property holding the minimum value of the slider.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code min} is {@code null} or allows {@code null} items.
*/
public final UIForSlider<S, N> withMin( Val<N> min ) {
NullUtil.nullArgCheck( min, "min", Val.class );
NullUtil.nullPropertyCheck( min, "min", "The minimum value of a slider must not be null!" );
return _withOnShow( min, (thisComponent,v) -> {
_sliderStateOf(thisComponent).setMin(v);
})
._with( thisComponent -> {
SliderState state = _sliderStateOf(thisComponent);
state.keepMinWithin(min);
state.setMin(min.orElseThrowUnchecked());
})
._this();
}
/**
* Sets the maximum value of the slider, which is the number at the end of its track.
*
* @param max The maximum 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, N> withMax( N max ) {
NullUtil.nullArgCheck( max, "max", Number.class );
return _with( thisComponent -> {
_sliderStateOf(thisComponent).setMax(max);
})
._this();
}
/**
* Binds the supplied {@link Val} property to the maximum value of the slider,
* so that when the value of the property changes, the maximum of the slider,
* and with it the position of the knob and of the tick marks, is updated accordingly.
* When the user moves the knob, the value written back by the slider is never
* larger than the item of this property.
*
* @param max The property holding the maximum value of the slider.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code max} is {@code null} or allows {@code null} items.
*/
public final UIForSlider<S, N> withMax( Val<N> max ) {
NullUtil.nullArgCheck( max, "max", Val.class );
NullUtil.nullPropertyCheck( max, "max", "The maximum value of a slider must not be null!" );
return _withOnShow( max, (thisComponent,v) -> {
_sliderStateOf(thisComponent).setMax(v);
})
._with( thisComponent -> {
SliderState state = _sliderStateOf(thisComponent);
state.keepMaxWithin(max);
state.setMax(max.orElseThrowUnchecked());
})
._this();
}
/**
* Sets the current value of the slider, which places the knob at that number.
* A number outside the range of the slider places the knob at the nearest end of the track.
*
* @param value The current 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, N> withValue( N value ) {
NullUtil.nullArgCheck( value, "value", Number.class );
return _with( thisComponent -> {
_sliderStateOf(thisComponent).setValue(value);
})
._this();
}
/**
* 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 moves
* the knob, the property will not be updated.
* Use {@link #withValue(Var)} if you want to bind a property bidirectionally.
* <p>
* While the user holds the knob with the mouse, changes of the property do not move it.
*
* @param value A 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} or allows {@code null} items.
*/
public final UIForSlider<S, N> withValue( Val<N> value ) {
NullUtil.nullArgCheck( value, "value", Val.class );
NullUtil.nullPropertyCheck( value, "value", "The value of a slider must not be null!" );
return _withOnShow( value, (thisComponent,v) -> {
_sliderStateOf(thisComponent).setValue(v);
})
._with( thisComponent -> {
_sliderStateOf(thisComponent).setValue(value.orElseThrowUnchecked());
})
._this();
}
/**
* Use this to bind the supplied {@link Var} property to the value of the slider.
* When the user moves the knob, the {@link Var} is updated,
* and when the item of the {@link Var} is changed as part of the application logic,
* the knob moves accordingly.
* <p>
* The number written into the property is of the property's own type, and it is the number
* at the position of the knob, or, if the knob snaps to tick marks
* (see {@link SliderTicks#withSnapToTicks(boolean)}), the number at the tick mark it snaps to.
* A click on the knob which does not move it writes nothing new, so the property keeps
* exactly the number your application set.
* <p>
* While the user holds the knob with the mouse, changes of the property do not move it,
* and when the user lets go, the number under the knob is written into the property.
*
* @param value A property holding the value of the slider.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code value} is {@code null} or allows {@code null} items.
*/
public final UIForSlider<S, N> withValue( Var<N> value ) {
NullUtil.nullArgCheck( value, "value", Var.class );
NullUtil.nullPropertyCheck( value, "value", "The value of a slider must not be null!" );
return _withOnShow( value, (thisComponent,v) -> {
_sliderStateOf(thisComponent).setValue(v);
})
._with( thisComponent -> {
SliderState state = _sliderStateOf(thisComponent);
state.setValue(value.orElseThrowUnchecked());
state.writeUserChangesTo(value);
})
._this();
}
/**
* Sets how far apart the major tick marks of the slider are, in the numbers of the slider.
* The major tick marks count from the minimum of the slider, so a spacing of 25 on a slider
* running from 0 to 100 puts a major tick mark at 0, 25, 50, 75 and 100.
* A spacing of zero or less removes the major tick marks.
* <p>
* Like {@link JSlider#setMajorTickSpacing(int)}, this method only sets where the major tick
* marks are. The slider draws them once {@link JSlider#setPaintTicks(boolean)} was called with
* {@code true}, for example through {@code peek( s -> s.setPaintTicks(true) )}.
* Unlike {@link JSlider#setMajorTickSpacing(int)}, the spacing is a number of the slider's own type,
* so a slider for a {@code Double} property can have a major tick mark every {@code 0.25}.
* <p>
* To describe tick marks, labels and snapping together, use {@link #withTicks(SliderTicks)} instead.
* A slider which has been given a {@link SliderTicks} value takes its tick marks from that value
* and ignores the spacing given to this method, no matter in which order the two methods are called.
*
* @param spacing The distance between two neighbouring major tick marks, in the numbers 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, N> withMajorTickSpacing( N spacing ) {
NullUtil.nullArgCheck( spacing, "spacing", Number.class );
return _with( thisComponent -> {
_sliderStateOf(thisComponent).setMajorTickSpacing(spacing);
})
._this();
}
/**
* Binds the supplied {@link Val} property to the spacing of the major tick marks of the slider,
* so that the major tick marks move whenever the item of the property changes.
* The spacing is a number of the slider's own type, and the major tick marks count from the
* minimum of the slider, so a spacing of 25 on a slider running from 0 to 100 puts a major
* tick mark at 0, 25, 50, 75 and 100. A spacing of zero or less removes the major tick marks.
* <p>
* Like {@link JSlider#setMajorTickSpacing(int)}, the property only decides where the major tick
* marks are. The slider draws them once {@link JSlider#setPaintTicks(boolean)} was called with {@code true}.
* A slider which has been given a {@link SliderTicks} value through {@link #withTicks(SliderTicks)}
* or {@link #withTicks(Val)} takes its tick marks from that value and ignores this property.
*
* @param spacing A property holding the distance between two neighbouring major tick marks, in the numbers of the slider.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code spacing} is {@code null} or allows {@code null} items.
*/
public final UIForSlider<S, N> withMajorTickSpacing( Val<N> spacing ) {
NullUtil.nullArgCheck( spacing, "spacing", Val.class );
NullUtil.nullPropertyCheck( spacing, "spacing", "The major tick spacing of a slider must not be null!" );
return _withOnShow( spacing, (thisComponent,v) -> {
_sliderStateOf(thisComponent).setMajorTickSpacing(v);
})
._with( thisComponent -> {
_sliderStateOf(thisComponent).setMajorTickSpacing(spacing.orElseThrowUnchecked());
})
._this();
}
/**
* Sets how far apart the minor tick marks of the slider are, in the numbers of the slider.
* The minor tick marks count from the minimum of the slider, so a spacing of 5 on a slider
* running from 0 to 100 puts a minor tick mark at 0, 5, 10 and so on up to 100.
* A spacing of zero or less removes the minor tick marks.
* <p>
* Like {@link JSlider#setMinorTickSpacing(int)}, this method only sets where the minor tick
* marks are. The slider draws them once {@link JSlider#setPaintTicks(boolean)} was called with
* {@code true}, for example through {@code peek( s -> s.setPaintTicks(true) )}.
* Unlike {@link JSlider#setMinorTickSpacing(int)}, the spacing is a number of the slider's own type,
* so a slider for a {@code Double} property can have a minor tick mark every {@code 0.05}.
* <p>
* The minor tick marks do not depend on the major tick marks. With a major spacing of 25 and
* a minor spacing of 10, the minor tick marks sit at 0, 10, 20, 30 and 40, while the major tick
* marks sit at 0, 25 and 50, so the minor tick marks do not divide the gaps between the major
* ones evenly. {@link SliderTicks#withMinorTicksBetween(int)} counts the minor tick marks
* between two major tick marks instead, which always divides the gaps evenly.
* A slider which has been given a {@link SliderTicks} value through {@link #withTicks(SliderTicks)}
* takes its tick marks from that value and ignores the spacing given to this method,
* no matter in which order the two methods are called.
*
* @param spacing The distance between two neighbouring minor tick marks, in the numbers 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, N> withMinorTickSpacing( N spacing ) {
NullUtil.nullArgCheck( spacing, "spacing", Number.class );
return _with( thisComponent -> {
_sliderStateOf(thisComponent).setMinorTickSpacing(spacing);
})
._this();
}
/**
* Binds the supplied {@link Val} property to the spacing of the minor tick marks of the slider,
* so that the minor tick marks move whenever the item of the property changes.
* The spacing is a number of the slider's own type, and the minor tick marks count from the
* minimum of the slider, so a spacing of 5 on a slider running from 0 to 100 puts a minor
* tick mark at 0, 5, 10 and so on up to 100. A spacing of zero or less removes the minor tick marks.
* <p>
* Like {@link JSlider#setMinorTickSpacing(int)}, the property only decides where the minor tick
* marks are. The slider draws them once {@link JSlider#setPaintTicks(boolean)} was called with {@code true}.
* A slider which has been given a {@link SliderTicks} value through {@link #withTicks(SliderTicks)}
* or {@link #withTicks(Val)} takes its tick marks from that value and ignores this property.
*
* @param spacing A property holding the distance between two neighbouring minor tick marks, in the numbers of the slider.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code spacing} is {@code null} or allows {@code null} items.
*/
public final UIForSlider<S, N> withMinorTickSpacing( Val<N> spacing ) {
NullUtil.nullArgCheck( spacing, "spacing", Val.class );
NullUtil.nullPropertyCheck( spacing, "spacing", "The minor tick spacing of a slider must not be null!" );
return _withOnShow( spacing, (thisComponent,v) -> {
_sliderStateOf(thisComponent).setMinorTickSpacing(v);
})
._with( thisComponent -> {
_sliderStateOf(thisComponent).setMinorTickSpacing(spacing.orElseThrowUnchecked());
})
._this();
}
/**
* Configures the tick marks and labels along the slider through the supplied
* {@link SliderTicks} value, which describes where the major and minor tick marks are,
* whether they are drawn, whether the knob snaps to them and which labels are shown:
* <pre>{@code
* UI.slider(UI.Axis.HORIZONTAL, 0, 100, volume)
* .withTicks(
* SliderTicks.of(Integer.class)
* .withMajorSpacing(25)
* .withMinorTicksBetween(4)
* .withLabelsAtMajorTicks( v -> v + "%" )
* )
* }</pre>
* The spacings and label positions are numbers of the slider's own type, so a slider
* for a {@code Double} property is configured with a {@code SliderTicks<Double>}.
* Whenever the minimum or maximum of the slider changes, the tick marks and labels
* are laid out again for the new range.
*
* @param ticks The tick marks and labels of the slider.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code ticks} is {@code null}.
*/
public final UIForSlider<S, N> withTicks( SliderTicks<N> ticks ) {
NullUtil.nullArgCheck( ticks, "ticks", SliderTicks.class );
return _with( thisComponent -> {
_sliderStateOf(thisComponent).setTicks(ticks);
})
._this();
}
/**
* Binds the tick marks and labels along the slider to the supplied property holding a
* {@link SliderTicks} value, so that the slider shows the tick marks and labels of the
* property's current item, and follows every change of it.
* <p>
* The property may live in your view model, or be derived from view model state right
* in the view, like this slider which shows its labels only while a flag says so:
* <pre>{@code
* SliderTicks<Integer> plain = SliderTicks.of(Integer.class).withMajorSpacing(25);
* SliderTicks<Integer> labelled = plain.withLabelsAtMajorTicks();
*
* UI.slider(UI.Axis.HORIZONTAL, 0, 100, volume)
* .withTicks( showLabels.viewAs(SliderTicks.classTyped(Integer.class), show -> show ? labelled : plain) )
* }</pre>
* Under the {@link swingtree.threading.EventProcessor#DECOUPLED} event processor the
* new {@link SliderTicks} reaches the slider as part of the property's change event,
* in the same order as every other change of the slider's properties.
*
* @param ticks A property holding the tick marks and labels of the slider.
* @return This very instance, which enables builder-style method chaining.
* @throws IllegalArgumentException if {@code ticks} is {@code null} or allows {@code null} items.
*/
public final UIForSlider<S, N> withTicks( Val<SliderTicks<N>> ticks ) {
NullUtil.nullArgCheck( ticks, "ticks", Val.class );
NullUtil.nullPropertyCheck( ticks, "ticks", "Use 'SliderTicks.of(..)' instead of null to describe a slider without tick marks!" );
return _withOnShow( ticks, (thisComponent,v) -> {
_sliderStateOf(thisComponent).setTicks(v);
})
._with( thisComponent -> {
_sliderStateOf(thisComponent).setTicks(ticks.orElseThrowUnchecked());
})
._this();
}
}