
eu.binjr.common.javafx.controls.TimeRangePicker Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of binjr-core Show documentation
Show all versions of binjr-core Show documentation
A Time Series Data Browser
/*
* Copyright 2017-2020 Frederic Thevenet
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.binjr.common.javafx.controls;
import eu.binjr.common.javafx.bindings.BindingManager;
import eu.binjr.common.logging.Logger;
import eu.binjr.core.dialogs.Dialogs;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.util.StringConverter;
import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.TextFields;
import java.io.IOException;
import java.net.URL;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.ResourceBundle;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class TimeRangePicker extends HBox {
private static final Logger logger = Logger.create(TimeRangePicker.class);
private final ToggleButton timeRangeLabel;
private final ButtonBase previousIntervalBtn;
private final ButtonBase nextIntervalBtn;
private final ButtonBase resetIntervalBtn;
private TimeRangePickerController timeRangePickerController;
private final PopupControl popup;
private final Property zoneId;
private final Property timeRange = new SimpleObjectProperty<>(TimeRange.of(ZonedDateTime.now(), ZonedDateTime.now()));
private final ObjectProperty selectedRange = new SimpleObjectProperty<>(TimeRange.of(ZonedDateTime.now().minusHours(1), ZonedDateTime.now()));
private final BindingManager bindingManager = new BindingManager();
private final ObjectProperty> onResetInterval = new SimpleObjectProperty<>();
public TimeRangePicker() throws IOException {
super();
this.previousIntervalBtn = new ToolButtonBuilder<>()
.setStyleClass("dialog-button")
.setHeight(USE_COMPUTED_SIZE)
.setWidth(20.0)
.setIconStyleClass("left-arrow-icon")
.setAction(event -> {
timeRangePickerController.stepBy(
Duration.between(timeRangePickerController.endDate.getDateTimeValue(),
timeRangePickerController.startDate.getDateTimeValue()));
event.consume();
})
.setTooltip("Step Back")
.build(Button::new);
previousIntervalBtn.setMaxHeight(Double.MAX_VALUE);
this.nextIntervalBtn = new ToolButtonBuilder<>()
.setHeight(USE_COMPUTED_SIZE)
.setWidth(20)
.setStyleClass("dialog-button")
.setIconStyleClass("right-arrow-icon")
.setAction(event -> {
timeRangePickerController.stepBy(
Duration.between(timeRangePickerController.startDate.getDateTimeValue(),
timeRangePickerController.endDate.getDateTimeValue()));
event.consume();
})
.setTooltip("Step Forward")
.build(Button::new);
nextIntervalBtn.setMaxHeight(Double.MAX_VALUE);
this.resetIntervalBtn = new ToolButtonBuilder<>()
.setHeight(USE_COMPUTED_SIZE)
.setWidth(40.0)
.setStyleClass("dialog-button")
.setAction(event -> {
if (getOnResetInterval() != null) {
var range = getOnResetInterval().get();
timeRangePickerController.applyNewTimeRange.accept(range.getBeginning(), range.getEnd());
}
event.consume();
})
.setIconStyleClass("reset-clock-icon", "medium-icon")
.setTooltip("Reset Time Range")
.build(Button::new);
resetIntervalBtn.setMaxHeight(Double.MAX_VALUE);
timeRangeLabel = new ToggleButton();
var tooltip = new Tooltip("Edit the time range");
tooltip.setShowDelay(javafx.util.Duration.millis(500));
timeRangeLabel.setTooltip(tooltip);
timeRangeLabel.setMaxHeight(Double.MAX_VALUE);
this.setAlignment(Pos.CENTER);
this.getChildren().addAll(
previousIntervalBtn,
timeRangeLabel,
resetIntervalBtn,
nextIntervalBtn);
timeRangeLabel.setGraphic(ToolButtonBuilder.makeIconNode(Pos.CENTER, "combo-box-arrow-icon", "small-icon"));
timeRangeLabel.setContentDisplay(ContentDisplay.RIGHT);
timeRangeLabel.setGraphicTextGap(8);
timeRangeLabel.setPadding(new Insets(0, 10, 0, 10));
FXMLLoader loader = new FXMLLoader(getClass().getResource("/eu/binjr/views/TimeRangePickerView.fxml"));
this.timeRangePickerController = new TimeRangePickerController();
loader.setController(timeRangePickerController);
Pane timeRangePickerPane = loader.load();
popup = new PopupControl();
popup.setAutoHide(true);
popup.getScene().setRoot(timeRangePickerPane);
popup.showingProperty().addListener((observable, oldValue, newValue) -> {
timeRangeLabel.setSelected(newValue);
});
this.timeRangeLabel.setOnAction(actionEvent -> {
Node owner = (Node) actionEvent.getSource();
Bounds bounds = owner.localToScreen(owner.getBoundsInLocal());
this.popup.show(owner.getScene().getWindow(), bounds.getMinX(), bounds.getMaxY());
});
timeRangePickerController.applyNewTimeRange = (beginning, end) -> {
this.selectedRange.setValue(TimeRange.of(beginning, end));
};
timeRangePickerController.startDate.setDateTimeValue(selectedRange.getValue().getBeginning());
timeRangePickerController.endDate.setDateTimeValue(selectedRange.getValue().getEnd());
zoneId = timeRangePickerController.endDate.zoneIdProperty();
bindingManager.bind(timeRangePickerController.startDate.zoneIdProperty(), zoneId);
bindingManager.bindBidirectional(timeRangePickerController.zoneIdProperty(), zoneId);
bindingManager.attachListener(zoneId, (observable, oldValue, newValue) -> updateText());
bindingManager.attachListener(selectedRange, (ChangeListener) (observable, oldValue, newValue) -> {
if (newValue != null) {
bindingManager.suspend();
try {
zoneId.setValue(newValue.getZoneId());
timeRangePickerController.startDate.setDateTimeValue(newValue.getBeginning());
timeRangePickerController.endDate.setDateTimeValue(newValue.getEnd());
} finally {
bindingManager.resume();
updateText();
}
}
});
bindingManager.attachListener(timeRangePickerController.startDate.dateTimeValueProperty(),
(ChangeListener) (observable, oldValue, newValue) -> {
if (newValue != null && timeRangePickerController.endDate.getDateTimeValue() != null) {
TimeRange newRange = TimeRange.of(newValue, timeRangePickerController.endDate.getDateTimeValue());
if (newRange.isNegative()) {
TimeRange oldRange = TimeRange.of(oldValue, timeRangePickerController.endDate.getDateTimeValue());
this.selectedRange.setValue(TimeRange.of(newValue, newValue.plus(oldRange.getDuration())));
} else {
this.selectedRange.setValue(newRange);
}
}
});
bindingManager.attachListener(timeRangePickerController.endDate.dateTimeValueProperty(),
(ChangeListener) (observable, oldValue, newValue) -> {
if (newValue != null && timeRangePickerController.startDate.getDateTimeValue() != null) {
TimeRange newRange = TimeRange.of(timeRangePickerController.startDate.getDateTimeValue(), newValue);
if (newRange.isNegative()) {
TimeRange oldRange = TimeRange.of(timeRangePickerController.startDate.getDateTimeValue(), oldValue);
this.selectedRange.setValue(TimeRange.of(newValue.minus(oldRange.getDuration()), newValue));
} else {
this.selectedRange.setValue(newRange);
}
}
});
}
public String getText() {
return timeRangeLabel.getText();
}
public StringProperty textProperty() {
return timeRangeLabel.textProperty();
}
public void setText(String text) {
timeRangeLabel.setText(text);
}
public ZoneId getZoneId() {
return zoneId.getValue();
}
public Property zoneIdProperty() {
return zoneId;
}
public void dispose() {
bindingManager.close();
}
private void updateText() {
Dialogs.runOnFXThread(() -> {
this.timeRange.setValue(TimeRange.of(timeRangePickerController.startDate.getDateTimeValue(), timeRangePickerController.endDate.getDateTimeValue()));
timeRangeLabel.setText(String.format("From %s to %s (%s)",
timeRangePickerController.startDate.getDateTimeValue().format(timeRangePickerController.startDate.getFormatter()),
timeRangePickerController.endDate.getDateTimeValue().format(timeRangePickerController.endDate.getFormatter()),
getZoneId().toString()));
});
}
public Supplier getOnResetInterval() {
return onResetInterval.get();
}
public ObjectProperty> onResetIntervalProperty() {
return onResetInterval;
}
public void setOnResetInterval(Supplier onResetInterval) {
this.onResetInterval.set(onResetInterval);
}
public TimeRange getSelectedRange() {
return selectedRange.getValue();
}
public Property selectedRangeProperty() {
return selectedRange;
}
public void initSelectedRange(TimeRange selectedRange) {
this.selectedRange.setValue(selectedRange);
}
public void updateSelectedRange(TimeRange newValue) {
bindingManager.suspend();
try {
logger.trace(() -> "updateRangeBeginning -> " + newValue.toString());
timeRangePickerController.startDate.setDateTimeValue(newValue.getBeginning());
timeRangePickerController.endDate.setDateTimeValue(newValue.getEnd());
this.selectedRange.setValue(TimeRange.of(newValue.getBeginning(), newValue.getEnd()));
zoneId.setValue(newValue.getZoneId());
updateText();
} finally {
bindingManager.resume();
}
}
public TimeRange getTimeRange() {
return timeRange.getValue();
}
public Property timeRangeProperty() {
return timeRange;
}
private void onZoneIdChanged(ObservableValue extends ZoneId> observable, ZoneId oldValue, ZoneId newValue) {
updateText();
}
public void setOnSelectedRangeChanged(ChangeListener timeRangeChangeListener) {
bindingManager.attachListener(selectedRange, timeRangeChangeListener);
}
public Boolean isTimeRangeLinked() {
return timeRangePickerController.linkTimeRangeButton.isSelected();
}
public Property timeRangeLinkedProperty() {
return timeRangePickerController.linkTimeRangeButton.selectedProperty();
}
public void setTimeRangeLinked(Boolean timeRangeLinked) {
timeRangePickerController.linkTimeRangeButton.setSelected(timeRangeLinked);
}
private class TimeRangePickerController {
@FXML
private ResourceBundle resources;
@FXML
private URL location;
@FXML
private AnchorPane root;
@FXML
private Button previousIntervalBtn;
@FXML
private ZonedDateTimePicker startDate;
@FXML
private ZonedDateTimePicker endDate;
@FXML
private Button nextIntervalBtn;
@FXML
private ComboBox timezoneField;
@FXML
private Button last6Hours;
@FXML
private Button last3Hours;
@FXML
private Button last24Hours;
@FXML
private Button last7Days;
@FXML
private Button last15Days;
@FXML
private Button last30Days;
@FXML
private Button last90Days;
@FXML
private Button last15Minutes;
@FXML
private Button last30Minutes;
@FXML
private Button last60Minutes;
@FXML
private Button last12Hours;
@FXML
private Button last90Minutes;
@FXML
private Button today;
@FXML
private Button yesterday;
@FXML
private Button thisWeek;
@FXML
private Button lastWeek;
@FXML
private Button minus1Hour;
@FXML
private Button plus1Hour;
@FXML
private Button minus24Hours;
@FXML
private Button plus24Hours;
@FXML
private Button pasteTimeRangeButton;
@FXML
private Button copyTimeRangeButton;
@FXML
private ToggleButton linkTimeRangeButton;
private TextFormatter formatter;
private AutoCompletionBinding autoCompletionBinding;
private BiConsumer applyNewTimeRange = (start, end) -> {
startDate.dateTimeValueProperty().setValue(start);
endDate.dateTimeValueProperty().setValue(end);
};
private void stepBy(Duration intervalDuration) {
applyNewTimeRange.accept(startDate.getDateTimeValue().plus(intervalDuration), endDate.getDateTimeValue().plus(intervalDuration));
}
private void last(Duration duration) {
ZonedDateTime end = ZonedDateTime.now(zoneId.getValue());
applyNewTimeRange.accept(end.minus(duration), end);
// hide popup
popup.hide();
}
private void forward(Duration duration) {
shift(duration, ZonedDateTime::plus);
}
private void backward(Duration duration) {
shift(duration, ZonedDateTime::minus);
}
private void shift(Duration duration, BiFunction move) {
var ref = TimeRange.of(selectedRange.get());
applyNewTimeRange.accept(
move.apply(ref.getBeginning(), duration),
move.apply(ref.getEnd(), duration));
// hide popup
popup.hide();
}
private void updateAutoCompletionBinding() {
if (autoCompletionBinding != null) {
autoCompletionBinding.dispose();
}
autoCompletionBinding = TextFields.bindAutoCompletion(timezoneField.getEditor(),
ZoneId.getAvailableZoneIds().stream().sorted().collect(Collectors.toList()));
autoCompletionBinding.setPrefWidth(200);
}
@FXML
void initialize() {
formatter = new TextFormatter(new StringConverter() {
@Override
public String toString(ZoneId object) {
if (object == null) {
return "null";
}
return object.toString();
}
@Override
public ZoneId fromString(String string) {
return ZoneId.of(string);
}
});
updateAutoCompletionBinding();
timezoneField.getEditor().setTextFormatter(formatter);
timezoneField.setItems(FXCollections.observableArrayList(ZoneId.getAvailableZoneIds().stream().sorted().collect(Collectors.toList())));
timezoneField.showingProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
autoCompletionBinding.dispose();
} else {
updateAutoCompletionBinding();
}
});
nextIntervalBtn.setOnAction(event -> {
stepBy(Duration.between(startDate.getDateTimeValue(), endDate.getDateTimeValue()));
});
previousIntervalBtn.setOnAction(event -> {
stepBy(Duration.between(endDate.getDateTimeValue(), startDate.getDateTimeValue()));
});
last3Hours.setOnAction(event -> last(Duration.of(3, ChronoUnit.HOURS)));
last6Hours.setOnAction(event -> last(Duration.of(6, ChronoUnit.HOURS)));
last12Hours.setOnAction(event -> last(Duration.of(12, ChronoUnit.HOURS)));
last24Hours.setOnAction(event -> last(Duration.of(24, ChronoUnit.HOURS)));
last7Days.setOnAction(event -> last(Duration.of(7, ChronoUnit.DAYS)));
last15Days.setOnAction(event -> last(Duration.of(15, ChronoUnit.DAYS)));
last30Days.setOnAction(event -> last(Duration.of(30, ChronoUnit.DAYS)));
last90Days.setOnAction(event -> last(Duration.of(90, ChronoUnit.DAYS)));
last15Minutes.setOnAction(event -> last(Duration.of(15, ChronoUnit.MINUTES)));
last30Minutes.setOnAction(event -> last(Duration.of(30, ChronoUnit.MINUTES)));
last60Minutes.setOnAction(event -> last(Duration.of(60, ChronoUnit.MINUTES)));
last90Minutes.setOnAction(event -> last(Duration.of(90, ChronoUnit.MINUTES)));
today.setOnAction(event -> {
LocalDate today = LocalDate.now(zoneId.getValue());
applyNewTimeRange.accept(
ZonedDateTime.of(today, LocalTime.MIDNIGHT, zoneId.getValue()),
ZonedDateTime.of(today.plusDays(1), LocalTime.MIDNIGHT, zoneId.getValue()));
// hide popup
popup.hide();
});
yesterday.setOnAction(event -> {
LocalDate today = LocalDate.now(zoneId.getValue());
applyNewTimeRange.accept(
ZonedDateTime.of(today.minusDays(1), LocalTime.MIDNIGHT, zoneId.getValue()),
ZonedDateTime.of(today, LocalTime.MIDNIGHT, zoneId.getValue()));
// hide popup
popup.hide();
});
thisWeek.setOnAction(event -> {
LocalDate refDay = LocalDate.now(zoneId.getValue());
int n = refDay.getDayOfWeek().getValue();
applyNewTimeRange.accept(
ZonedDateTime.of(refDay.minusDays(n - 1), LocalTime.MIDNIGHT, zoneId.getValue()),
ZonedDateTime.of(refDay.plusDays(8 - n), LocalTime.MIDNIGHT, zoneId.getValue()));
// hide popup
popup.hide();
});
lastWeek.setOnAction(event -> {
LocalDate refDay = LocalDate.now(zoneId.getValue()).minusWeeks(1);
int n = refDay.getDayOfWeek().getValue();
applyNewTimeRange.accept(
ZonedDateTime.of(refDay.minusDays(n - 1), LocalTime.MIDNIGHT, zoneId.getValue()),
ZonedDateTime.of(refDay.plusDays(8 - n), LocalTime.MIDNIGHT, zoneId.getValue()));
// hide popup
popup.hide();
});
minus1Hour.setOnAction(event -> backward(Duration.ofHours(1)));
plus1Hour.setOnAction(event -> forward(Duration.ofHours(1)));
minus24Hours.setOnAction(event -> backward(Duration.ofHours(24)));
plus24Hours.setOnAction(event -> forward(Duration.ofHours(24)));
copyTimeRangeButton.setOnAction(event -> {
try {
final ClipboardContent content = new ClipboardContent();
content.put(TimeRange.TIME_RANGE_DATA_FORMAT, timeRange.getValue().serialize());
Clipboard.getSystemClipboard().setContent(content);
} catch (Exception e) {
logger.error("Failed to copy time range to clipboard", e);
}
});
pasteTimeRangeButton.setOnAction(event -> {
try {
if (Clipboard.getSystemClipboard().hasContent(TimeRange.TIME_RANGE_DATA_FORMAT)) {
String content = (String) Clipboard.getSystemClipboard().getContent(TimeRange.TIME_RANGE_DATA_FORMAT);
TimeRange range = TimeRange.deSerialize(content);
zoneId.setValue(range.getBeginning().getZone());
selectedRange.setValue(range);
}
} catch (Exception e) {
logger.error("Failed to paste range output from clipboard", e);
}
});
}
Property zoneIdProperty() {
return formatter.valueProperty();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy