/* Copyright (c) 2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky 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 for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.ui; import javafx.beans.binding.Bindings; import javafx.beans.property.DoubleProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.util.converter.NumberStringConverter; import java.util.function.Consumer; /** * A UI control combining a label, slider, and text field for adjusting one property. */ public abstract class Adjuster<T extends Number> extends HBox { private StringProperty name = new SimpleStringProperty("Name"); private final Label nameLbl = new Label(); private final Slider valueSlider = new Slider(); private final TextField valueField = new TextField(); private final Property<Number> value; protected boolean clampMax; protected boolean clampMin; private double sliderMin = 0.01; // Lower limit for logarithmic calculations. private double min = 0; // TODO: handle minimum. private double max = 100; private boolean logarithmic = false; private boolean maxInfinity = false; private ChangeListener<Number> listener; protected Adjuster(Property<Number> value) { this.value = value; nameLbl.textProperty().bind(Bindings.concat(name, ":")); setAlignment(Pos.CENTER_LEFT); setSpacing(10); getChildren().addAll(nameLbl, valueSlider, valueField); valueField.setPrefWidth(103); valueField.textProperty().bindBidirectional(value, new NumberStringConverter()); valueSlider.valueProperty().bindBidirectional(value); valueSlider.setMin(0); valueSlider.setMax(100); } public void setName(String name) { this.name.set(name); } public String getName() { return name.get(); } public StringProperty nameProperty() { return name; } public void setRange(double min, double max) { if (min < 0.01 && min >= 0) { sliderMin = 0.01; } else { sliderMin = min; } this.min = min; this.max = max; if (!logarithmic) { valueSlider.setMin(min); valueSlider.setMax(max); } } /** * Set value without triggering listeners to update. */ public void set(Number newValue) { if (listener != null) { value.removeListener(listener); value.setValue(newValue); value.addListener(listener); } else { value.setValue(newValue); } } /** * Sets the value and updates the listeners. */ public void setAndUpdate(Number newValue) { value.setValue(newValue); } public T get() { return (T) value.getValue(); } public void setTooltip(String tooltip) { nameLbl.setTooltip(new Tooltip(tooltip)); valueField.setTooltip(new Tooltip(tooltip)); valueSlider.setTooltip(new Tooltip(tooltip)); } /** * Make the adjuster use a logarithmic mapping for the slider position. */ public void makeLogarithmic() { logarithmic = true; valueSlider.setMin(0); valueSlider.setMax(100); DoubleProperty sliderValue = new SimpleDoubleProperty(); ChangeListener<Number> sliderListener = (observable, oldValue, newValue) -> { double result; if (maxInfinity && newValue.doubleValue() > 99.9) { result = Double.POSITIVE_INFINITY; } else { double logMin = Math.log(sliderMin); double logMax = Math.log(max); double range = logMax - logMin; result = Math.pow(Math.E, (newValue.doubleValue() / 100.0) * range + logMin); } value.setValue(result); }; ChangeListener<Number> valueListener = (observable, oldValue, newValue) -> { double result; double logMin = Math.log(sliderMin); double logMax = Math.log(max); double logValue = Math.log(newValue.doubleValue()); logValue = Math.max(logMin, logValue); logValue = Math.min(logMax, logValue); double pos = (logValue - logMin) / (logMax - logMin); result = pos * 100; // Temporarily stop listening to avoid event recursion. sliderValue.removeListener(sliderListener); sliderValue.set(result); sliderValue.addListener(sliderListener); }; sliderValue.addListener(sliderListener); value.addListener(valueListener); valueSlider.valueProperty().unbindBidirectional(value); valueSlider.valueProperty().bindBidirectional(sliderValue); } /** * When set to true the value is set to infinity when the slider is at the maximum position. */ public void setMaxInfinity(boolean maxInfinity) { this.maxInfinity = maxInfinity; } public void onValueChange(Consumer<T> changeConsumer) { listener = (observable, oldValue, newValue) -> changeConsumer.accept(clamp(newValue)); value.addListener(listener); } protected abstract T clamp(Number value); public void clampBoth() { clampMax(); clampMin(); } public void clampMax() { clampMax = true; } public void clampMin() { clampMin = true; } }