All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.gsi.chart.plugins.EditAxis Maven / Gradle / Ivy

package de.gsi.chart.plugins;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import javafx.util.converter.NumberStringConverter;

import org.controlsfx.control.PopOver;
import org.kordamp.ikonli.javafx.FontIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.gsi.chart.Chart;
import de.gsi.chart.axes.Axis;
import de.gsi.chart.axes.AxisMode;
import de.gsi.chart.axes.spi.AbstractAxis;
import de.gsi.chart.axes.spi.DefaultNumericAxis;

/**
 * Allows editing of the chart axes (auto range, minimum/maximum range, etc.)
 * 

* * @author rstein */ public class EditAxis extends ChartPlugin { private static final Logger LOGGER = LoggerFactory.getLogger(EditAxis.class); public static final String STYLE_CLASS_AXIS_EDITOR = "chart-axis-editor"; protected static final int DEFAULT_SHUTDOWN_PERIOD = 5000; // [ms] protected static final int DEFAULT_UPDATE_PERIOD = 100; // [ms] protected static final int DEFAULT_PREFERRED_WIDTH = 700; // [pixel] protected static final int DEFAULT_PREFERRED_HEIGHT = 200; // [pixel] // private static final String NUMBER_REGEX = // "[\\x00-\\x20]*[+-]?(((((\\p{Digit}+)(\\.)?((\\p{Digit}+)?)([eE][+-]?(\\p{Digit}+))?)|(\\.((\\p{Digit}+))([eE][+-]?(\\p{Digit}+))?)|(((0[xX](\\p{XDigit}+)(\\.)?)|(0[xX](\\p{XDigit}+)?(\\.)(\\p{XDigit}+)))[pP][+-]?(\\p{Digit}+)))[fFdD]?))[\\x00-\\x20]*"; private static final Duration DEFAULT_ANIMATION_DURATION = Duration.millis(500); private final BooleanProperty animated = new SimpleBooleanProperty(this, "animated", false); protected final List popUpList = new ArrayList<>(); private final ObjectProperty fadeDuration = new SimpleObjectProperty<>(this, "fadeDuration", EditAxis.DEFAULT_ANIMATION_DURATION) { @Override protected void invalidated() { Objects.requireNonNull(get(), "The " + getName() + " must not be null"); } }; private final ObjectProperty axisMode = new SimpleObjectProperty<>(this, "axisMode", AxisMode.XY) { @Override protected void invalidated() { Objects.requireNonNull(get(), "The " + getName() + " must not be null"); } }; /** * Creates a new instance of EditAxis with animation disabled and with {@link #axisModeProperty() editMode} * initialized to {@link AxisMode#XY}. */ public EditAxis() { this(AxisMode.XY); } /** * Creates a new instance of EditAxis with animation disabled * * @param editMode initial value of {@link #axisModeProperty() editMode} property */ public EditAxis(final AxisMode editMode) { this(editMode, false); } /** * Creates a new instance of EditAxis. * * @param editMode initial value of {@link #axisModeProperty() axisMode} property * @param animated initial value of {@link #animatedProperty() animated} property */ public EditAxis(final AxisMode editMode, final boolean animated) { super(); setAxisMode(editMode); setAnimated(animated); chartProperty().addListener((obs, oldChart, newChart) -> { removeMouseEventHandlers(oldChart); addMouseEventHandlers(newChart); }); } /** * Creates a new instance of EditAxis with {@link #axisModeProperty() editMode} initialized to {@link AxisMode#XY}. * * @param animated initial value of {@link #animatedProperty() animated} property */ public EditAxis(final boolean animated) { this(AxisMode.XY, animated); } protected void addMouseEventHandlers(final Chart newChart) { if (newChart == null) { return; } newChart.getAxes().forEach(axis -> popUpList.add(new MyPopOver(axis, axis.getSide().isHorizontal()))); newChart.getAxes().addListener(this::axesChangedHandler); } private void axesChangedHandler(@SuppressWarnings("unused") Change ch) { // parameter for EventHandler api removeMouseEventHandlers(null); addMouseEventHandlers(getChart()); } /** * When {@code true} zooming will be animated. By default it's {@code false}. * * @return the animated property * @see #zoomDurationProperty() */ public final BooleanProperty animatedProperty() { return animated; } /** * The mode defining axis along which the zoom can be performed. By default initialized to {@link AxisMode#XY}. * * @return the axis mode property */ public final ObjectProperty axisModeProperty() { return axisMode; } /** * Returns the value of the {@link #axisModeProperty()}. * * @return current mode */ public final AxisMode getAxisMode() { return axisModeProperty().get(); } /** * Returns the value of the {@link #zoomDurationProperty()}. * * @return the current zoom duration */ public final Duration getZoomDuration() { return zoomDurationProperty().get(); } /** * Returns the value of the {@link #animatedProperty()}. * * @return {@code true} if zoom is animated, {@code false} otherwise * @see #getZoomDuration() */ public final boolean isAnimated() { return animatedProperty().get(); } protected void removeMouseEventHandlers(final Chart oldChart) { popUpList.forEach(MyPopOver::deregisterMouseEvents); popUpList.clear(); if (oldChart == null) { return; } oldChart.getAxes().removeListener(this::axesChangedHandler); } /** * Sets the value of the {@link #animatedProperty()}. * * @param value if {@code true} zoom will be animated * @see #setZoomDuration(Duration) */ public final void setAnimated(final boolean value) { animatedProperty().set(value); } /** * Sets the value of the {@link #axisModeProperty()}. * * @param mode the mode to be used */ public final void setAxisMode(final AxisMode mode) { axisModeProperty().set(mode); } /** * Sets the value of the {@link #zoomDurationProperty()}. * * @param duration duration of the zoom */ public final void setZoomDuration(final Duration duration) { zoomDurationProperty().set(duration); } /** * Duration of the animated fade (in and out). Used only when {@link #animatedProperty()} is set to {@code true}. By * default initialized to 500ms. * * @return the zoom duration property */ public final ObjectProperty zoomDurationProperty() { return fadeDuration; } protected static class AxisEditor extends BorderPane { AxisEditor(final Axis axis, final boolean isHorizontal) { super(); setTop(getLabelEditor(axis, isHorizontal)); final Pane box = isHorizontal ? new HBox() : new VBox(); setCenter(box); if (isHorizontal) { box.setPrefWidth(EditAxis.DEFAULT_PREFERRED_WIDTH); } else { box.setPrefHeight(EditAxis.DEFAULT_PREFERRED_HEIGHT); } box.getChildren().add(getMinMaxButtons(axis, isHorizontal, true)); // add lower-bound text field box.getChildren().add(getBoundField(axis, isHorizontal)); box.getChildren().add(createSpacer()); box.getChildren().add(getLogCheckBoxes(axis)); box.getChildren().add(getRangeChangeButtons(axis, isHorizontal)); box.getChildren().add(getAutoRangeCheckBoxes(axis)); box.getChildren().add(createSpacer()); // add upper-bound text field box.getChildren().add(getBoundField(axis, !isHorizontal)); box.getChildren().add(getMinMaxButtons(axis, isHorizontal, false)); } protected void changeAxisRange(final Axis axis, final boolean isIncrease) { final double width = Math.abs(axis.getMax() - axis.getMin()); // TODO: check for linear and logarithmic axis changeAxisRangeLinearScale(width, axis.minProperty(), !isIncrease); changeAxisRangeLinearScale(width, axis.maxProperty(), isIncrease); } private void changeAxisRangeLimit(final Axis axis, final boolean isHorizontal, final boolean isIncrease) { final boolean isInverted = axis.isInvertedAxis(); DoubleProperty prop; if (isHorizontal) { prop = isInverted ? axis.maxProperty() : axis.minProperty(); } else { prop = isInverted ? axis.minProperty() : axis.maxProperty(); } double minTickDistance = Double.MAX_VALUE; final List tickList = new ArrayList<>(); axis.getTickMarks().forEach(tickMark -> tickList.add(tickMark.getValue())); if (!axis.isLogAxis()) { axis.getMinorTickMarks().forEach(minorTick -> tickList.add(minorTick.getPosition())); } for (final Number check1 : tickList) { for (final Number check2 : tickList) { minTickDistance = Math.min(Math.abs(check1.doubleValue() - check2.doubleValue()), minTickDistance); } } if (axis.isLogAxis()) { minTickDistance *= 0.1; } if ((minTickDistance == Double.MAX_VALUE) || (minTickDistance <= 0)) { // default fall-back in case no minor tick have been defined for // the axis minTickDistance = 0.05 * Math.abs(axis.getMax() - axis.getMin()); } if (axis.getTickUnit() > 0) { minTickDistance = axis.getTickUnit(); } // TODO: check for linear and logarithmic axis changeAxisRangeLinearScale(minTickDistance, prop, isIncrease); if (axis instanceof AbstractAxis) { // ((AbstractAxis) axis).recomputeTickMarks(); axis.setTickUnit(((AbstractAxis) axis).computePreferredTickUnit(axis.getLength())); } } protected void changeAxisRangeLinearScale(final double minTickDistance, final DoubleProperty property, final boolean isIncrease) { final double value = property.doubleValue(); if (isIncrease) { property.set(value + minTickDistance); } else { property.set(value - minTickDistance); } } private Node createSpacer() { final Region spacer = new Region(); // Make it always grow or shrink according to the available space VBox.setVgrow(spacer, Priority.ALWAYS); HBox.setHgrow(spacer, Priority.ALWAYS); return spacer; } private Pane getAutoRangeCheckBoxes(final Axis axis) { final Pane boxMax = new VBox(); VBox.setVgrow(boxMax, Priority.ALWAYS); final CheckBox autoRanging = new CheckBox("auto ranging"); HBox.setHgrow(autoRanging, Priority.ALWAYS); VBox.setVgrow(autoRanging, Priority.ALWAYS); autoRanging.setMaxWidth(Double.MAX_VALUE); autoRanging.setSelected(axis.isAutoRanging()); autoRanging.selectedProperty().bindBidirectional(axis.autoRangingProperty()); boxMax.getChildren().add(autoRanging); final CheckBox autoGrow = new CheckBox("auto grow"); HBox.setHgrow(autoGrow, Priority.ALWAYS); VBox.setVgrow(autoGrow, Priority.ALWAYS); autoGrow.setMaxWidth(Double.MAX_VALUE); autoGrow.setSelected(axis.isAutoGrowRanging()); autoGrow.selectedProperty().bindBidirectional(axis.autoGrowRangingProperty()); boxMax.getChildren().add(autoGrow); return boxMax; } private TextField getBoundField(final Axis axis, final boolean isLowerBound) { final TextField textField = new TextField(); // ValidationSupport has a slow memory leak // final ValidationSupport support = new ValidationSupport(); // final Validator validator = (final Control control, final // String value) -> { // boolean condition = value == null ? true : // !value.matches(NUMBER_REGEX); // // // additional check in case of logarithmic axis // if (!condition && axis.isLogAxis() && Double.parseDouble(value) // <= 0) { // condition = true; // } // // change text colour depending on validity as a number // textField.setStyle(condition ? "-fx-text-inner-color: red;" : // "-fx-text-inner-color: black;"); // return ValidationResult.fromMessageIf(control, "not a number", // Severity.ERROR, condition); // }; // support.registerValidator(textField, true, validator); final Runnable lambda = () -> { final double value; final boolean isInverted = axis.isInvertedAxis(); if (isLowerBound) { value = isInverted ? axis.getMax() : axis.getMin(); } else { value = isInverted ? axis.getMin() : axis.getMax(); } textField.setText(Double.toString(value)); }; axis.invertAxisProperty().addListener((ch, o, n) -> lambda.run()); axis.minProperty().addListener((ch, o, n) -> lambda.run()); axis.maxProperty().addListener((ch, o, n) -> lambda.run()); // force the field to be numeric only textField.textProperty().addListener((observable, oldValue, newValue) -> { if ((newValue != null) && !newValue.matches("\\d*")) { final double val; try { val = Double.parseDouble(newValue); } catch (NullPointerException | NumberFormatException e) { // not a parsable number textField.setText(oldValue); return; } if (axis.isLogAxis() && (val <= 0)) { textField.setText(oldValue); return; } textField.setText(Double.toString(val)); } }); textField.setOnKeyPressed(ke -> { if (ke.getCode().equals(KeyCode.ENTER)) { final double presentValue = Double.parseDouble(textField.getText()); if (isLowerBound && !axis.isInvertedAxis()) { axis.setMin(presentValue); } else { axis.setMax(presentValue); } axis.setAutoRanging(false); if (axis instanceof AbstractAxis) { // ((AbstractAxis) axis).recomputeTickMarks(); axis.setTickUnit(((AbstractAxis) axis).computePreferredTickUnit(axis.getLength())); if (LOGGER.isDebugEnabled()) { LOGGER.debug("recompute axis tick unit to {}", ((AbstractAxis) axis).computePreferredTickUnit(axis.getLength())); } } } }); HBox.setHgrow(textField, Priority.ALWAYS); VBox.setVgrow(textField, Priority.ALWAYS); return textField; } /** * Creates the header for the Axis Editor popup, allowing to configure axis label and unit * * @param axis The axis to be edited * @return pane containing label, label editor and unit editor */ private Node getLabelEditor(final Axis axis, final boolean isHorizontal) { final GridPane header = new GridPane(); header.setAlignment(Pos.BASELINE_LEFT); final TextField axisLabelTextField = new TextField(axis.getName()); axisLabelTextField.textProperty().bindBidirectional(axis.nameProperty()); header.addRow(0, new Label(" axis label: "), axisLabelTextField); final TextField axisUnitTextField = new TextField(axis.getUnit()); axisUnitTextField.setPrefWidth(50.0); axisUnitTextField.textProperty().bindBidirectional(axis.unitProperty()); header.addRow(isHorizontal ? 0 : 1, new Label(" unit: "), axisUnitTextField); final TextField unitScaling = new TextField(); unitScaling.setPrefWidth(80.0); final CheckBox autoUnitScaling = new CheckBox(" auto"); if (axis instanceof DefaultNumericAxis) { autoUnitScaling.selectedProperty() .bindBidirectional(axis.autoUnitScalingProperty()); unitScaling.textProperty().bindBidirectional(axis.unitScalingProperty(), new NumberStringConverter(new DecimalFormat("0.0####E0"))); unitScaling.disableProperty().bind(autoUnitScaling.selectedProperty()); } else { // TODO: consider adding an interface on whether // autoUnitScaling is editable autoUnitScaling.setDisable(true); unitScaling.setDisable(true); } final HBox unitScalingBox = new HBox(unitScaling, autoUnitScaling); unitScalingBox.setAlignment(Pos.BASELINE_LEFT); header.addRow(isHorizontal ? 0 : 2, new Label(" unit scale:"), unitScalingBox); return header; } private Pane getLogCheckBoxes(final Axis axis) { final Pane boxMax = new VBox(); VBox.setVgrow(boxMax, Priority.ALWAYS); final CheckBox logAxis = new CheckBox("log axis"); HBox.setHgrow(logAxis, Priority.ALWAYS); VBox.setVgrow(logAxis, Priority.ALWAYS); logAxis.setMaxWidth(Double.MAX_VALUE); logAxis.setSelected(axis.isLogAxis()); boxMax.getChildren().add(logAxis); if (axis instanceof DefaultNumericAxis) { logAxis.selectedProperty().bindBidirectional(((DefaultNumericAxis) axis).logAxisProperty()); } else { // TODO: consider adding an interface on whether log/non-log // is editable logAxis.setDisable(true); } final CheckBox invertedAxis = new CheckBox("inverted"); HBox.setHgrow(invertedAxis, Priority.ALWAYS); VBox.setVgrow(invertedAxis, Priority.ALWAYS); invertedAxis.setMaxWidth(Double.MAX_VALUE); invertedAxis.setSelected(axis.isInvertedAxis()); boxMax.getChildren().add(invertedAxis); if (axis instanceof DefaultNumericAxis) { invertedAxis.selectedProperty().bindBidirectional(axis.invertAxisProperty()); } else { // TODO: consider adding an interface on whether // invertedAxis is editable invertedAxis.setDisable(true); } final CheckBox timeAxis = new CheckBox("time axis"); HBox.setHgrow(timeAxis, Priority.ALWAYS); VBox.setVgrow(timeAxis, Priority.ALWAYS); timeAxis.setMaxWidth(Double.MAX_VALUE); timeAxis.setSelected(axis.isTimeAxis()); boxMax.getChildren().add(timeAxis); if (axis instanceof DefaultNumericAxis) { timeAxis.selectedProperty().bindBidirectional(axis.timeAxisProperty()); } else { // TODO: consider adding an interface on whether // timeAxis is editable timeAxis.setDisable(true); } return boxMax; } private Pane getMinMaxButtons(final Axis axis, final boolean isHorizontal, final boolean isMin) { final Button incMaxButton = new Button("", new FontIcon("fa-chevron-up")); incMaxButton.setMaxWidth(Double.MAX_VALUE); VBox.setVgrow(incMaxButton, Priority.ALWAYS); HBox.setHgrow(incMaxButton, Priority.ALWAYS); incMaxButton.setOnAction(evt -> { axis.setAutoRanging(false); changeAxisRangeLimit(axis, isHorizontal == isMin, true); }); final Button decMaxButton = new Button("", new FontIcon("fa-chevron-down")); decMaxButton.setMaxWidth(Double.MAX_VALUE); VBox.setVgrow(decMaxButton, Priority.ALWAYS); HBox.setHgrow(decMaxButton, Priority.ALWAYS); decMaxButton.setOnAction(evt -> { axis.setAutoRanging(false); changeAxisRangeLimit(axis, isHorizontal == isMin, false); }); final Pane box = isHorizontal ? new VBox() : new HBox(); box.getChildren().addAll(incMaxButton, decMaxButton); return box; } private Pane getRangeChangeButtons(final Axis axis, final boolean isHorizontal) { final Button incMaxButton = new Button("", new FontIcon("fa-expand")); incMaxButton.setMaxWidth(Double.MAX_VALUE); VBox.setVgrow(incMaxButton, Priority.NEVER); HBox.setHgrow(incMaxButton, Priority.NEVER); incMaxButton.setOnAction(evt -> { axis.setAutoRanging(false); changeAxisRange(axis, true); }); final Button decMaxButton = new Button("", new FontIcon("fa-compress")); decMaxButton.setMaxWidth(Double.MAX_VALUE); VBox.setVgrow(decMaxButton, Priority.NEVER); HBox.setHgrow(decMaxButton, Priority.NEVER); decMaxButton.setOnAction(evt -> { axis.setAutoRanging(false); changeAxisRange(axis, false); }); final Pane boxMax = isHorizontal ? new VBox() : new HBox(); boxMax.getChildren().addAll(incMaxButton, decMaxButton); return boxMax; } } private class MyPopOver extends PopOver { private long popOverShowStartTime; private boolean isMouseInPopOver; private Axis axis; private final ChangeListener fadeDurationListener = (ch, o, n) -> { super.fadeInDurationProperty().set(n.multiply(2.0)); super.fadeOutDurationProperty().set(n); }; private final EventHandler axisClickEventHandler = evt -> { if (evt.getButton() == MouseButton.SECONDARY) { final double x = evt.getScreenX(); final double y = evt.getScreenY(); if (axis != null) { show((Node) axis, x, y); } } }; MyPopOver(final Axis axis, final boolean isHorizontal) { super(new AxisEditor(axis, isHorizontal)); this.axis = axis; popOverShowStartTime = 0; getStyleClass().add(STYLE_CLASS_AXIS_EDITOR); super.setAutoHide(true); super.animatedProperty().bind(EditAxis.this.animatedProperty()); EditAxis.this.zoomDurationProperty().addListener(fadeDurationListener); super.fadeInDurationProperty().set(EditAxis.this.getZoomDuration().multiply(2.0)); super.fadeOutDurationProperty().set(EditAxis.this.getZoomDuration()); setFadeInDuration(Duration.millis(1000)); setFadeOutDuration(Duration.millis(500)); switch (axis.getSide()) { case TOP: setArrowLocation(ArrowLocation.TOP_CENTER); break; case LEFT: setArrowLocation(ArrowLocation.LEFT_CENTER); break; case RIGHT: setArrowLocation(ArrowLocation.RIGHT_CENTER); break; case BOTTOM: default: setArrowLocation(ArrowLocation.BOTTOM_CENTER); break; } setOpacity(0.0); getRoot().setBackground(Background.EMPTY); // getRoot().setStyle("-fx-background-color: rgba(0, 255, 0, 1);"); getScene().getStylesheets().add("plugin/editaxis.css"); getStyleClass().add("axis-editor-view-pane"); final Timeline checkMouseInsidePopUp = new Timeline( new KeyFrame(Duration.millis(EditAxis.DEFAULT_UPDATE_PERIOD), event -> { if (!isShowing()) { return; } final long now = System.currentTimeMillis(); if (isMouseInPopOver) { popOverShowStartTime = System.currentTimeMillis(); } if (Math.abs(now - popOverShowStartTime) > EditAxis.DEFAULT_SHUTDOWN_PERIOD) { hide(); } })); checkMouseInsidePopUp.play(); registerMouseEvents(); } public void deregisterMouseEvents() { ((Node) axis).removeEventHandler(MouseEvent.MOUSE_CLICKED, axisClickEventHandler); EditAxis.this.zoomDurationProperty().removeListener(fadeDurationListener); super.animatedProperty().unbind(); } public final void registerMouseEvents() { setOnShowing(evt -> popOverShowStartTime = System.currentTimeMillis()); getContentNode().setOnMouseEntered(mevt -> isMouseInPopOver = true); getContentNode().setOnMouseExited(mevt -> isMouseInPopOver = false); ((Node) axis).setOnMouseClicked(axisClickEventHandler); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy