/******************************************************************************* * Copyright (c) 2016 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: * Matthias Wienand (itemis AG) - initial API and implementation * *******************************************************************************/ package org.eclipse.gef.mvc.fx.parts; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import org.eclipse.gef.common.adapt.AdapterKey; import org.eclipse.gef.fx.nodes.Connection; import org.eclipse.gef.fx.nodes.OrthogonalRouter; import org.eclipse.gef.fx.utils.NodeUtils; import org.eclipse.gef.geometry.convert.fx.FX2Geometry; import org.eclipse.gef.geometry.convert.fx.Geometry2FX; import org.eclipse.gef.geometry.planar.BezierCurve; import org.eclipse.gef.geometry.planar.ICurve; import org.eclipse.gef.geometry.planar.IGeometry; import org.eclipse.gef.geometry.planar.IShape; import org.eclipse.gef.geometry.planar.Polyline; import org.eclipse.gef.geometry.planar.Rectangle; import org.eclipse.gef.mvc.fx.behaviors.IBehavior; import org.eclipse.gef.mvc.fx.providers.ResizableTransformableBoundsProvider; import com.google.common.reflect.TypeToken; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; import javafx.scene.Node; import javafx.scene.paint.Color; /** * * @author mwienand * */ public class DefaultSelectionHandlePartFactory implements IHandlePartFactory { /** * A binding key for the fill color of intra segment handles. */ public static final String INSERT_HANDLE_COLOR_PROVIDER = "INSERT_HANDLE_COLOR_PROVIDER"; /** * Defines the default {@link Color} for insertion handles. */ public static final Color DEFAULT_INSERT_HANDLE_COLOR = Color.WHITE; /** * An adapter role for the fill color of segment end handles. */ public static final String MOVE_HANDLE_COLOR_PROVIDER = "MOVE_HANDLE_COLOR_PROVIDER"; /** * Defines the default {@link Color} for movement handles. */ public static final Color DEFAULT_MOVE_HANDLE_COLOR = Color.web("#7986cb"); /** * An adapter role for the fill color of connected handles. */ public static final String CONNECTED_HANDLE_COLOR_PROVIDER = "CONNECTED_HANDLE_COLOR_PROVIDER"; /** * Defines the default {@link Color} for connected handles. */ public static final Color DEFAULT_CONNECTED_HANDLE_COLOR = Color.RED; /** * The role name for the <code>Provider<IGeometry></code> that will be * used to generate selection handles. */ public static final String SELECTION_HANDLES_GEOMETRY_PROVIDER = "SELECTION_HANDLES_GEOMETRY_PROVIDER"; /** * The role name for the <code>Provider<IGeometry></code> that will be * used to generate selection handles for a multi selection. */ public static final String MULTI_SELECTION_HANDLES_GEOMETRY_PROVIDER = "MULTI_SELECTION_HANDLES_GEOMETRY_PROVIDER"; /** * The minimum segment length so that creation handles are shown. */ protected static final double BENDPOINT_CREATE_HANDLE_MINIMUM_SEGMENT_LENGTH = 30; /** * The minimum segment length for the creation of segment bend handles. */ protected static final double SEGMENT_MOVE_HANDLE_MINIMUM_SEGMENT_LENGTH = 5; /** * The minimum segment length for creation of segment create handles */ protected static final double SEGMENT_CREATE_HANDLE_MINIMUM_SEGMENT_LENGTH = 45; private static Provider<BezierCurve[]> createSegmentsProvider( final Provider<? extends IGeometry> geometryProvider) { // TODO: get() call is very expensive, maybe cache the result and/or use // a binding mechanism instead? return new Provider<BezierCurve[]>() { @Override public BezierCurve[] get() { IGeometry geometry = geometryProvider.get(); if (geometry == null) { return new BezierCurve[] {}; } if (geometry instanceof IShape) { List<BezierCurve> segments = new ArrayList<>(); for (ICurve os : ((IShape) geometry).getOutlineSegments()) { segments.addAll(Arrays.asList(os.toBezier())); } return segments.toArray(new BezierCurve[] {}); } else if (geometry instanceof ICurve) { return ((ICurve) geometry).toBezier(); } else { throw new IllegalStateException( "Unable to deduce segments from geometry: Expected IShape or ICurve but got: " + geometry); } } }; } @Inject private Injector injector; // entry point @Override public List<IHandlePart<? extends Node>> createHandleParts( List<? extends IVisualPart<? extends Node>> targets, Map<Object, Object> contextMap) { // check that we have targets if (targets == null || targets.isEmpty()) { throw new IllegalArgumentException( "Part factory is called without targets."); } if (targets.size() == 1) { return createSingleSelectionHandleParts(targets.get(0), contextMap); } else { return createMultiSelectionHandleParts(targets, contextMap); } } /** * Creates handle parts for a multi selection. * * @param targets * The target {@link IVisualPart}s for which handles are to be * created. * @param contextMap * A map in which the state-less context {@link IBehavior}) may * place additional context information for the creation process. * It may either directly contain additional information needed * by the {@link IHandlePartFactory}, or may be passed back by * the {@link IHandlePartFactory} to the calling context * {@link IBehavior} to query such kind of information (in which * case it will allow the context {@link IBehavior} to identify * the creation context). * @return A list of {@link IHandlePart}s that can be used to manipulate the * given targets. */ protected List<IHandlePart<? extends Node>> createMultiSelectionHandleParts( final List<? extends IVisualPart<? extends Node>> targets, Map<Object, Object> contextMap) { // determine handle geometry provider @SuppressWarnings("serial") Provider<? extends IGeometry> multiSelectionHandlesGeometryInSceneProvider = targets .get(0).getRoot().getAdapter(AdapterKey .get(new TypeToken<Provider<? extends IGeometry>>() { }, MULTI_SELECTION_HANDLES_GEOMETRY_PROVIDER)); if (multiSelectionHandlesGeometryInSceneProvider == null) { // generate default handle geometry provider that unions the // ResizableTransformable bounds of all targets multiSelectionHandlesGeometryInSceneProvider = new Provider<IGeometry>() { @Override public IGeometry get() { Rectangle bounds = null; for (IVisualPart<? extends Node> part : targets) { ResizableTransformableBoundsProvider boundsProvider = new ResizableTransformableBoundsProvider(); boundsProvider.setAdaptable(part); IGeometry boundsInLocal = boundsProvider.get(); // transform to scene if (boundsInLocal != null) { Rectangle boundsInScene = FX2Geometry .toRectangle(part.getVisual() .localToScene(Geometry2FX .toFXBounds(boundsInLocal .getBounds()))); if (bounds == null) { bounds = boundsInScene; } else { bounds.union(boundsInScene); } } } return bounds; } }; } // per default, handle parts are created for the 4 corners of the // multi selection bounds Provider<BezierCurve[]> segmentsProvider = createSegmentsProvider( multiSelectionHandlesGeometryInSceneProvider); // check if provider is OK int segments = segmentsProvider.get().length; if (segments != 0 && segments != 4) { throw new IllegalStateException( "The multi selection handle geometry provider is expected to return bounds around the selection. However, instead of 4 segments, the provider provides " + segments + " segments."); } // create a handle for each start point of the segments List<IHandlePart<? extends Node>> handleParts = new ArrayList<>(); for (int i = 0; i < segments; i++) { SquareSegmentHandlePart part = injector .getInstance(SquareSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0); handleParts.add(part); } return handleParts; } /** * Creates handle parts for a single selection. * * @param target * The target {@link IVisualPart} for which handles are to be * created. * @param contextMap * A map in which the state-less context {@link IBehavior}) may * place additional context information for the creation process. * It may either directly contain additional information needed * by the {@link IHandlePartFactory}, or may be passed back by * the {@link IHandlePartFactory} to the calling context * {@link IBehavior} to query such kind of information (in which * case it will allow the context {@link IBehavior} to identify * the creation context). * @return A list of {@link IHandlePart}s that can be used to manipulate the * given targets. */ @SuppressWarnings("serial") protected List<IHandlePart<? extends Node>> createSingleSelectionHandleParts( final IVisualPart<? extends Node> target, Map<Object, Object> contextMap) { // determine handle geometry (in target visual local coordinates) final Provider<? extends IGeometry> selectionHandlesGeometryInTargetLocalProvider = target .getAdapter(AdapterKey .get(new TypeToken<Provider<? extends IGeometry>>() { }, SELECTION_HANDLES_GEOMETRY_PROVIDER)); IGeometry selectionHandlesGeometry = (selectionHandlesGeometryInTargetLocalProvider != null) ? selectionHandlesGeometryInTargetLocalProvider.get() : null; if (selectionHandlesGeometry == null) { return Collections.emptyList(); } // create provider that returns the geometry in scene coordinates final Provider<IGeometry> selectionHandlesGeometryInSceneProvider = new Provider<IGeometry>() { @Override public IGeometry get() { return NodeUtils.localToScene(target.getVisual(), selectionHandlesGeometryInTargetLocalProvider.get()); } }; Provider<BezierCurve[]> selectionHandlesSegmentsInSceneProvider = createSegmentsProvider( selectionHandlesGeometryInSceneProvider); if (selectionHandlesGeometry instanceof ICurve) { // create curve handles return createSingleSelectionHandlePartsForCurve(target, contextMap, selectionHandlesSegmentsInSceneProvider); } else if (selectionHandlesGeometry instanceof IShape) { if (selectionHandlesGeometry instanceof Rectangle) { // create box handles return createSingleSelectionHandlePartsForRectangularOutline( target, contextMap, selectionHandlesSegmentsInSceneProvider); } else { // create segment handles (based on outline) return createSingleSelectionHandlePartsForPolygonalOutline( target, contextMap, selectionHandlesSegmentsInSceneProvider); } } else { throw new IllegalStateException( "Unable to generate handles for this handle geometry. Expected ICurve or IShape, but got: " + selectionHandlesGeometry); } } /** * Creates handle parts for a single selection of which the handle geometry * is an {@link ICurve}. * * @param target * The target {@link IVisualPart} for which handles are to be * created. * @param contextMap * A map in which the state-less context {@link IBehavior}) may * place additional context information for the creation process. * It may either directly contain additional information needed * by the {@link IHandlePartFactory}, or may be passed back by * the {@link IHandlePartFactory} to the calling context * {@link IBehavior} to query such kind of information (in which * case it will allow the context {@link IBehavior} to identify * the creation context). * @param segmentsProvider * A provider for the segments of the handle geometry for which * handles are to be created. * @return A list of {@link IHandlePart}s that can be used to manipulate the * given targets. */ protected List<IHandlePart<? extends Node>> createSingleSelectionHandlePartsForCurve( final IVisualPart<? extends Node> target, Map<Object, Object> contextMap, Provider<BezierCurve[]> segmentsProvider) { List<IHandlePart<? extends Node>> hps = new ArrayList<>(); BezierCurve[] segments = segmentsProvider.get(); if (target.getVisual() instanceof Connection && ((Connection) target.getVisual()) .getRouter() instanceof OrthogonalRouter) { // generate segment based handles for (int i = 0; i < segments.length; i++) { // create handle for the start point of the curve if (i == 0) { CircleSegmentHandlePart part = injector .getInstance(CircleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0.0); hps.add(part); } // create quarter handle for the creation of a new segment double segmentLength = new Polyline(segments[i].getPoints()) .getLength(); if (segmentLength > SEGMENT_CREATE_HANDLE_MINIMUM_SEGMENT_LENGTH) { RectangleSegmentHandlePart part = injector .getInstance(RectangleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0.25); hps.add(part); } // mid handle for segment drag if (segmentLength > SEGMENT_MOVE_HANDLE_MINIMUM_SEGMENT_LENGTH) { RectangleSegmentHandlePart midPart = injector .getInstance(RectangleSegmentHandlePart.class); midPart.setSegmentsProvider(segmentsProvider); midPart.setSegmentIndex(i); midPart.setSegmentParameter(0.5); hps.add(midPart); } // create quarter handle for the creation of a new segment if (segmentLength > SEGMENT_CREATE_HANDLE_MINIMUM_SEGMENT_LENGTH) { RectangleSegmentHandlePart part = injector .getInstance(RectangleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0.75); hps.add(part); } // create handle for the end point of the curve if (i == segments.length - 1) { CircleSegmentHandlePart part = injector .getInstance(CircleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(1.0); hps.add(part); } } } else { // generate vertex based handles for (int i = 0; i < segments.length; i++) { // create handle for the start point of a segment CircleSegmentHandlePart part = injector .getInstance(CircleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0.0); hps.add(part); double segmentLength = new Polyline(segments[i].getPoints()) .getLength(); if (segmentLength >= BENDPOINT_CREATE_HANDLE_MINIMUM_SEGMENT_LENGTH) { // create handle for the middle of a segment part = injector.getInstance(CircleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0.5); hps.add(part); } // create handle for the end point of the curve if (i == segments.length - 1) { part = injector.getInstance(CircleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(1.0); hps.add(part); } } } return hps; } /** * Creates handle parts for a single selection of which the handle geometry * is an {@link IShape} but not a {@link Rectangle}. * * @param target * The target {@link IVisualPart} for which handles are to be * created. * @param contextMap * A map in which the state-less context {@link IBehavior}) may * place additional context information for the creation process. * It may either directly contain additional information needed * by the {@link IHandlePartFactory}, or may be passed back by * the {@link IHandlePartFactory} to the calling context * {@link IBehavior} to query such kind of information (in which * case it will allow the context {@link IBehavior} to identify * the creation context). * @param segmentsProvider * A provider for the segments of the handle geometry for which * handles are to be created. * @return A list of {@link IHandlePart}s that can be used to manipulate the * given targets. */ protected List<IHandlePart<? extends Node>> createSingleSelectionHandlePartsForPolygonalOutline( IVisualPart<? extends Node> target, Map<Object, Object> contextMap, Provider<BezierCurve[]> segmentsProvider) { List<IHandlePart<? extends Node>> handleParts = new ArrayList<>(); BezierCurve[] segments = segmentsProvider.get(); for (int i = 0; i < segments.length; i++) { // create handle for the start point of the segment CircleSegmentHandlePart part = injector .getInstance(CircleSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0); handleParts.add(part); } return handleParts; } /** * Creates handle parts for a single selection of which the handle geometry * is a {@link Rectangle}. * * @param target * The target {@link IVisualPart} for which handles are to be * created. * @param contextMap * A map in which the state-less context {@link IBehavior}) may * place additional context information for the creation process. * It may either directly contain additional information needed * by the {@link IHandlePartFactory}, or may be passed back by * the {@link IHandlePartFactory} to the calling context * {@link IBehavior} to query such kind of information (in which * case it will allow the context {@link IBehavior} to identify * the creation context). * @param segmentsProvider * A provider for the segments of the handle geometry for which * handles are to be created. * @return A list of {@link IHandlePart}s that can be used to manipulate the * given targets. */ protected List<IHandlePart<? extends Node>> createSingleSelectionHandlePartsForRectangularOutline( IVisualPart<? extends Node> target, Map<Object, Object> contextMap, Provider<BezierCurve[]> segmentsProvider) { List<IHandlePart<? extends Node>> hps = new ArrayList<>(); BezierCurve[] segments = segmentsProvider.get(); for (int i = 0; i < segments.length; i++) { SquareSegmentHandlePart part = injector .getInstance(SquareSegmentHandlePart.class); part.setSegmentsProvider(segmentsProvider); part.setSegmentIndex(i); part.setSegmentParameter(0); hps.add(part); } return hps; } }