/** * Copyright (c) 2017 European Organisation for Nuclear Research (CERN), All Rights Reserved. */ package de.gsi.chart.plugins; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener.Change; import javafx.geometry.Bounds; import javafx.geometry.HPos; import javafx.geometry.Point2D; import javafx.geometry.VPos; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Label; import de.gsi.chart.Chart; import de.gsi.chart.axes.Axis; /** * Base class for plugins indicating a specific value or range of values on a {@link Chart} with an optional * {@link #textProperty() text label} description. * * @author mhrabia */ public abstract class AbstractValueIndicator extends ChartPlugin { private final Axis axis; private final ChangeListener<? super Number> axisBoundsListener = (obs, oldVal, newVal) -> layoutChildren(); private final ListChangeListener<? super ChartPlugin> pluginsListListener = (final Change<? extends ChartPlugin> change) -> updateStyleClass(); private double xOffset; private double yOffset; protected final Label label = new Label(); /* Difference between the mouse press position and the indicators center */ protected final Delta dragDelta = new Delta(); protected final BooleanProperty editableIndicator = new SimpleBooleanProperty(this, "editableIndicator", true) { @Override protected void invalidated() { layoutChildren(); } }; private final ObjectProperty<HPos> labelHorizontalAnchor = new SimpleObjectProperty<HPos>(this, "labelHorizontalAnchor", HPos.CENTER) { @Override protected void invalidated() { layoutChildren(); } }; private final ObjectProperty<VPos> labelVerticalAnchor = new SimpleObjectProperty<VPos>(this, "labelVerticalAnchor", VPos.CENTER) { @Override protected void invalidated() { layoutChildren(); } }; /** * Creates a new instance of the indicator. * * @param axis the axis this indicator is associated with * @param text the text to be shown by the label. Value of {@link #textProperty()}. */ protected AbstractValueIndicator(Axis axis, final String text) { super(); this.axis = axis; setText(text); label.mouseTransparentProperty().bind(editableIndicatorProperty().not()); label.pickOnBoundsProperty().set(true); label.setOnMousePressed(mouseEvent -> { /* * Record a delta distance for the drag and drop operation. PROBLEM: At this point, we need to know the * relative position of the label with respect to the indicator value. */ Point2D c = label.sceneToLocal(mouseEvent.getSceneX(), mouseEvent.getSceneY()); dragDelta.x = -(c.getX() + xOffset); dragDelta.y = c.getY() + yOffset; label.setCursor(Cursor.MOVE); mouseEvent.consume(); }); editableIndicatorProperty().addListener((ch, o, n) -> updateMouseListener(n)); updateMouseListener(isEditable()); chartProperty().addListener((obs, oldChart, newChart) -> { if (oldChart != null) { removeAxisListener(); removePluginsListListener(oldChart); } if (newChart != null) { addAxisListener(); addPluginsListListener(newChart); } }); textProperty().addListener((obs, oldText, newText) -> layoutChildren()); } private void addAxisListener() { final Axis valueAxis = getNumericAxis(); valueAxis.minProperty().addListener(axisBoundsListener); valueAxis.maxProperty().addListener(axisBoundsListener); } protected void addChildNodeIfNotPresent(final Node node) { if (!getChartChildren().contains(node)) { getChartChildren().add(node); } } private void addPluginsListListener(final Chart chart) { chart.getPlugins().addListener(pluginsListListener); updateStyleClass(); } /** * whether this plugin is editable * * @return boolean property */ public final BooleanProperty editableIndicatorProperty() { return editableIndicator; } private int getIndicatorInstanceIndex() { if (getChart() == null) { return 0; } final Class<?> thisClass = getClass(); int instanceIndex = -1; for (final ChartPlugin plugin : getChart().getPlugins()) { if (plugin.getClass().equals(thisClass)) { instanceIndex++; } if (plugin == this) { break; } } return instanceIndex < 0 ? 0 : instanceIndex; } /** * Returns the value of the {@link #labelHorizontalAnchorProperty()}. * * @return value of the labelHorizontalAnchor property */ public final HPos getLabelHorizontalAnchor() { return labelHorizontalAnchorProperty().get(); } /** * Returns the value of the {@link #labelVerticalAnchorProperty()}. * * @return value of the labelVerticalAnchor property */ public final VPos getLabelVerticalAnchor() { return labelVerticalAnchorProperty().get(); } /** * Returns the Axis that this indicator is associated with. * * @return associated Axis */ @Deprecated protected Axis getNumericAxis() { return axis; } /** * Returns the Axis that this indicator is associated with. * * @return associated Axis */ public Axis getAxis() { return axis; } /** * Returns the value of the {@link #textProperty()}. * * @return text displayed within or next to the indicator */ public final String getText() { return textProperty().get(); } /** * @return whether this plugin is editable */ public final boolean isEditable() { return editableIndicatorProperty().get(); } /** * Specifies anchor of the {@link #textProperty() text label} with respect to the horizontal label position i.e. it * describes whether the position is related to the LEFT, CENTER or RIGHT side of the label. The position itself * should be specified by the extending classes. * <p> * <b>Default value: {@link HPos#CENTER}</b> * </p> * * @return labelHorizontalAnchor property */ public final ObjectProperty<HPos> labelHorizontalAnchorProperty() { return labelHorizontalAnchor; } /** * Specifies anchor of the {@link #textProperty() text label} with respect to the vertical label position i.e. it * describes whether the position is related to the TOP, CENTER, BASELINE or BOTTOM of of the label. The position * itself should be specified by the extending classes. * <p> * <b>Default value: {@link VPos#CENTER}</b> * </p> * * @return labelVerticalAnchor property */ public final ObjectProperty<VPos> labelVerticalAnchorProperty() { return labelVerticalAnchor; } /** * Layouts the label within specified bounds and given horizontal and vertical position, taking into account * {@link #labelHorizontalAnchorProperty() horizontal} and {@link #labelVerticalAnchorProperty() vertical} anchor. * * @param bounds the bounding rectangle with respect to which the label should be positioned * @param hPos relative [0, 1] horizontal position of the label within the bounds * @param vPos relative [0, 1] vertical position of the label within the bounds */ protected final void layoutLabel(final Bounds bounds, final double hPos, final double vPos) { if (label.getText() == null || label.getText().isEmpty()) { getChartChildren().remove(label); return; } double xPos = bounds.getMinX(); double yPos = bounds.getMinY(); xOffset = bounds.getWidth() * hPos; yOffset = bounds.getHeight() * (1 - vPos); final double width = label.prefWidth(-1); final double height = label.prefHeight(width); if (getLabelHorizontalAnchor() == HPos.CENTER) { xOffset -= width / 2; } else if (getLabelHorizontalAnchor() == HPos.RIGHT) { xOffset -= width; } if (getLabelVerticalAnchor() == VPos.CENTER) { yOffset -= height / 2; } else if (getLabelVerticalAnchor() == VPos.BASELINE) { yOffset -= label.getBaselineOffset(); } else if (getLabelVerticalAnchor() == VPos.BOTTOM) { yOffset -= height; } label.resizeRelocate(xPos + xOffset, yPos + yOffset, width, height); addChildNodeIfNotPresent(label); } private void removeAxisListener() { final Axis valueAxis = getNumericAxis(); valueAxis.minProperty().removeListener(axisBoundsListener); valueAxis.maxProperty().removeListener(axisBoundsListener); } private void removePluginsListListener(final Chart chart) { chart.getPlugins().removeListener(pluginsListListener); } /** * Sets the state whether this plugin is editable * * @param newState true: edits are allowed */ public final void setEditable(final boolean newState) { editableIndicatorProperty().set(newState); } /** * Sets the value of the {@link #labelHorizontalAnchorProperty()}. * * @param anchor new anchor */ public final void setLabelHorizontalAnchor(final HPos anchor) { labelHorizontalAnchorProperty().set(anchor); } /** * Sets the value of the {@link #labelVerticalAnchorProperty()}. * * @param anchor new anchor */ public final void setLabelVerticalAnchor(final VPos anchor) { labelVerticalAnchorProperty().set(anchor); } public void setStyleClasses(final Node node, final String prefix, final String root) { node.getStyleClass().setAll(root, prefix + root, prefix + root + getIndicatorInstanceIndex()); } /** * Sets the value of the {@link #textProperty()}. * * @param text the new text. If {@code null}, the label will be hidden. */ public final void setText(final String text) { textProperty().set(text); } /** * Text to be displayed by the label. If set to {@code null}, the label is not shown. * * @return text of the indicator's label */ public final StringProperty textProperty() { return label.textProperty(); } private void updateMouseListener(final boolean state) { if (state) { label.setOnMouseReleased(mouseEvent -> label.setCursor(Cursor.HAND)); label.setOnMouseEntered(mouseEvent -> label.setCursor(Cursor.HAND)); } else { label.setOnMouseReleased(null); label.setOnMouseEntered(null); } } /** * There might be several instances of a given indicator class. If one wants to specify different CSS for each * instance - we need a unique class name for each, so whenever the list of plugins changes, this method should * update name of it's CSS class. */ public abstract void updateStyleClass(); // records relative x and y co-ordinates. protected class Delta { protected double x; protected double y; } }