/*
* Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
* Modified by Altug Uzunali (altug.uzunali@gmail.com) at 08/02/2012
*
* Added two thumbs functionality
*
* Modified by Jose Pereda (pereda@eii.uva.es) at 14/09/2012
*
* Added a TickLabelFormatter and a highlighted track
*
*/
package weeklyschedulerfx.doubleSlider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.WritableValue;
import javafx.geometry.Orientation;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.util.StringConverter;
import com.sun.javafx.Utils;
import com.sun.javafx.css.StyleableBooleanProperty;
import com.sun.javafx.css.StyleableDoubleProperty;
import com.sun.javafx.css.StyleableIntegerProperty;
import com.sun.javafx.css.StyleableObjectProperty;
import com.sun.javafx.css.StyleableProperty;
import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.EnumConverter;
import com.sun.javafx.css.converters.SizeConverter;
public class DoubleSlider extends Control {
public DoubleSlider() {
initialize();
}
private void initialize() {
// Initialize the style class to be 'double-slider'.
getStyleClass().setAll(DEFAULT_STYLE_CLASS);
}
/**
* The maximum value represented by this DoubleSlider. This must be a value
* greater than {@link #minProperty() min}.
*/
private DoubleProperty max;
public final void setMax(double value) {
maxProperty().set(value);
}
public final double getMax() {
return max == null ? 100 : max.get();
}
public final DoubleProperty maxProperty() {
if (max == null) {
max = new DoublePropertyBase(100) {
@Override
protected void invalidated() {
if (get() < getMin()) {
setMin(get());
}
adjustValues();
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "max";
}
};
}
return max;
}
/**
* The minimum value represented by this DoubleSlider. This must be a value
* less than {@link #maxProperty() max}.
*/
private DoubleProperty min;
public final void setMin(double value) {
minProperty().set(value);
}
public final double getMin() {
return min == null ? 0 : min.get();
}
public final DoubleProperty minProperty() {
if (min == null) {
min = new DoublePropertyBase(0) {
@Override
protected void invalidated() {
if (get() > getMax()) {
setMax(get());
}
adjustValues();
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "min";
}
};
}
return min;
}
/**
* Adjusts {@link #valueProperty1() value1} and {@link #valueProperty2()
* value2} to match <code>newValue</code>. The <code>value</code>is the
* actual amount between the {@link #minProperty() min} and
* {@link #maxProperty() max}. This function also takes into account
* {@link #snapToTicksProperty() snapToTicks}, which is the main difference
* between adjustValue and setValue. It also ensures that the value is some
* valid number between min and max.
*
* @expert This function is intended to be used by experts, primarily by
* those implementing new Skins or Behaviors. It is not common for
* developers or designers to access this function directly.
*/
public void adjustValue(double newValue) {
// figure out the "value" associated with the specified position
final double _min = getMin();
final double _max = getMax();
if (_max <= _min)
return;
newValue = newValue < _min ? _min : newValue;
newValue = newValue > _max ? _max : newValue;
// snap value to ticks doesn't work since the keyboard bindings have
// been removed
setValue1(snapValueToTicks(newValue));
setValue2(snapValueToTicks(newValue));
adjustValues();
}
/**
* The current value1 represented by this DoubleSlider. This value must
* always be between {@link #minProperty() min} and
* {@link #valueProperty2() value2}, inclusive. If it is ever out of bounds
* either due to {@code min} or {@code value2} changing or due to itself
* being changed, then it will be clamped to always remain valid.
*/
private DoubleProperty value1;
public final void setValue1(double value) {
if (!value1Property().isBound())
value1Property().set(value);
}
public final double getValue1() {
return value1 == null ? 0 : value1.get();
}
public final DoubleProperty value1Property() {
if (value1 == null) {
value1 = new DoublePropertyBase(0) {
@Override
protected void invalidated() {
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "value1";
}
};
}
return value1;
}
/**
* The current value2 represented by this DoubleSlider. This value must
* always be between {@link #valueProperty1() value1} and
* {@link #maxProperty() max}, inclusive. If it is ever out of bounds either
* due to {@code max} or {@code value1} changing or due to itself being
* changed, then it will be clamped to always remain valid.
*/
private DoubleProperty value2;
public final void setValue2(double value) {
if (!value2Property().isBound())
value2Property().set(value);
}
public final double getValue2() {
return value2 == null ? 0 : value2.get();
}
public final DoubleProperty value2Property() {
if (value2 == null) {
value2 = new DoublePropertyBase(0) {
@Override
protected void invalidated() {
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "value2";
}
};
}
return value2;
}
/**
* The orientation of the {@code DoubleSlider} can either be horizontal or
* vertical.
*/
private ObjectProperty<Orientation> orientation;
public final void setOrientation(Orientation value) {
orientationProperty().set(value);
}
public final Orientation getOrientation() {
return orientation == null ? Orientation.HORIZONTAL : orientation.get();
}
public final ObjectProperty<Orientation> orientationProperty() {
if (orientation == null) {
orientation = new StyleableObjectProperty<Orientation>(
Orientation.HORIZONTAL) {
@SuppressWarnings("deprecation")
@Override
protected void invalidated() {
impl_pseudoClassStateChanged(PSEUDO_CLASS_VERTICAL);
impl_pseudoClassStateChanged(PSEUDO_CLASS_HORIZONTAL);
}
@Override
public StyleableProperty<DoubleSlider, Orientation> getStyleableProperty() {
return StyleableProperties.ORIENTATION;
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "orientation";
}
};
}
return orientation;
}
/**
* Indicates that the labels for tick marks should be shown. Typically a
* {@link Skin} implementation will only show labels if
* {@link #showTickMarksProperty() showTickMarks} is also true.
*/
private BooleanProperty showTickLabels;
public final void setShowTickLabels(boolean value) {
showTickLabelsProperty().set(value);
}
public final boolean isShowTickLabels() {
return showTickLabels == null ? false : showTickLabels.get();
}
public final BooleanProperty showTickLabelsProperty() {
if (showTickLabels == null) {
showTickLabels = new StyleableBooleanProperty(false) {
@Override
public StyleableProperty<DoubleSlider, Boolean> getStyleableProperty() {
return StyleableProperties.SHOW_TICK_LABELS;
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "showTickLabels";
}
};
}
return showTickLabels;
}
/**
* Specifies whether the {@link Skin} implementation should show tick marks.
*/
private BooleanProperty showTickMarks;
public final void setShowTickMarks(boolean value) {
showTickMarksProperty().set(value);
}
public final boolean isShowTickMarks() {
return showTickMarks == null ? false : showTickMarks.get();
}
public final BooleanProperty showTickMarksProperty() {
if (showTickMarks == null) {
showTickMarks = new StyleableBooleanProperty(false) {
@Override
public StyleableProperty<DoubleSlider, Boolean> getStyleableProperty() {
return StyleableProperties.SHOW_TICK_MARKS;
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "showTickMarks";
}
};
}
return showTickMarks;
}
/**
* The unit distance between major tick marks. For example, if the
* {@link #minProperty() min} is 0 and the {@link #maxProperty() max} is 100
* and the {@link #majorTickUnitProperty() majorTickUnit} is 25, then there
* would be 5 tick marks: one at position 0, one at position 25, one at
* position 50, one at position 75, and a final one at position 100.
* <p>
* This value should be positive and should be a value less than the span.
* Out of range values are essentially the same as disabling tick marks.
*/
private DoubleProperty majorTickUnit;
public final void setMajorTickUnit(double value) {
if (value <= 0) {
throw new IllegalArgumentException(
"MajorTickUnit cannot be less than or equal to 0.");
}
majorTickUnitProperty().set(value);
}
public final double getMajorTickUnit() {
return majorTickUnit == null ? 25 : majorTickUnit.get();
}
public final DoubleProperty majorTickUnitProperty() {
if (majorTickUnit == null) {
majorTickUnit = new StyleableDoubleProperty(25) {
@Override
public void invalidated() {
if (get() <= 0) {
throw new IllegalArgumentException(
"MajorTickUnit cannot be less than or equal to 0.");
}
}
@Override
public StyleableProperty<DoubleSlider, Number> getStyleableProperty() {
return StyleableProperties.MAJOR_TICK_UNIT;
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "majorTickUnit";
}
};
}
return majorTickUnit;
}
/**
* The number of minor ticks to place between any two major ticks. This
* number should be positive or zero. Out of range values will disable
* disable minor ticks, as will a value of zero.
*/
private IntegerProperty minorTickCount;
public final void setMinorTickCount(int value) {
minorTickCountProperty().set(value);
}
public final int getMinorTickCount() {
return minorTickCount == null ? 3 : minorTickCount.get();
}
public final IntegerProperty minorTickCountProperty() {
if (minorTickCount == null) {
minorTickCount = new StyleableIntegerProperty(3) {
@Override
public StyleableProperty<DoubleSlider, Number> getStyleableProperty() {
return StyleableProperties.MINOR_TICK_COUNT;
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "minorTickCount";
}
};
}
return minorTickCount;
}
/**
* Indicates whether the {@link #valueProperty() value} of the
* {@code DoubleSlider} should always be aligned with the tick marks. This
* is honored even if the tick marks are not shown.
*
* However it is useless since the keyborad binding have been removed (Altug
* Uzunali)
*/
private BooleanProperty snapToTicks;
public final void setSnapToTicks(boolean value) {
snapToTicksProperty().set(value);
}
public final boolean isSnapToTicks() {
return snapToTicks == null ? false : snapToTicks.get();
}
public final BooleanProperty snapToTicksProperty() {
if (snapToTicks == null) {
snapToTicks = new StyleableBooleanProperty(false) {
@Override
public StyleableProperty<DoubleSlider, Boolean> getStyleableProperty() {
return StyleableProperties.SNAP_TO_TICKS;
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "snapToTicks";
}
};
}
return snapToTicks;
}
/**
* A function for formatting the label for a major tick. The number
* representing the major tick will be passed to the function. If this
* function is not specified, then a default function will be used by the
* {@link Skin} implementation.
*/
private ObjectProperty<StringConverter<Double>> labelFormatter;
public final void setLabelFormatter(StringConverter<Double> value) {
labelFormatterProperty().set(value);
}
public final StringConverter<Double> getLabelFormatter() {
return labelFormatter == null ? null : labelFormatter.get();
}
public final ObjectProperty<StringConverter<Double>> labelFormatterProperty() {
if (labelFormatter == null) {
labelFormatter = new SimpleObjectProperty<StringConverter<Double>>(
this, "labelFormatter");
}
return labelFormatter;
}
/**
* The amount by which to adjust the DoubleSlider if the track of the
* DoubleSlider is clicked. This is used when manipulating the DoubleSlider
* position using keys. If {@link #snapToTicksProperty() snapToTicks} is
* true then the nearest tick mark to the adjusted value will be used.
*/
private DoubleProperty blockIncrement;
public final void setBlockIncrement(double value) {
blockIncrementProperty().set(value);
}
public final double getBlockIncrement() {
return blockIncrement == null ? 10 : blockIncrement.get();
}
public final DoubleProperty blockIncrementProperty() {
if (blockIncrement == null) {
blockIncrement = new StyleableDoubleProperty(10) {
@Override
public StyleableProperty<DoubleSlider, Number> getStyleableProperty() {
return StyleableProperties.BLOCK_INCREMENT;
}
@Override
public Object getBean() {
return DoubleSlider.this;
}
@Override
public String getName() {
return "blockIncrement";
}
};
}
return blockIncrement;
}
/**
* Ensures that value1 is always somewhere between the min and value2, and
* that value2 is always somewhere between the value1 and max, and that if
* snapToTicks is set then the values will always be set to align with a
* tick mark.
*/
private void adjustValues() {
if ((getValue1() < getMin() || getValue1() > getValue2()) /*
* &&
* !isReadOnly
* (value)
*/)
setValue1(Utils.clamp(getMin(), getValue1(), getValue2()));
if ((getValue2() < getValue1() || getValue2() > getMax()) /*
* &&
* !isReadOnly
* (value)
*/)
setValue2(Utils.clamp(getValue1(), getValue2(), getMax()));
}
/**
* Utility function which, given the specified value, will position it
* either aligned with a tick, or simply clamp between min & max value,
* depending on whether snapToTicks is set.
*
* @expert This function is intended to be used by experts, primarily by
* those implementing new Skins or Behaviors. It is not common for
* developers or designers to access this function directly.
*/
private double snapValueToTicks(double val) {
double v = val;
if (isSnapToTicks()) {
double tickSpacing = 0;
// compute the nearest tick to this value
if (getMinorTickCount() != 0) {
tickSpacing = getMajorTickUnit()
/ (Math.max(getMinorTickCount(), 0) + 1);
} else {
tickSpacing = getMajorTickUnit();
}
int prevTick = (int) ((v - getMin()) / tickSpacing);
double prevTickValue = (prevTick) * tickSpacing + getMin();
double nextTickValue = (prevTick + 1) * tickSpacing + getMin();
v = Utils.nearest(prevTickValue, v, nextTickValue);
}
return Utils.clamp(getMin(), v, getMax());
}
/**
* When true, indicates the current value of this Slider is changing. It
* provides notification that the value is changing. Once the value is
* computed, it is reset back to false.
*/
private BooleanProperty valueChanging;
public final void setValueChanging(boolean value) {
valueChangingProperty().set(value);
}
public final boolean isValueChanging() {
return valueChanging == null ? false : valueChanging.get();
}
public final BooleanProperty valueChangingProperty() {
if (valueChanging == null) {
valueChanging = new SimpleBooleanProperty(this, "valueChanging",
false);
}
return valueChanging;
}
/***************************************************************************
* * Stylesheet Handling * *
**************************************************************************/
private static final String DEFAULT_STYLE_CLASS = "double-slider";
private static final String PSEUDO_CLASS_VERTICAL = "vertical";
private static final String PSEUDO_CLASS_HORIZONTAL = "horizontal";
@SuppressWarnings({ "rawtypes", "deprecation" })
private static class StyleableProperties {
private static final StyleableProperty<DoubleSlider, Number> BLOCK_INCREMENT = new StyleableProperty<DoubleSlider, Number>(
"-fx-block-increment", SizeConverter.getInstance(), 10.0) {
@Override
public boolean isSettable(DoubleSlider n) {
return n.blockIncrement == null || !n.blockIncrement.isBound();
}
@Override
public WritableValue<Number> getWritableValue(DoubleSlider n) {
return n.blockIncrementProperty();
}
};
private static final StyleableProperty<DoubleSlider, Boolean> SHOW_TICK_LABELS = new StyleableProperty<DoubleSlider, Boolean>(
"-fx-show-tick-labels", BooleanConverter.getInstance(),
Boolean.FALSE) {
@Override
public boolean isSettable(DoubleSlider n) {
return n.showTickLabels == null || !n.showTickLabels.isBound();
}
@Override
public WritableValue<Boolean> getWritableValue(DoubleSlider n) {
return n.showTickLabelsProperty();
}
};
private static final StyleableProperty<DoubleSlider, Boolean> SHOW_TICK_MARKS = new StyleableProperty<DoubleSlider, Boolean>(
"-fx-show-tick-marks", BooleanConverter.getInstance(),
Boolean.FALSE) {
@Override
public boolean isSettable(DoubleSlider n) {
return n.showTickMarks == null || !n.showTickMarks.isBound();
}
@Override
public WritableValue<Boolean> getWritableValue(DoubleSlider n) {
return n.showTickMarksProperty();
}
};
private static final StyleableProperty<DoubleSlider, Boolean> SNAP_TO_TICKS = new StyleableProperty<DoubleSlider, Boolean>(
"-fx-snap-to-ticks", BooleanConverter.getInstance(),
Boolean.FALSE) {
@Override
public boolean isSettable(DoubleSlider n) {
return n.snapToTicks == null || !n.snapToTicks.isBound();
}
@Override
public WritableValue<Boolean> getWritableValue(DoubleSlider n) {
return n.snapToTicksProperty();
}
};
private static final StyleableProperty<DoubleSlider, Number> MAJOR_TICK_UNIT = new StyleableProperty<DoubleSlider, Number>(
"-fx-major-tick-unit", SizeConverter.getInstance(), 25.0) {
@Override
public boolean isSettable(DoubleSlider n) {
return n.majorTickUnit == null || !n.majorTickUnit.isBound();
}
@Override
public WritableValue<Number> getWritableValue(DoubleSlider n) {
return n.majorTickUnitProperty();
}
};
private static final StyleableProperty<DoubleSlider, Number> MINOR_TICK_COUNT = new StyleableProperty<DoubleSlider, Number>(
"-fx-minor-tick-count", SizeConverter.getInstance(), 3.0) {
@Override
public boolean isSettable(DoubleSlider n) {
return n.minorTickCount == null || !n.minorTickCount.isBound();
}
@Override
public WritableValue<Number> getWritableValue(DoubleSlider n) {
return n.minorTickCountProperty();
}
};
private static final StyleableProperty<DoubleSlider, Orientation> ORIENTATION = new StyleableProperty<DoubleSlider, Orientation>(
"-fx-orientation", new EnumConverter<Orientation>(
Orientation.class), Orientation.HORIZONTAL) {
@Override
public boolean isSettable(DoubleSlider n) {
return n.orientation == null || !n.orientation.isBound();
}
@Override
public WritableValue<Orientation> getWritableValue(DoubleSlider n) {
return n.orientationProperty();
}
};
@SuppressWarnings({ "unused" })
private static final List<StyleableProperty> STYLEABLES;
static {
final List<StyleableProperty> styleables = new ArrayList<StyleableProperty>(
Control.impl_CSS_STYLEABLES());
Collections.addAll(styleables, BLOCK_INCREMENT, SHOW_TICK_LABELS,
SHOW_TICK_MARKS, SNAP_TO_TICKS, MAJOR_TICK_UNIT,
MINOR_TICK_COUNT, ORIENTATION);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
}