MoveConnectionPointTool.java
/**
* Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
* All Rights Reserved.
*
* NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
* KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
* OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
* MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
* SPDX-License-Identifier: EPL-2.0
*
* Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
*
* This program includes and/or can make use of certain third party source code, object code, documentation and other
* files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
* configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
* conditions contained in any such Third Party Software or separate license file distributed with such Third Party
* Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
* aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
* censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
*/
package org.osate.ge.gef.ui.editor;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.eclipse.gef.fx.nodes.Connection;
import org.eclipse.gef.geometry.convert.fx.FX2Geometry;
import org.eclipse.gef.geometry.convert.fx.Geometry2FX;
import org.eclipse.gef.geometry.planar.Point;
import org.osate.ge.gef.AgeGefRuntimeException;
import org.osate.ge.gef.BaseConnectionNode;
import org.osate.ge.gef.FlowIndicatorNode;
import org.osate.ge.gef.PreferredPosition;
import org.osate.ge.gef.ui.diagram.GefAgeDiagram;
import org.osate.ge.gef.ui.diagram.GefAgeDiagramUtil;
import org.osate.ge.gef.ui.editor.overlays.ConnectionPointHandle;
import org.osate.ge.gef.ui.editor.overlays.ControlPointHandle;
import org.osate.ge.gef.ui.editor.overlays.CreateControlPointHandle;
import org.osate.ge.gef.ui.editor.overlays.FlowIndicatorPositionHandle;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.input.InputEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.transform.NonInvertibleTransformException;
import javafx.scene.transform.Transform;
/**
* {@link InputEventHandler} for moving {@link ConnectionPointHandle}.
* Starts interactions for creating control points, moving control points, and moving
* flow indicators.
*/
public class MoveConnectionPointTool implements InputEventHandler {
private final AgeEditor editor;
/**
* Creates a new instance
* @param editor the editor from which events originate.
*/
public MoveConnectionPointTool(final AgeEditor editor) {
this.editor = Objects.requireNonNull(editor, "editor must not be null");
}
@Override
public Cursor getCursor(final MouseEvent mouseMoveEvent) {
return mouseMoveEvent.getTarget() instanceof ConnectionPointHandle ? Cursor.MOVE : null;
}
@Override
public HandledEvent handleEvent(final InputEvent e) {
if (e.getEventType() != MouseEvent.MOUSE_PRESSED || ((MouseEvent) e).getButton() != MouseButton.PRIMARY
|| !(e.getTarget() instanceof ConnectionPointHandle)) {
return null;
}
return HandledEvent.newInteraction(new MoveConnectionPointInteraction(editor, (MouseEvent) e));
}
}
/**
* Interaction for moving connection points
*
*/
class MoveConnectionPointInteraction extends BaseInteraction {
private static final double REMOVE_POINT_DISTANCE = 2.0;
private static final double ADD_POINT_DISTANCE = 15.0;
private final AgeEditor editor;
/**
* The handle with which the user is interacting.
*/
private ConnectionPointHandle activeHandle;
/**
* The index of the control point. If the control point does not exist, this is the index where the point will be inserted.
*/
private Integer controlPointIndex;
/**
* Whether the control point exists. The point may be removed or added while the interaction is active.
*/
private boolean controlPointExists;
/**
* Flag which indicates whether the scene graph should be updated based on the diagram when the interaction is
* completed.
*/
private boolean updateSceneGraphOnComplete = true;
/**
* Creates a new instance
* @param editor the editor containing the diagram being edited
* @param e the mouse pressed event which started the interaction
*/
public MoveConnectionPointInteraction(final AgeEditor editor, final MouseEvent e) {
this.editor = editor;
final ConnectionPointHandle handle = (ConnectionPointHandle) e.getTarget();
if (handle instanceof CreateControlPointHandle) {
final CreateControlPointHandle createControlPointHandle = (CreateControlPointHandle) handle;
controlPointIndex = createControlPointHandle.getInsertionIndex();
controlPointExists = false;
} else if (handle instanceof ControlPointHandle) {
final ControlPointHandle controlPointHandle = (ControlPointHandle) handle;
controlPointIndex = controlPointHandle.getControlPointIndex();
controlPointExists = true;
} else if (handle instanceof FlowIndicatorPositionHandle) {
controlPointExists = true;
} else {
throw new AgeGefRuntimeException("Unexpected handle: " + handle);
}
activeHandle = handle;
}
@Override
public void close() {
// Reset state
activeHandle = null;
controlPointIndex = null;
if (updateSceneGraphOnComplete) {
// Update scene graph based on diagram element. This is needed to revert any scene changes that have been made
// during the interaction.
// Changes could be reverted more efficiently but this is simple and reliable.
editor.getGefDiagram().updateSceneGraph();
}
}
@Override
protected Interaction.InteractionState onMouseDragged(final MouseEvent e) {
if (e.getButton() != MouseButton.PRIMARY) {
return super.onMouseDragged(e);
}
final Transform sceneToDiagramTransform = editor.getGefDiagram().getSceneNode().getSceneToLocalTransform();
final Point2D eventInDiagram = sceneToDiagramTransform.transform(e.getSceneX(), e.getSceneY());
final Point2D snappedDiagramPosition = InputEventHandlerUtil.snap(editor, eventInDiagram, false);
if (activeHandle instanceof FlowIndicatorPositionHandle) {
final FlowIndicatorNode c = ((FlowIndicatorPositionHandle) activeHandle).getSceneNode();
final Node positioningReference = c.getPositioningReferenceOrThrow();
// The the position relative to the reference
final Point2D newPoint = getLocalPositionFromDiagram(editor.getGefDiagram(), snappedDiagramPosition,
positioningReference);
final Point2D oldPosition = PreferredPosition.get(activeHandle.getSceneNode());
PreferredPosition.set(activeHandle.getSceneNode(), newPoint);
// Adjust positions of control points so that only the ending position of the flow indicator is moved.
if (oldPosition != null) {
final double dx = newPoint.getX() - oldPosition.getX();
final double dy = newPoint.getY() - oldPosition.getY();
c.getInnerConnection().setControlPoints(c.getInnerConnection().getControlPoints().stream()
.map(p -> new Point(p.x - dx, p.y - dy)).collect(Collectors.toList()));
}
} else if (controlPointIndex != null) {
final BaseConnectionNode c = activeHandle.getSceneNode();
final Connection ic = c.getInnerConnection();
final Point2D newPosition = getLocalPositionFromDiagram(editor.getGefDiagram(), snappedDiagramPosition, ic);
// Get a list of the control points and the start and end points of connection.
final List<Point> allPoints = ic.getPointsUnmodifiable();
final List<org.eclipse.gef.geometry.planar.Point> controlPoints = ic.getControlPoints();
final ArrayList<org.eclipse.gef.geometry.planar.Point> endAndControlPoints = new ArrayList<>(
controlPoints.size() + 2);
endAndControlPoints.add(allPoints.get(0));
endAndControlPoints.addAll(controlPoints);
endAndControlPoints.add(allPoints.get(allPoints.size() - 1));
// Determine whether the point should be removed or added
final boolean remove = controlPointExists && distanceToLineSegment(newPosition, endAndControlPoints,
controlPointIndex, controlPointIndex + 2) <= REMOVE_POINT_DISTANCE;
final boolean add = !remove && !controlPointExists && distanceToLineSegment(newPosition,
endAndControlPoints, controlPointIndex, controlPointIndex + 1) >= ADD_POINT_DISTANCE;
if (remove) {
c.getInnerConnection().removeControlPoint(controlPointIndex);
controlPointExists = false;
} else if (add) {
controlPoints.add(controlPointIndex, FX2Geometry.toPoint(newPosition));
ic.setControlPoints(controlPoints);
controlPointExists = true;
} else if (controlPointExists) {
ic.setControlPoint(controlPointIndex, FX2Geometry.toPoint(newPosition));
}
}
return InteractionState.IN_PROGRESS;
}
@Override
protected Interaction.InteractionState onMouseReleased(final MouseEvent e) {
if (e.getButton() != MouseButton.PRIMARY) {
return super.onMouseReleased(e);
}
final BaseConnectionNode connectionNode = activeHandle.getSceneNode();
try {
final Transform sceneToDiagramTransform = editor.getGefDiagram().getSceneNode().getLocalToSceneTransform()
.createInverse();
final Transform connectionToDiagramTransform = sceneToDiagramTransform
.createConcatenation(connectionNode.getLocalToSceneTransform());
editor.getDiagram().modify("Update Control Point", m -> {
final DiagramElement diagramElementToModify = editor.getGefDiagram().getDiagramElement(connectionNode);
if (diagramElementToModify == null) {
throw new AgeGefRuntimeException("Unable to find diagram element");
}
m.setBendpoints(diagramElementToModify, connectionNode.getInnerConnection().getControlPoints().stream()
.map(p -> GefAgeDiagramUtil.toAgePoint(connectionToDiagramTransform.transform(p.x, p.y)))
.collect(Collectors.toList()));
if (connectionNode instanceof FlowIndicatorNode) {
m.setPosition(diagramElementToModify,
GefAgeDiagramUtil.toAgePoint(PreferredPosition.get(connectionNode)));
}
});
// The scene will be updated based on our modification. No need to update the scene in close().
updateSceneGraphOnComplete = false;
} catch (NonInvertibleTransformException ex) {
throw new AgeGefRuntimeException("Unable to create diagram scene to local transform", ex);
}
return InteractionState.COMPLETE;
}
/**
* Finds the distance between a point and a line segment. The line segment is defined by referencing points in a list.
* @param p the point for which to get the distance
* @param segmentPoints a list containing points
* @param segmentStartIndex the index of the start of the line segment
* @param segmentEndIndex the index of the end of the line segment
* @return the distance between the point and the segment. If either of the specified indices are invalid,
* {@link Double#POSITIVE_INFINITY} will be returned.
*/
private static double distanceToLineSegment(final Point2D p, final List<Point> segmentPoints, int segmentStartIndex,
int segmentEndIndex) {
if (segmentStartIndex < 0 || segmentStartIndex >= segmentPoints.size()) {
return Double.POSITIVE_INFINITY;
}
if (segmentEndIndex < 0 || segmentEndIndex >= segmentPoints.size()) {
return Double.POSITIVE_INFINITY;
}
final Point2D segmentStart = Geometry2FX.toFXPoint(segmentPoints.get(segmentStartIndex));
final Point2D segmentEnd = Geometry2FX.toFXPoint(segmentPoints.get(segmentEndIndex));
return distanceToLineSegment(p, segmentStart, segmentEnd);
}
/**
* Find the distance between a point and a line segment.
* Based on http://geomalgorithms.com/a02-_lines.html
* @param p the point for which to get the distance
* @param segmentStart the start of the line segment
* @param segmentEnd the end of the line segment
* @return the distance between the point and the segment.
*/
private static double distanceToLineSegment(Point2D p, Point2D segmentStart, Point2D segmentEnd) {
final Point2D v = segmentEnd.subtract(segmentStart);
final Point2D w = p.subtract(segmentStart);
final double c1 = w.dotProduct(v);
// Before segment start
if (c1 <= 0.0) {
return p.distance(segmentStart);
}
final double c2 = v.dotProduct(v);
// After segment end
if (c2 <= c1) {
return p.distance(segmentEnd);
}
final double b = c1 / c2;
final Point2D pb = segmentStart.add(b * v.getX(), b * v.getY());
return pb.distance(p);
}
/**
* Gets the diagram position in the local coordinates of a node.
* @param gefDiagram is the GEF diagram to use to determine transformation to diagram coordinates.
* @param positionInDiagram a position in scene coordinates.
* @param local the node to which the return value will be local.
* @return the position of the mouse event in the local coordinates system of the specified node.
*/
private static Point2D getLocalPositionFromDiagram(final GefAgeDiagram gefDiagram, final Point2D positionInDiagram, final Node local) {
final Point2D positionScene = gefDiagram.getSceneNode().getLocalToSceneTransform()
.transform(positionInDiagram);
try {
return local.getLocalToSceneTransform()
.createInverse()
.transform(positionScene.getX(),
positionScene.getY());
} catch (final NonInvertibleTransformException ex) {
throw new AgeGefRuntimeException(ex);
}
}
}