/******************************************************************************* * Copyright (c) 2014, 2015 itemis AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Alexander Nyßen (itemis AG) - initial API and implementation * Matthias Wienand (itemis AG) - contributions for Bugzilla #469491 * *******************************************************************************/ package org.eclipse.gef.fx.swt.controls; import java.util.ArrayList; import java.util.List; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.embed.swt.FXCanvas; import javafx.event.EventHandler; import javafx.geometry.Point2D; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.effect.BoxBlur; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.paint.CycleMethod; import javafx.scene.paint.LinearGradient; import javafx.scene.paint.Paint; import javafx.scene.paint.RadialGradient; import javafx.scene.paint.Stop; import javafx.scene.shape.Circle; import javafx.scene.shape.Line; import javafx.scene.shape.Polygon; import javafx.scene.shape.Rectangle; import javafx.util.Duration; /** * A picker for multi-stop {@link LinearGradient}s. * * @author anyssen * @author mwienand * */ public class FXAdvancedLinearGradientPicker extends Composite { private class StopPicker extends Group { private static final double SIZE = 8; private int index = 0; private DoubleProperty offsetProperty = new SimpleDoubleProperty(); private ObjectProperty<Color> colorProperty = new SimpleObjectProperty<>( Color.WHITE); private Polygon tip; private Rectangle picker; private double initialMouseX; private double initialTx; private boolean draggable; private EventHandler<? super MouseEvent> onDrag = new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (draggable) { double dx = event.getSceneX() - initialMouseX; double newOffset = (initialTx + dx) / preview.getWidth(); newOffset = Math.max(getPrevOffset(index), Math.min(getNextOffset(index), newOffset)); offsetProperty.set(newOffset); updateStop(index, offsetProperty.get(), colorProperty.get()); } } }; { tip = new Polygon(0, 0, SIZE / 2, SIZE / 2, -SIZE / 2, SIZE / 2); tip.setStroke(Color.BLACK); tip.setFill(Color.BLACK); picker = new Rectangle(-SIZE / 2, SIZE / 2, SIZE, SIZE); picker.setStroke(Color.BLACK); picker.fillProperty().bind(colorProperty); getChildren().addAll(tip, picker); } public StopPicker(int index) { this.index = index; // bind translation to offset translateXProperty() .bind(preview.widthProperty().multiply(offsetProperty)); // mouse feedback setOnMouseEntered(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (draggable) { BoxBlur boxBlur = new BoxBlur(0, 0, 1); setEffect(boxBlur); Timeline timeline = new Timeline( new KeyFrame(Duration.millis(0), new KeyValue(boxBlur.widthProperty(), 0), new KeyValue(boxBlur.heightProperty(), 0)), new KeyFrame(Duration.millis(150), new KeyValue(boxBlur.widthProperty(), 3), new KeyValue(boxBlur.heightProperty(), 3))); timeline.play(); } } }); setOnMouseExited(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (!isPressed()) { setEffect(null); } } }); // make draggable setOnMousePressed(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { initialMouseX = event.getSceneX(); initialTx = getTranslateX(); } }); setOnMouseDragged(onDrag); setOnMouseReleased(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { onDrag.handle(event); if (!isHover()) { setEffect(null); } } }); // copy values from Stop refresh(); // pick color on double click picker.setOnMouseClicked(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (event.getClickCount() > 1) { // double click colorProperty.set(FXColorPicker.pickColor(getShell(), colorProperty.get())); updateStop(StopPicker.this.index, offsetProperty.get(), colorProperty.get()); } else if (draggable && MouseButton.SECONDARY .equals(event.getButton())) { removeStop(StopPicker.this.index); } } }); } /** * Refreshes this stop picker by copying offset and color from the stops * list. */ public void refresh() { // copy offset and color from stop offsetProperty.set(getStops().get(index).getOffset()); colorProperty.set(getStops().get(index).getColor()); // determine if draggable (all but start and end) draggable = offsetProperty.get() != 0 && offsetProperty.get() != 1; } } /** * Property name used in change events related to * {@link #advancedLinearGradientProperty()} */ public static final String ADVANCED_LINEAR_GRADIENT_PROPERTY = "advancedLinearGradient"; private static final int DIRECTION_RADIUS = 16; private static final double OFFSET_THRESHOLD = 0.005; /** * Creates an "advanced" linear color gradient with 3 stops from the given * colors. * * @param c1 * The start color. * @param c2 * The middle color (t = 0.5). * @param c3 * The end color. * @return An "advanced" {@link LinearGradient} from the given colors. */ public static LinearGradient createAdvancedLinearGradient(Color c1, Color c2, Color c3) { Stop[] stops = new Stop[] { new Stop(0, c1), new Stop(0.5, c2), new Stop(1, c3) }; return new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops); } /** * Returns <code>true</code> if the given {@link Paint} is considered to be * an "advanced" gradient. Otherwise returns <code>false</code>. An advanced * gradient can either be a linear gradient with at least 3 stops, or any * radial gradient. * * @param paint * The {@link Paint} in question. * @return <code>true</code> if the given {@link Paint} is considered to be * an "advanced" gradient, othwerise <code>false</code>. */ public static boolean isAdvancedLinearGradient(Paint paint) { if (paint instanceof LinearGradient) { return ((LinearGradient) paint).getStops().size() > 2; } else if (paint instanceof RadialGradient) { return true; } return false; } private Property<LinearGradient> advancedLinearGradient = new SimpleObjectProperty<>( this, ADVANCED_LINEAR_GRADIENT_PROPERTY); private double directionX = 1; private double directionY = 0; private AnchorPane root; private Rectangle preview; private Group pickerGroup; private Line directionLine; /** * Constructs a new {@link FXAdvancedLinearGradientPicker}. * * @param parent * The parent {@link Composite}. * @param color1 * The first color of the initial three-stop * {@link LinearGradient}. * @param color2 * The second color of the initial three-stop * {@link LinearGradient}. * @param color3 * The third color of the initial three-stop * {@link LinearGradient}. */ public FXAdvancedLinearGradientPicker(Composite parent, Color color1, Color color2, Color color3) { super(parent, SWT.NONE); setLayout(new FillLayout()); // create a canvas to render the JavaFX controls FXCanvas canvas = new FXCanvas(this, SWT.NONE); // create preview pane and direction circle root = new AnchorPane(); root.setStyle("-fx-background-color: transparent;"); final Pane previewPane = new Pane(); final Circle directionCircle = new Circle(DIRECTION_RADIUS, Color.WHITE); directionLine = new Line(); directionLine.setMouseTransparent(true); directionLine.setEndX(DIRECTION_RADIUS); directionLine.setEndY(0); directionLine.startXProperty().bind(directionCircle.centerXProperty()); directionLine.startYProperty().bind(directionCircle.centerYProperty()); directionLine.translateXProperty() .bind(directionCircle.layoutXProperty()); directionLine.translateYProperty() .bind(directionCircle.layoutYProperty()); root.getChildren().addAll(previewPane, directionCircle, directionLine); // layout preview pane AnchorPane.setTopAnchor(previewPane, 2d); AnchorPane.setBottomAnchor(previewPane, 20d); AnchorPane.setLeftAnchor(previewPane, 15d); AnchorPane.setRightAnchor(previewPane, 40d); // layout direction circle AnchorPane.setTopAnchor(directionCircle, 5d); AnchorPane.setRightAnchor(directionCircle, 0d); // create a preview rectangle that displays the gradient preview = new Rectangle(); preview.setStroke(Color.DARKGRAY); pickerGroup = new Group(); root.getChildren().addAll(preview, pickerGroup); preview.xProperty().bind(previewPane.layoutXProperty()); preview.yProperty().bind(previewPane.layoutYProperty()); preview.widthProperty().bind(previewPane.widthProperty()); preview.heightProperty().bind(previewPane.heightProperty()); preview.setFill(advancedLinearGradient.getValue()); // create highlight line for showing where new spots are created final Rectangle highlightSpotCreation = new Rectangle(); highlightSpotCreation.setStroke(Color.TRANSPARENT); highlightSpotCreation.setFill(new Color(1, 1, 0, 0.5)); highlightSpotCreation.heightProperty() .bind(preview.heightProperty().add(10)); highlightSpotCreation.yProperty().bind(preview.yProperty()); highlightSpotCreation.setWidth(3); highlightSpotCreation.setTranslateX(-1.5); highlightSpotCreation.setVisible(false); highlightSpotCreation.setMouseTransparent(true); root.getChildren().add(highlightSpotCreation); // update highlighting when the mouse is moved preview.setOnMouseEntered(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { highlightSpotCreation.setVisible(true); } }); preview.setOnMouseMoved(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { highlightSpotCreation.setX(event.getX()); } }); preview.setOnMouseExited(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { highlightSpotCreation.setVisible(false); } }); // create a new stop with primary mouse button preview.setOnMouseClicked(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (MouseButton.PRIMARY.equals(event.getButton())) { // create new stop Point2D previewPosition = previewPane .sceneToLocal(event.getSceneX(), event.getSceneY()); double offset = previewPosition.getX() / preview.getWidth(); offset = Math.max(0, Math.min(1, offset)); createStop(offset); } } }); // change direction when clicking into the direction circle directionCircle.setOnMouseClicked(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (directionX == 1) { directionX = 0; directionY = 1; } else { directionX = 1; directionY = 0; } updateDirectionLine(); List<Stop> newStops = new ArrayList<>(getStops()); updateGradient(newStops); } }); Scene scene = new Scene(root); // copy background color from parent composite org.eclipse.swt.graphics.Color backgroundColor = parent.getBackground(); scene.setFill(Color.rgb(backgroundColor.getRed(), backgroundColor.getGreen(), backgroundColor.getBlue())); canvas.setScene(scene); // create an initial linear gradient with three stops setAdvancedGradient( createAdvancedLinearGradient(color1, color2, color3)); } /** * Returns a writable {@link Property} for the advanced gradient. * * @return A writable {@link Property}. */ public Property<LinearGradient> advancedLinearGradientProperty() { return advancedLinearGradient; } /** * Creates a new spot for the given offset. * * @param offset * The offset for the new spot. */ protected void createStop(double offset) { List<Stop> newStops = new ArrayList<>(getStops()); int addIndex = newStops.size(); for (int i = 0; i < newStops.size(); i++) { if (newStops.get(i).getOffset() > offset) { addIndex = i; break; } } newStops.add(addIndex, new Stop(offset, Color.WHITE)); updateGradient(newStops); } /** * Returns the currently selected advanced gradient. * * @return The currently selected advanced gradient. */ public LinearGradient getAdvancedLinearGradient() { return advancedLinearGradient.getValue(); } /** * Computes the maximum offset for the given stop index. * * @param stopIndex * The index of the stop for which to compute the next offset. * @return The maximum offset for the given stop index. */ protected double getNextOffset(int stopIndex) { if (stopIndex == getStops().size() - 1) { return 1 - OFFSET_THRESHOLD; } return getStops().get(stopIndex + 1).getOffset() - OFFSET_THRESHOLD; } /** * Computes the minimum offset for the given stop index. * * @param stopIndex * The index of the stop for which to compute the previous * offset. * @return The minimum offset for the given stop index. */ protected double getPrevOffset(int stopIndex) { if (stopIndex == 0) { return 0 + OFFSET_THRESHOLD; } return getStops().get(stopIndex - 1).getOffset() + OFFSET_THRESHOLD; } /** * Returns a list of the {@link Stop}s of the currently selected advanced * gradient. * * @return A list of the {@link Stop}s of the currently selected advanced * gradient. */ protected List<Stop> getStops() { return advancedLinearGradient.getValue().getStops(); } /** * Removes the spot specified by the given index. * * @param index * The spot index. */ protected void removeStop(int index) { List<Stop> newStops = new ArrayList<>(getStops()); newStops.remove(index); updateGradient(newStops); } /** * Sets the gradient managed by this gradient picker to the given value. * Does also update the UI so that the new gradient can be manipulated. * * @param advancedLinearGradient * The new gradient. */ public void setAdvancedGradient(LinearGradient advancedLinearGradient) { if (!isAdvancedLinearGradient(advancedLinearGradient)) { throw new IllegalArgumentException( "Given value '" + advancedLinearGradient + "' is no advanced linear gradient"); } this.advancedLinearGradient.setValue(advancedLinearGradient); preview.setFill(advancedLinearGradient); // adapt direction directionX = advancedLinearGradient.getEndX(); if (directionX == 1) { directionY = 0; } else { directionX = 0; directionY = 1; } updateDirectionLine(); // adapt stops List<Stop> stops = getStops(); for (int i = 0; i < stops.size(); i++) { if (pickerGroup.getChildren().size() > i) { // refresh existing stop pickers ((StopPicker) pickerGroup.getChildren().get(i)).refresh(); } else { // add new stop pickers StopPicker stopPicker = new StopPicker(i); pickerGroup.getChildren().add(stopPicker); stopPicker.layoutXProperty().bind(preview.xProperty()); stopPicker.layoutYProperty().bind( preview.yProperty().add(preview.heightProperty())); } } // remove unused stop pickers for (int i = pickerGroup.getChildren().size() - 1; i >= stops .size(); i--) { pickerGroup.getChildren().remove(i); } } /** * Updates the direction line to display the current direction (specified by * directionX and directionY). */ protected void updateDirectionLine() { directionLine.setEndX(directionX * DIRECTION_RADIUS); directionLine.setEndY(directionY * DIRECTION_RADIUS); } /** * Changes the currently selected advanced gradient to a new linear gradient * that is constructed from the given list of {@link Stop}s. * * @param newStops * The list of {@link Stop}s from which the newly selected * advanced gradient is constructed. */ protected void updateGradient(List<Stop> newStops) { setAdvancedGradient(new LinearGradient(0, 0, directionX, directionY, true, CycleMethod.NO_CYCLE, newStops)); } /** * Sets the offset and color of the spot specified by the given index to the * given values. * * @param index * The index of the spot. * @param offset * The new offset for that spot. * @param color * The new color for that spot. */ protected void updateStop(int index, double offset, Color color) { List<Stop> newStops = new ArrayList<>(getStops()); newStops.set(index, new Stop(offset, color)); updateGradient(newStops); } }