package rmblworx.tools.timey.gui.component; import java.io.IOException; import java.time.LocalTime; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.AnchorPane; import javafx.util.StringConverter; /* * Copyright 2014-2015 Christian Raue * MIT License http://opensource.org/licenses/mit-license.php */ /** * JavaFX-Komponente zur Angabe einer Zeit bestehend aus Stunden, Minuten und Sekunden. * @author Christian Raue {@literal <christian.raue@gmail.com>} */ public class TimePicker extends AnchorPane { /** * Minimalwert für Stunden, Minuten und Sekunden. */ private static final long MIN_VALUE = 0L; /** * Maximalwert für Stunden. */ private static final long MAX_HOURS = 23L; /** * Maximalwert für Minuten. */ private static final long MAX_MINUTES = 59L; /** * Maximalwert für Sekunden. */ private static final long MAX_SECONDS = 59L; /** * Zeit als Property. */ private final ObjectProperty<LocalTime> timeProperty = new SimpleObjectProperty<>(); @FXML private TextField hoursTextField; @FXML private TextField minutesTextField; @FXML private TextField secondsTextField; @FXML private Slider hoursSlider; @FXML private Slider minutesSlider; @FXML private Slider secondsSlider; public TimePicker() { // Zeit auf 0 setzen timeProperty.set(LocalTime.ofNanoOfDay(0)); createGui(); } private void createGui() { try { final FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("TimePicker.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); fxmlLoader.load(); getStylesheets().add(getClass().getResource("TimePicker.css").toExternalForm()); } catch (final IOException e) { e.printStackTrace(); } } @FXML private void initialize() { bindTextInputListenersAndSlider(hoursTextField, hoursSlider, MAX_HOURS); bindTextInputListenersAndSlider(minutesTextField, minutesSlider, MAX_MINUTES); bindTextInputListenersAndSlider(secondsTextField, secondsSlider, MAX_SECONDS); } /** * @param time Zeit */ public void setValue(final LocalTime time) { hoursTextField.setText(getTwoDigitValue(time.getHour())); minutesTextField.setText(getTwoDigitValue(time.getMinute())); secondsTextField.setText(getTwoDigitValue(time.getSecond())); } /** * @return Zeit */ public LocalTime getValue() { return LocalTime.of(Integer.parseInt(hoursTextField.getText()), Integer.parseInt(minutesTextField.getText()), Integer.parseInt(secondsTextField.getText())); } /** * @return Zeit als Property */ public ObjectProperty<LocalTime> getTimeProperty() { return timeProperty; } /** * Verbindet ein Textfeld bidirektional mit einem Slider und sorgt für Validierung der Eingaben. * @param textField Textfeld * @param slider Slider * @param maxValue Maximalwert für das Textfeld */ private void bindTextInputListenersAndSlider(final TextField textField, final Slider slider, final long maxValue) { final StringProperty textProperty = textField.textProperty(); // Eingaben auf Zahlen beschränken textField.addEventFilter(KeyEvent.KEY_TYPED, new EventHandler<KeyEvent>() { public void handle(final KeyEvent keyEvent) { if (!"0123456789".contains(keyEvent.getCharacter())) { keyEvent.consume(); } } }); textProperty.addListener(new ChangeListener<String>() { public void changed(final ObservableValue<? extends String> property, final String oldValue, final String newValue) { // Wert auf gültigen Bereich beschränken try { final long parsedNumber = Long.parseLong(newValue); if (parsedNumber < MIN_VALUE) { textProperty.setValue(getTwoDigitValue(MIN_VALUE)); } else if (parsedNumber > maxValue) { textProperty.setValue(getTwoDigitValue(maxValue)); } else if (newValue.length() > 2) { textProperty.setValue(getTwoDigitValue(parsedNumber)); } else { textProperty.setValue(newValue); } // Änderung an Zeit-Property durchstellen timeProperty.set(getValue()); } catch (final NumberFormatException e) { // alten Wert wieder setzen (sinnvoll bei Änderung per Copy & Paste) textProperty.setValue(oldValue); } } }); // bei Verlassen des Feldes sicherstellen, dass Wert zweistellig textField.focusedProperty().addListener(new ChangeListener<Boolean>() { public void changed(final ObservableValue<? extends Boolean> property, final Boolean oldValue, final Boolean newValue) { if (Boolean.FALSE.equals(newValue)) { try { textProperty.setValue(getTwoDigitValue(Long.parseLong(textProperty.get()))); } catch (final NumberFormatException e) { textProperty.setValue(getTwoDigitValue(MIN_VALUE)); } } } }); // Textfeld mit Slider koppeln if (slider != null) { slider.setMax(maxValue); textProperty.bindBidirectional(slider.valueProperty(), new StringConverter<Number>() { public String toString(final Number number) { return getTwoDigitValue(number.longValue()); } public Number fromString(final String string) { return Long.parseLong(string); } }); } // auf Scroll-Ereignis im Textfeld reagieren textField.setOnScroll(new EventHandler<ScrollEvent>() { public void handle(final ScrollEvent event) { // Scrollen über Feld ohne Fokus hat keine Auswirkung if (!textField.isFocused()) { return; } final double change = event.getDeltaY() > 0 ? 1 : -1; slider.setValue(slider.getValue() + change); event.consume(); } }); } /** * @param value Wert * @return zweistellig formatierter Wert */ private String getTwoDigitValue(final long value) { return String.format("%02d", value); } }