/* * Copyright (c) 2013 by Gerrit Grunwald * * 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.hansolo.enzo.gauge.skin; import eu.hansolo.enzo.common.ConicalGradient; import eu.hansolo.enzo.common.Marker; import eu.hansolo.enzo.common.Section; import eu.hansolo.enzo.common.ValueEvent; import eu.hansolo.enzo.gauge.RadialBargraph; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ParallelTransition; import javafx.animation.PauseTransition; import javafx.animation.SequentialTransition; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.collections.ListChangeListener; import javafx.collections.MapChangeListener; import javafx.event.EventHandler; import javafx.event.EventType; import javafx.geometry.Point2D; import javafx.geometry.VPos; import javafx.scene.CacheHint; import javafx.scene.Node; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; import javafx.scene.effect.Blend; import javafx.scene.effect.BlendMode; import javafx.scene.effect.BlurType; import javafx.scene.effect.DropShadow; import javafx.scene.effect.InnerShadow; import javafx.scene.image.Image; import javafx.scene.input.MouseEvent; import javafx.scene.input.TouchEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.scene.paint.CycleMethod; import javafx.scene.paint.ImagePattern; import javafx.scene.paint.RadialGradient; import javafx.scene.paint.Stop; import javafx.scene.shape.Arc; import javafx.scene.shape.ArcType; import javafx.scene.shape.StrokeLineCap; import javafx.scene.shape.StrokeType; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import javafx.scene.transform.Rotate; import javafx.util.Duration; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Locale; /** * Created by * User: hansolo * Date: 17.07.13 * Time: 08:02 */ public class RadialBargraphSkin extends SkinBase<RadialBargraph> implements Skin<RadialBargraph> { private static final double PREFERRED_WIDTH = 200; private static final double PREFERRED_HEIGHT = 200; private static final double MINIMUM_WIDTH = 50; private static final double MINIMUM_HEIGHT = 50; private static final double MAXIMUM_WIDTH = 1024; private static final double MAXIMUM_HEIGHT = 1024; private double size; private double centerX; private double centerY; private Pane pane; private Region background; private Canvas ticksAndSectionsCanvas; private GraphicsContext ticksAndSections; private Region threshold; private Rotate thresholdRotate; private boolean thresholdExceeded; private Region minMeasuredValue; private Rotate minMeasuredValueRotate; private Region maxMeasuredValue; private Rotate maxMeasuredValueRotate; private DoubleProperty angle; private Arc bar; private Region knob; private DropShadow dropShadow; private Text title; private Text unit; private Text value; private DropShadow valueBlendBottomShadow; private InnerShadow valueBlendTopShadow; private Blend blend; private double angleStep; private Timeline timeline; private double interactiveAngle; private EventHandler<MouseEvent> mouseEventHandler; private EventHandler<TouchEvent> touchEventHandler; private List<Node> markersToRemove; private Color barColor; private ConicalGradient barGradient; // ******************** Constructors ************************************** public RadialBargraphSkin(RadialBargraph radialBargraph) { super(radialBargraph); angleStep = radialBargraph.getAngleRange() / (radialBargraph.getMaxValue() - radialBargraph.getMinValue()); angle = new SimpleDoubleProperty(this, "angle", getSkinnable().getValue() * angleStep); timeline = new Timeline(); mouseEventHandler = mouseEvent -> handleMouseEvent(mouseEvent); touchEventHandler = touchEvent -> handleTouchEvent(touchEvent); markersToRemove = new ArrayList<>(); init(); initGraphics(); registerListeners(); } // ******************** Initialization ************************************ private void init() { if (Double.compare(getSkinnable().getPrefWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getPrefHeight(), 0.0) <= 0 || Double.compare(getSkinnable().getWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getHeight(), 0.0) <= 0) { if (getSkinnable().getPrefWidth() > 0 && getSkinnable().getPrefHeight() > 0) { getSkinnable().setPrefSize(getSkinnable().getPrefWidth(), getSkinnable().getPrefHeight()); } else { getSkinnable().setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT); } } if (Double.compare(getSkinnable().getMinWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getMinHeight(), 0.0) <= 0) { getSkinnable().setMinSize(MINIMUM_WIDTH, MINIMUM_HEIGHT); } if (Double.compare(getSkinnable().getMaxWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getMaxHeight(), 0.0) <= 0) { getSkinnable().setMaxSize(MAXIMUM_WIDTH, MAXIMUM_HEIGHT); } } private void initGraphics() { Font.loadFont(getClass().getResourceAsStream("/eu/hansolo/enzo/fonts/opensans-semibold.ttf"), (0.06 * PREFERRED_HEIGHT)); // "OpenSans" barColor = getSkinnable().getBarColor(); barGradient = new ConicalGradient(new Stop(0.0, Color.TRANSPARENT), new Stop(1.0, Color.TRANSPARENT)); valueBlendBottomShadow = new DropShadow(); valueBlendBottomShadow.setBlurType(BlurType.TWO_PASS_BOX); valueBlendBottomShadow.setColor(Color.rgb(255, 255, 255, 0.5)); valueBlendBottomShadow.setOffsetX(0); valueBlendBottomShadow.setOffsetY(0.005 * PREFERRED_WIDTH); valueBlendBottomShadow.setRadius(0); valueBlendTopShadow = new InnerShadow(); valueBlendTopShadow.setBlurType(BlurType.TWO_PASS_BOX); valueBlendTopShadow.setColor(Color.rgb(0, 0, 0, 0.7)); valueBlendTopShadow.setOffsetX(0); valueBlendTopShadow.setOffsetY(0.005 * PREFERRED_WIDTH); valueBlendTopShadow.setRadius(0.005 * PREFERRED_WIDTH); blend = new Blend(); blend.setMode(BlendMode.MULTIPLY); blend.setBottomInput(valueBlendBottomShadow); blend.setTopInput(valueBlendTopShadow); background = new Region(); background.getStyleClass().setAll("background"); ticksAndSectionsCanvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT); ticksAndSections = ticksAndSectionsCanvas.getGraphicsContext2D(); minMeasuredValue = new Region(); minMeasuredValue.getStyleClass().setAll("min-measured-value"); minMeasuredValueRotate = new Rotate(180 - getSkinnable().getStartAngle()); minMeasuredValue.getTransforms().setAll(minMeasuredValueRotate); minMeasuredValue.setOpacity(getSkinnable().isMinMeasuredValueVisible() ? 1 : 0); minMeasuredValue.setManaged(getSkinnable().isMinMeasuredValueVisible()); maxMeasuredValue = new Region(); maxMeasuredValue.getStyleClass().setAll("max-measured-value"); maxMeasuredValueRotate = new Rotate(180 - getSkinnable().getStartAngle()); maxMeasuredValue.getTransforms().setAll(maxMeasuredValueRotate); maxMeasuredValue.setOpacity(getSkinnable().isMaxMeasuredValueVisible() ? 1 : 0); maxMeasuredValue.setManaged(getSkinnable().isMaxMeasuredValueVisible()); threshold = new Region(); threshold.getStyleClass().setAll("threshold"); thresholdRotate = new Rotate(180 - getSkinnable().getStartAngle()); threshold.getTransforms().setAll(thresholdRotate); threshold.setOpacity(getSkinnable().isThresholdVisible() ? 1 : 0); threshold.setManaged(getSkinnable().isThresholdVisible()); thresholdExceeded = false; bar = new Arc(); bar.setType(ArcType.ROUND); bar.setCenterX(PREFERRED_WIDTH * 0.5); bar.setCenterY(PREFERRED_HEIGHT * 0.5); bar.setRadiusX(PREFERRED_WIDTH * 0.5 - 4); bar.setRadiusY(PREFERRED_HEIGHT * 0.5 - 4); bar.setStartAngle(getSkinnable().getStartAngle() - 90); bar.setLength(0); bar.setStrokeType(StrokeType.CENTERED); bar.setStroke(null); bar.setFill(new RadialGradient(0, 0, PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.5, PREFERRED_WIDTH * 0.45, false, CycleMethod.NO_CYCLE, new Stop(0.0, barColor), new Stop(0.76, barColor.deriveColor(-5, 1, 1, 1)), // -5 for on the barColorHue) new Stop(0.79, barColor), new Stop(0.97, barColor), new Stop(1.0, barColor.deriveColor(-5, 1, 1, 1)))); // -5 for on the barColorHue) knob = new Region(); knob.setPickOnBounds(false); knob.getStyleClass().setAll("knob"); dropShadow = new DropShadow(); dropShadow.setColor(Color.rgb(0, 0, 0, 0.25)); dropShadow.setBlurType(BlurType.TWO_PASS_BOX); dropShadow.setRadius(0.015 * PREFERRED_WIDTH); dropShadow.setOffsetY(0.015 * PREFERRED_WIDTH); title = new Text(getSkinnable().getTitle()); title.setMouseTransparent(true); title.setTextOrigin(VPos.CENTER); title.getStyleClass().setAll("title"); title.setEffect(getSkinnable().isPlainValue() ? null : blend); unit = new Text(getSkinnable().getUnit()); unit.setMouseTransparent(true); unit.setTextOrigin(VPos.CENTER); unit.getStyleClass().setAll("unit"); unit.setEffect(getSkinnable().isPlainValue() ? null : blend); value = new Text(); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", 0.0)); value.setMouseTransparent(true); value.setTextOrigin(VPos.CENTER); value.getStyleClass().setAll("value"); value.setEffect(getSkinnable().isPlainValue() ? null : blend); // Add all nodes pane = new Pane(); pane.getChildren().setAll(background, bar, ticksAndSectionsCanvas, minMeasuredValue, maxMeasuredValue, threshold, knob, title, unit, value); pane.getChildren().addAll(getSkinnable().getMarkers().keySet()); getChildren().setAll(pane); } private void registerListeners() { getSkinnable().widthProperty().addListener(observable -> handleControlPropertyChanged("RESIZE")); getSkinnable().heightProperty().addListener(observable -> handleControlPropertyChanged("RESIZE")); getSkinnable().valueProperty().addListener(observable -> handleControlPropertyChanged("VALUE")); getSkinnable().minValueProperty().addListener(observable -> handleControlPropertyChanged("RECALC")); getSkinnable().maxValueProperty().addListener(observable -> handleControlPropertyChanged("RECALC")); getSkinnable().minMeasuredValueProperty().addListener(observable -> handleControlPropertyChanged("MIN_MEASURED_VALUE")); getSkinnable().minMeasuredValueVisibleProperty().addListener(observable -> handleControlPropertyChanged("MIN_MEASURED_VALUE_VISIBLE")); getSkinnable().maxMeasuredValueProperty().addListener(observable -> handleControlPropertyChanged("MAX_MEASURED_VALUE")); getSkinnable().maxMeasuredValueVisibleProperty().addListener(observable -> handleControlPropertyChanged("MAX_MEASURED_VALUE_VISIBLE")); getSkinnable().barColorProperty().addListener(observable -> handleControlPropertyChanged("BAR_COLOR")); getSkinnable().animatedProperty().addListener(observable -> handleControlPropertyChanged("ANIMATED")); getSkinnable().thresholdProperty().addListener(observable -> handleControlPropertyChanged("THRESHOLD")); getSkinnable().thresholdVisibleProperty().addListener(observable -> handleControlPropertyChanged("THRESHOLD_VISIBLE")); getSkinnable().angleRangeProperty().addListener(observable -> handleControlPropertyChanged("ANGLE_RANGE")); getSkinnable().numberFormatProperty().addListener(observable -> handleControlPropertyChanged("RECALC")); getSkinnable().plainValueProperty().addListener(observable -> handleControlPropertyChanged("PLAIN_VALUE")); getSkinnable().interactiveProperty().addListener(observable -> handleControlPropertyChanged("INTERACTIVE")); getSkinnable().getSections().addListener((ListChangeListener<Section>) change -> handleControlPropertyChanged("CANVAS_REFRESH")); getSkinnable().getMarkers().addListener((MapChangeListener<Marker, Rotate>) change -> handleControlPropertyChanged("MARKER")); getSkinnable().barGradientProperty().addListener((ListChangeListener<Stop>) change -> handleControlPropertyChanged("BAR_GRADIENT")); getSkinnable().barGradientEnabledProperty().addListener(observable -> handleControlPropertyChanged("BAR_COLOR")); angle.addListener(observable -> handleControlPropertyChanged("ANGLE")); knob.setOnMousePressed(event -> getSkinnable().setInteractive(!getSkinnable().isInteractive())); minMeasuredValue.setOnMousePressed(mouseEventHandler); minMeasuredValue.setOnMouseReleased(mouseEventHandler); minMeasuredValue.setOnTouchPressed(touchEventHandler); minMeasuredValue.setOnTouchReleased(touchEventHandler); maxMeasuredValue.setOnMousePressed(mouseEventHandler); maxMeasuredValue.setOnMouseReleased(mouseEventHandler); maxMeasuredValue.setOnTouchPressed(touchEventHandler); maxMeasuredValue.setOnTouchReleased(touchEventHandler); threshold.setOnMousePressed(mouseEventHandler); threshold.setOnMouseDragged(mouseEventHandler); threshold.setOnMouseReleased(mouseEventHandler); threshold.setOnTouchPressed(touchEventHandler); threshold.setOnTouchMoved(touchEventHandler); threshold.setOnTouchReleased(touchEventHandler); for (Marker marker : getSkinnable().getMarkers().keySet()) { marker.setOnMousePressed(mouseEventHandler); marker.setOnMouseDragged(mouseEventHandler); marker.setOnMouseReleased(mouseEventHandler); marker.setOnTouchPressed(touchEventHandler); marker.setOnTouchMoved(touchEventHandler); marker.setOnTouchReleased(touchEventHandler); } } // ******************** Methods ******************************************* protected void handleControlPropertyChanged(final String PROPERTY) { if ("RESIZE".equals(PROPERTY)) { resize(); } else if ("VALUE".equals(PROPERTY)) { setBar(); } else if ("RECALC".equals(PROPERTY)) { angleStep = getSkinnable().getAngleRange() / (getSkinnable().getMaxValue() - getSkinnable().getMinValue()); resize(); } else if ("ANGLE".equals(PROPERTY)) { if (getSkinnable().isInteractive()) return; double currentValue = angle.get() / angleStep; value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", currentValue)); value.setTranslateX((size - value.getLayoutBounds().getWidth()) * 0.5); bar.setLength(-currentValue * angleStep); // Check threshold if (thresholdExceeded) { if (currentValue < getSkinnable().getThreshold()) { getSkinnable().fireEvent(new ValueEvent(this, null, ValueEvent.VALUE_UNDERRUN)); thresholdExceeded = false; } } else { if (currentValue > getSkinnable().getThreshold()) { getSkinnable().fireEvent(new ValueEvent(this, null, ValueEvent.VALUE_EXCEEDED)); thresholdExceeded = true; } } // Check each marker for (Marker marker : getSkinnable().getMarkers().keySet()) { if (marker.isExceeded()) { if (currentValue < marker.getValue()) { marker.fireMarkerEvent(new Marker.MarkerEvent(this, null, Marker.MarkerEvent.MARKER_UNDERRUN)); marker.setExceeded(false); } } else { if (currentValue > marker.getValue()) { marker.fireMarkerEvent(new Marker.MarkerEvent(this, null, Marker.MarkerEvent.MARKER_EXCEEDED)); marker.setExceeded(true); } } } // Check min- and maxMeasuredValue if (currentValue < getSkinnable().getMinMeasuredValue()) { getSkinnable().setMinMeasuredValue(currentValue); minMeasuredValueRotate.setAngle(currentValue * angleStep - 180 - getSkinnable().getStartAngle()); } if (currentValue > getSkinnable().getMaxMeasuredValue()) { getSkinnable().setMaxMeasuredValue(currentValue); maxMeasuredValueRotate.setAngle(currentValue * angleStep - 180 - getSkinnable().getStartAngle()); } } else if ("PLAIN_VALUE".equals(PROPERTY)) { value.setEffect(getSkinnable().isPlainValue() ? null : blend); } else if ("INTERACTIVE".equals(PROPERTY)) { if (getSkinnable().isInteractive()) { unit.setText("Interactive"); value.setText(""); resizeText(); } else { unit.setText(getSkinnable().getUnit()); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", (angle.get() / angleStep))); resizeText(); } } else if ("CANVAS_REFRESH".equals(PROPERTY)) { ticksAndSections.clearRect(0, 0, size, size); drawSections(ticksAndSections); drawTickMarks(ticksAndSections); } else if ("THRESHOLD".equals(PROPERTY)) { thresholdRotate.setAngle(getSkinnable().getThreshold() * angleStep - 180 - getSkinnable().getStartAngle()); } else if ("THRESHOLD_VISIBLE".equals(PROPERTY)) { threshold.setOpacity(getSkinnable().isThresholdVisible() ? 1 : 0); threshold.setManaged(getSkinnable().isThresholdVisible()); } else if ("MIN_MEASURED_VALUE_VISIBLE".equals(PROPERTY)) { minMeasuredValue.setOpacity(getSkinnable().isMinMeasuredValueVisible() ? 1 : 0); minMeasuredValue.setManaged(getSkinnable().isMinMeasuredValueVisible()); } else if ("MAX_MEASURED_VALUE_VISIBLE".equals(PROPERTY)) { maxMeasuredValue.setOpacity(getSkinnable().isMaxMeasuredValueVisible() ? 1 : 0); maxMeasuredValue.setManaged(getSkinnable().isMaxMeasuredValueVisible()); } else if ("MARKER".equals(PROPERTY)) { checkForRemovedMarkers(); for (Marker marker : getSkinnable().getMarkers().keySet()) { if (pane.getChildren().contains(marker)) continue; pane.getChildren().add(marker); // Add MouseEvent handler marker.setOnMousePressed(mouseEventHandler); marker.setOnMouseDragged(mouseEventHandler); marker.setOnMouseReleased(mouseEventHandler); // Add TouchEvent handler marker.setOnTouchPressed(touchEventHandler); marker.setOnTouchMoved(touchEventHandler); marker.setOnTouchReleased(touchEventHandler); } drawMarkers(); } else if ("BAR_COLOR".equals(PROPERTY)) { barColor = getSkinnable().getBarColor(); resize(); } } @Override protected double computeMinWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) { return super.computeMinWidth(Math.max(MINIMUM_HEIGHT, HEIGHT - TOP_INSET - BOTTOM_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET); } @Override protected double computeMinHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) { return super.computeMinHeight(Math.max(MINIMUM_WIDTH, WIDTH - LEFT_INSET - RIGHT_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET); } @Override protected double computeMaxWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) { return super.computeMaxWidth(Math.min(MAXIMUM_HEIGHT, HEIGHT - TOP_INSET - BOTTOM_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET); } @Override protected double computeMaxHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) { return super.computeMaxHeight(Math.min(MAXIMUM_WIDTH, WIDTH - LEFT_INSET - RIGHT_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET); } @Override protected double computePrefWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) { double prefHeight = PREFERRED_HEIGHT; if (HEIGHT != -1) { prefHeight = Math.max(0, HEIGHT - TOP_INSET - BOTTOM_INSET); } return super.computePrefWidth(prefHeight, TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET); } @Override protected double computePrefHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) { double prefWidth = PREFERRED_WIDTH; if (WIDTH != -1) { prefWidth = Math.max(0, WIDTH - LEFT_INSET - RIGHT_INSET); } return super.computePrefHeight(prefWidth, TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET); } // ******************** Private Methods *********************************** private void checkForRemovedMarkers() { markersToRemove.clear(); for (Node node : pane.getChildren()) { if (node instanceof Marker) { if (getSkinnable().getMarkers().keySet().contains(node)) continue; node.setManaged(false); node.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseEventHandler); node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, mouseEventHandler); node.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseEventHandler); node.removeEventHandler(TouchEvent.TOUCH_PRESSED, touchEventHandler); node.removeEventHandler(TouchEvent.TOUCH_MOVED, touchEventHandler); node.removeEventHandler(TouchEvent.TOUCH_RELEASED, touchEventHandler); markersToRemove.add(node); } } for (Node node : markersToRemove) pane.getChildren().remove(node); } private void handleMouseEvent(final MouseEvent MOUSE_EVENT) { final Object SRC = MOUSE_EVENT.getSource(); final EventType TYPE = MOUSE_EVENT.getEventType(); if (getSkinnable().isInteractive() && SRC.equals(threshold)) { if (MouseEvent.MOUSE_PRESSED == TYPE) { unit.setText("Threshold"); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getThreshold())); resizeText(); } else if (MouseEvent.MOUSE_DRAGGED == TYPE) { touchRotate(MOUSE_EVENT.getSceneX() - getSkinnable().getLayoutX(), MOUSE_EVENT.getSceneY() - getSkinnable().getLayoutY(), thresholdRotate); } else if (MouseEvent.MOUSE_RELEASED == TYPE) { getSkinnable().setThreshold(Double.parseDouble(value.getText())); fadeBackToInteractive(); } } else if (getSkinnable().isInteractive() && SRC instanceof Marker) { if (MouseEvent.MOUSE_PRESSED == TYPE) { unit.setText(((Marker) SRC).getText()); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", ((Marker) SRC).getValue())); resizeText(); } else if (MouseEvent.MOUSE_DRAGGED == TYPE) { touchRotate(MOUSE_EVENT.getSceneX() - getSkinnable().getLayoutX(), MOUSE_EVENT.getSceneY() - getSkinnable().getLayoutY(), getSkinnable().getMarkers().get(SRC)); } else if (MouseEvent.MOUSE_RELEASED == TYPE) { ((Marker) SRC).setValue(Double.parseDouble(value.getText())); fadeBackToInteractive(); } } else if (getSkinnable().isInteractive() && SRC.equals(minMeasuredValue)) { if (MouseEvent.MOUSE_PRESSED == TYPE) { unit.setText("Min"); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMinMeasuredValue())); resizeText(); } else if (MouseEvent.MOUSE_RELEASED == TYPE) { fadeBackToInteractive(); } } else if (getSkinnable().isInteractive() && SRC.equals(maxMeasuredValue)) { if (MouseEvent.MOUSE_PRESSED == TYPE) { unit.setText("Max"); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMaxMeasuredValue())); resizeText(); } else if (MouseEvent.MOUSE_RELEASED == TYPE) { fadeBackToInteractive(); } } } private void handleTouchEvent(final TouchEvent TOUCH_EVENT) { final Object SRC = TOUCH_EVENT.getSource(); final EventType TYPE = TOUCH_EVENT.getEventType(); if (SRC.equals(threshold)) { if (TouchEvent.TOUCH_PRESSED == TYPE) { unit.setText("Threshold"); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getThreshold())); resizeText(); } else if (TouchEvent.TOUCH_MOVED == TYPE) { touchRotate(TOUCH_EVENT.getTouchPoint().getSceneX() - getSkinnable().getLayoutX(), TOUCH_EVENT.getTouchPoint().getSceneY() - getSkinnable().getLayoutY(), thresholdRotate); } else if (TouchEvent.TOUCH_RELEASED == TYPE) { getSkinnable().setThreshold(Double.parseDouble(value.getText())); fadeBackToInteractive(); } } else if (SRC instanceof Marker) { if (TouchEvent.TOUCH_PRESSED == TYPE) { unit.setText(((Marker) SRC).getText()); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", ((Marker) SRC).getValue())); resizeText(); } else if (TouchEvent.TOUCH_MOVED == TYPE) { touchRotate(TOUCH_EVENT.getTouchPoint().getSceneX() - getSkinnable().getLayoutX(), TOUCH_EVENT.getTouchPoint().getSceneY() - getSkinnable().getLayoutY(), getSkinnable().getMarkers().get(SRC)); } else if (TouchEvent.TOUCH_RELEASED == TYPE) { ((Marker) SRC).setValue(Double.parseDouble(value.getText())); fadeBackToInteractive(); } } else if (SRC.equals(minMeasuredValue)) { if (TouchEvent.TOUCH_PRESSED == TYPE) { unit.setText("Min"); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMinMeasuredValue())); resizeText(); } else if (TouchEvent.TOUCH_RELEASED == TYPE) { fadeBackToInteractive(); } } else if (SRC.equals(maxMeasuredValue)) { if (TouchEvent.TOUCH_PRESSED == TYPE) { unit.setText("Max"); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMaxMeasuredValue())); resizeText(); } else if (TouchEvent.TOUCH_RELEASED == TYPE) { fadeBackToInteractive(); } } } private double getTheta(double x, double y) { double deltaX = x - centerX; double deltaY = y - centerY; double radius = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)); double nx = deltaX / radius; double ny = deltaY / radius; double theta = Math.atan2(ny, nx); return Double.compare(theta, 0.0) >= 0 ? Math.toDegrees(theta) : Math.toDegrees((theta)) + 360.0; } private void touchRotate(final double X, final double Y, final Rotate ROTATE) { double theta = getTheta(X, Y); interactiveAngle = (theta + 90) % 360; double newValue = Double.compare(interactiveAngle, 180) <= 0 ? (interactiveAngle + 180.0 + getSkinnable().getStartAngle() - 360) / angleStep : (interactiveAngle - 180.0 + getSkinnable().getStartAngle() - 360) / angleStep; if (Double.compare(newValue, getSkinnable().getMinValue()) >= 0 && Double.compare(newValue, getSkinnable().getMaxValue()) <= 0) { ROTATE.setAngle(interactiveAngle); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", newValue)); resizeText(); } } private void fadeBackToInteractive() { FadeTransition fadeUnitOut = new FadeTransition(Duration.millis(425), unit); fadeUnitOut.setFromValue(1.0); fadeUnitOut.setToValue(0.0); FadeTransition fadeValueOut = new FadeTransition(Duration.millis(425), value); fadeValueOut.setFromValue(1.0); fadeValueOut.setToValue(0.0); PauseTransition pause = new PauseTransition(Duration.millis(50)); FadeTransition fadeUnitIn = new FadeTransition(Duration.millis(425), unit); fadeUnitIn.setFromValue(0.0); fadeUnitIn.setToValue(1.0); FadeTransition fadeValueIn = new FadeTransition(Duration.millis(425), value); fadeValueIn.setFromValue(0.0); fadeValueIn.setToValue(1.0); ParallelTransition parallelIn = new ParallelTransition(fadeUnitIn, fadeValueIn); ParallelTransition parallelOut = new ParallelTransition(fadeUnitOut, fadeValueOut); parallelOut.setOnFinished(event -> { unit.setText("Interactive"); value.setText(""); resizeText(); }); SequentialTransition sequence = new SequentialTransition(parallelOut, pause, parallelIn); sequence.play(); } private void setBar() { double range = (getSkinnable().getMaxValue() - getSkinnable().getMinValue()); double angleRange = getSkinnable().getAngleRange(); angleStep = angleRange / range; double targetAngle = getSkinnable().getValue() * angleStep; if (getSkinnable().isAnimated()) { timeline.stop(); final KeyValue KEY_VALUE = new KeyValue(angle, targetAngle, Interpolator.SPLINE(0.5, 0.4, 0.4, 1.0)); final KeyFrame KEY_FRAME = new KeyFrame(Duration.millis(getSkinnable().getAnimationDuration()), KEY_VALUE); timeline.getKeyFrames().setAll(KEY_FRAME); timeline.play(); } else { angle.set(targetAngle); } } private void drawTickMarks(final GraphicsContext CTX) { double sinValue; double cosValue; double startAngle = getSkinnable().getStartAngle(); Point2D center = new Point2D(size * 0.5, size * 0.5); for (double angle = 0, counter = getSkinnable().getMinValue() ; Double.compare(counter, getSkinnable().getMaxValue()) <= 0 ; angle -= angleStep, counter++) { sinValue = Math.sin(Math.toRadians(angle + startAngle)); cosValue = Math.cos(Math.toRadians(angle + startAngle)); Point2D innerPoint = new Point2D(center.getX() + size * 0.388 * sinValue, center.getY() + size * 0.388 * cosValue); Point2D outerPoint = new Point2D(center.getX() + size * 0.485 * sinValue, center.getY() + size * 0.485 * cosValue); CTX.setStroke(getSkinnable().getTickMarkFill()); if (counter % getSkinnable().getMinorTickSpace() == 0) { CTX.setLineWidth(size * 0.0035); CTX.strokeLine(innerPoint.getX(), innerPoint.getY(), outerPoint.getX(), outerPoint.getY()); } } } private final void drawSections(final GraphicsContext CTX) { final double xy = (size - 0.87 * size) * 0.5; final double wh = size * 0.87; final double MIN_VALUE = getSkinnable().getMinValue(); final double OFFSET = 90 - getSkinnable().getStartAngle(); for (int i = 0 ; i < getSkinnable().getSections().size() ; i++) { final Section SECTION = getSkinnable().getSections().get(i); final double ANGLE_START = (SECTION.getStart() - MIN_VALUE) * angleStep; final double ANGLE_EXTEND = (SECTION.getStop() - SECTION.getStart()) * angleStep; CTX.save(); switch(i) { case 0: CTX.setStroke(getSkinnable().getSectionFill0()); break; case 1: CTX.setStroke(getSkinnable().getSectionFill1()); break; case 2: CTX.setStroke(getSkinnable().getSectionFill2()); break; case 3: CTX.setStroke(getSkinnable().getSectionFill3()); break; case 4: CTX.setStroke(getSkinnable().getSectionFill4()); break; case 5: CTX.setStroke(getSkinnable().getSectionFill5()); break; case 6: CTX.setStroke(getSkinnable().getSectionFill6()); break; case 7: CTX.setStroke(getSkinnable().getSectionFill7()); break; case 8: CTX.setStroke(getSkinnable().getSectionFill8()); break; case 9: CTX.setStroke(getSkinnable().getSectionFill9()); break; } CTX.setLineWidth(size * 0.1); CTX.setLineCap(StrokeLineCap.BUTT); CTX.strokeArc(xy, xy, wh, wh, -(OFFSET + ANGLE_START), -ANGLE_EXTEND, ArcType.OPEN); CTX.restore(); } } private final void drawMarkers() { for (Marker marker : getSkinnable().getMarkers().keySet()) { marker.setPrefSize(0.05 * size, 0.05 * size); marker.relocate((size - marker.getPrefWidth()) * 0.5, size * 0.04); getSkinnable().getMarkers().get(marker).setPivotX(marker.getPrefWidth() * 0.5); getSkinnable().getMarkers().get(marker).setPivotY(size * 0.46); getSkinnable().getMarkers().get(marker).setAngle(marker.getValue() * angleStep - 180 - getSkinnable().getStartAngle()); } } private void recalculateBarGradient() { double angleFactor = 1d / 360d; double emptyRange = 360d - getSkinnable().getAngleRange(); double offset = angleFactor * ((360 - getSkinnable().getStartAngle() + 180 - emptyRange * 0.5) % 360); List<Stop> stops = new LinkedList<>(); double emptyOffset = (emptyRange * 0.5) * angleFactor; double minFraction = 1.0; double maxFraction = 0.0; Color minFractionColor = Color.TRANSPARENT; Color maxFractionColor = Color.TRANSPARENT; for (Stop stop : getSkinnable().getBarGradient()) { double fraction = stop.getOffset(); if (fraction < minFraction) { minFraction = fraction; minFractionColor = stop.getColor(); } if (fraction > maxFraction) { maxFraction = fraction; maxFractionColor = stop.getColor(); } } stops.add(new Stop(0d, minFractionColor)); stops.add(new Stop(0d + emptyOffset, minFractionColor)); stops.add(new Stop(1d - emptyOffset, maxFractionColor)); stops.add(new Stop(1d, maxFractionColor)); if (getSkinnable().getBarGradient().size() == 2) { stops.add(new Stop((maxFraction - minFraction) * 0.5, (Color) Interpolator.LINEAR.interpolate(minFractionColor, maxFractionColor, 0.5))); } for (Stop stop : getSkinnable().getBarGradient()) { if (Double.compare(stop.getOffset(), minFraction) == 0 || Double.compare(stop.getOffset(), maxFraction) == 0) continue; stops.add(stop); } barGradient = new ConicalGradient(new Point2D(size * 0.5, size * 0.5), offset, stops); } private void resizeText() { title.setFont(Font.font("Open Sans", FontWeight.NORMAL, size * 0.1)); title.setTranslateX((size - title.getLayoutBounds().getWidth()) * 0.5); title.setTranslateY(size * 0.3); unit.setFont(Font.font("Open Sans", FontWeight.NORMAL, size * 0.1)); unit.setTranslateX((size - unit.getLayoutBounds().getWidth()) * 0.5); unit.setTranslateY(size * 0.7); value.setFont(Font.font("Open Sans", FontWeight.BOLD, size * 0.25)); value.setTranslateX((size - value.getLayoutBounds().getWidth()) * 0.5); value.setTranslateY(size * 0.5); } private void resize() { size = getSkinnable().getWidth() < getSkinnable().getHeight() ? getSkinnable().getWidth() : getSkinnable().getHeight(); centerX = size * 0.5; centerY = size * 0.5; final double RADIUS = size * 0.5 - 2; valueBlendBottomShadow.setOffsetY(0.005 * size); valueBlendTopShadow.setOffsetY(0.005 * size); valueBlendTopShadow.setRadius(0.005 * size); dropShadow.setRadius(0.015 * size); dropShadow.setOffsetY(0.015 * size); background.setPrefSize(size, size); ticksAndSectionsCanvas.setWidth(size); ticksAndSectionsCanvas.setHeight(size); ticksAndSections.clearRect(0, 0, size, size); drawSections(ticksAndSections); drawTickMarks(ticksAndSections); ticksAndSectionsCanvas.setCache(true); ticksAndSectionsCanvas.setCacheHint(CacheHint.QUALITY); drawMarkers(); minMeasuredValue.setPrefSize(0.03 * size, 0.03 * size); minMeasuredValue.relocate((size - minMeasuredValue.getPrefWidth()) * 0.5, size * 0.11); minMeasuredValueRotate.setPivotX(minMeasuredValue.getPrefWidth() * 0.5); minMeasuredValueRotate.setPivotY(size * 0.39); minMeasuredValueRotate.setAngle(getSkinnable().getMinMeasuredValue() * angleStep - 180 - getSkinnable().getStartAngle()); maxMeasuredValue.setPrefSize(0.03 * size, 0.03 * size); maxMeasuredValue.relocate((size - maxMeasuredValue.getPrefWidth()) * 0.5, size * 0.11); maxMeasuredValueRotate.setPivotX(maxMeasuredValue.getPrefWidth() * 0.5); maxMeasuredValueRotate.setPivotY(size * 0.39); maxMeasuredValueRotate.setAngle(getSkinnable().getMaxMeasuredValue() * angleStep - 180 - getSkinnable().getStartAngle()); threshold.setPrefSize(0.06 * size, 0.055 * size); threshold.relocate((size - threshold.getPrefWidth()) * 0.5, size * 0.08); thresholdRotate.setPivotX(threshold.getPrefWidth() * 0.5); thresholdRotate.setPivotY(size * 0.42); thresholdRotate.setAngle(getSkinnable().getThreshold() * angleStep - 180 - getSkinnable().getStartAngle()); value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", (angle.get() / angleStep))); bar.setCenterX(centerX); bar.setCenterY(centerY); bar.setRadiusX(RADIUS); bar.setRadiusY(RADIUS); if (getSkinnable().isBarGradientEnabled()) { recalculateBarGradient(); Image image = barGradient.getImage(size, size); bar.setFill(new ImagePattern(image, 0, 0, size, size, false)); } else { bar.setFill(new RadialGradient(0, 0, centerX, centerY, RADIUS, false, CycleMethod.NO_CYCLE, new Stop(0.0, barColor), new Stop(0.76, barColor.deriveColor(-5, 1, 1, 1)), // -5 for on the barColorHue) new Stop(0.79, barColor), new Stop(0.97, barColor), new Stop(1.0, barColor.deriveColor(-5, 1, 1, 1)))); // -5 for on the barColorHue) } knob.setPrefSize(size * 0.75, size * 0.75); knob.setTranslateX((size - knob.getPrefWidth()) * 0.5); knob.setTranslateY((size - knob.getPrefHeight()) * 0.5); resizeText(); } }