MoveInputEventHandler.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.Set;
import java.util.stream.Collectors;

import org.osate.ge.gef.BaseConnectionNode;
import org.osate.ge.gef.ConnectionNode;
import org.osate.ge.gef.ContainerShape;
import org.osate.ge.gef.DockSide;
import org.osate.ge.gef.DockedShape;
import org.osate.ge.gef.LabelNode;
import org.osate.ge.gef.PreferredPosition;
import org.osate.ge.gef.ui.diagram.GefAgeDiagramUtil;
import org.osate.ge.gef.ui.editor.overlays.GuideOverlay;
import org.osate.ge.internal.diagram.runtime.AgeDiagramUtil;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramElementPredicates;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.diagram.runtime.DockArea;

import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.transform.Transform;

/**
 *  {@link InputEventHandler} which handles moving diagram elements.
 */
public class MoveInputEventHandler implements InputEventHandler {
	/**
	 * Minimum mouse dragged distance in diagram coordinates before starting the move interaction.
	 * This avoids starting the move interaction when the user is attempting to perform a rename or other interaction.
	 */
	private static final double MIN_MOUSE_DRAGGED_DISTANCE = 10.0;

	private final AgeEditor editor;

	/**
	 * Location of the last mouse pressed event in diagram coordinates. Used to determine if the mouse was dragged enough to
	 * start the move interaction.
	 */
	private Point2D mousePressLocationDiagram;

	/**
	 * The primary connection label from the mouse pressed event. Null if a primary connection label was not clicked.
	 */
	private Node mousePressPrimaryConnectionLabel;

	/**
	 * Creates a new instance
	 * @param editor the editor from which events originate.
	 */
	public MoveInputEventHandler(final AgeEditor editor) {
		this.editor = Objects.requireNonNull(editor, "editor must not be null");
	}

	@Override
	public Cursor getCursor(final MouseEvent mouseMoveEvent) {
		return null;
	}

	@Override
	public HandledEvent handleEvent(final InputEvent e) {
		if (!editor.getPaletteModel().isSelectToolActive()) {
			return null;
		}

		if (e instanceof MouseEvent) {
			final MouseEvent mouseEvent = (MouseEvent) e;
			if ((mouseEvent.getEventType() != MouseEvent.MOUSE_PRESSED
					&& mouseEvent.getEventType() != MouseEvent.MOUSE_DRAGGED)
					|| mouseEvent.getButton() != MouseButton.PRIMARY) {
				return null;
			}

			final DiagramElement clickedDiagramElement = InputEventHandlerUtil
					.getTargetDiagramElement(editor.getGefDiagram(), e.getTarget());
			if (clickedDiagramElement == null) {
				return null;
			}

			if (e.getEventType() == MouseEvent.MOUSE_PRESSED) {
				// Store the starting position.
				mousePressLocationDiagram = editor.getGefDiagram()
						.getSceneNode()
						.getSceneToLocalTransform()
						.transform(mouseEvent.getSceneX(), mouseEvent.getSceneY());

				// Determine if the primary label is the node being moved
				mousePressPrimaryConnectionLabel = null;
				if (DiagramElementPredicates.isConnection(clickedDiagramElement)) {
					for (Node tmp = (Node) e.getTarget(); tmp != null; tmp = tmp.getParent()) {
						if (tmp instanceof BaseConnectionNode) {
							break;
						}

						if (tmp instanceof LabelNode) {
							mousePressPrimaryConnectionLabel = tmp;
							break;
						}
					}
				}
			} else if (e.getEventType() == MouseEvent.MOUSE_DRAGGED && mousePressLocationDiagram != null) {
				final Point2D newPositionDiagram = editor.getGefDiagram()
						.getSceneNode()
						.getSceneToLocalTransform()
						.transform(mouseEvent.getSceneX(), mouseEvent.getSceneY());
				final double d = mousePressLocationDiagram.distance(newPositionDiagram);
				if (d > MIN_MOUSE_DRAGGED_DISTANCE) {
					try {
						if (mousePressPrimaryConnectionLabel == null) {
							final MouseMoveSelectedElementsInteraction newInteraction = new MouseMoveSelectedElementsInteraction(
									editor, mousePressLocationDiagram);
							return HandledEvent.newInteraction(newInteraction);
						} else {
							final BaseConnectionNode cn = InputEventHandlerUtil
									.getClosestConnection(mousePressPrimaryConnectionLabel);
							if (cn == null) {
								return null;
							}

							final MovePrimaryConnectionLabelInteraction newInteraction = new MovePrimaryConnectionLabelInteraction(
									editor, mousePressLocationDiagram, cn, mousePressPrimaryConnectionLabel);

							return HandledEvent.newInteraction(newInteraction);
						}
					} finally {
						mousePressLocationDiagram = null;
					}
				}
			}

			return HandledEvent.consumed();
		} else if (e instanceof KeyEvent) {
			if (e.getEventType() != KeyEvent.KEY_PRESSED) {
				return null;
			}

			final KeyEvent keyEvent = (KeyEvent) e;
			if (keyEvent.getCode() != KeyCode.PERIOD) {
				return null;
			}

			final KeyboardMoveSelectedElementsInteraction newInteraction = new KeyboardMoveSelectedElementsInteraction(
					editor);
			return HandledEvent.newInteraction(newInteraction);
		} else {
			return null;
		}
	}
}

/**
 * Interaction for moving a primary primary connection label using the mouse.
 */
class MovePrimaryConnectionLabelInteraction extends BaseInteraction {
	private final AgeEditor editor;
	private final Node label;
	private final Point2D initialClickLocationInDiagram;
	private final Point2D connectionMidpointPositionInDiagram;
	private final Bounds originalLabelLayoutInDiagram;

	/**
	 * Creates a new instance
	 * @param editor the editor containing the diagram being modified.
	 * @param initialClickLocationInDiagram the location in diagram coordinates of the initial click
	 * @param connection the connection whose label is being moved
	 * @param label the scene graph node of the primary label being moved
	 */
	public MovePrimaryConnectionLabelInteraction(final AgeEditor editor, final Point2D initialClickLocationInDiagram,
			final BaseConnectionNode connection, final Node label) {
		this.editor = editor;
		this.label = label;
		this.initialClickLocationInDiagram = initialClickLocationInDiagram;

		final Transform sceneToDiagramTransform = editor.getGefDiagram().getSceneNode().getSceneToLocalTransform();
		this.connectionMidpointPositionInDiagram = sceneToDiagramTransform
				.transform(connection.getLocalToSceneTransform().transform(connection.getMidpointAnchorPosition()));
		this.originalLabelLayoutInDiagram = sceneToDiagramTransform
				.transform(label.getLocalToSceneTransform().transform(label.getLayoutBounds()));
	}

	@Override
	public void close() {
		// Update scene graph based on diagram elements. This is needed to revert any scene changes that have been made
		// during the interaction and to ensure that the scene node reflects the diagram elements after modification.
		editor.getGefDiagram().updateSceneGraph();
	}

	@Override
	public Cursor getCursor() {
		return Cursor.MOVE;
	}

	@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 totalDelta = eventInDiagram.subtract(initialClickLocationInDiagram);

		// Calculate a new position and update the preferred position of the label
		final double originalX = originalLabelLayoutInDiagram.getMinX();
		final double originalY = originalLabelLayoutInDiagram.getMinY();
		final double newPositionX = InputEventHandlerUtil.snapX(editor, originalX + totalDelta.getX(), false);
		final double newPositionY = InputEventHandlerUtil.snapX(editor, originalY + totalDelta.getY(), false);
		PreferredPosition.set(label, new Point2D(newPositionX - connectionMidpointPositionInDiagram.getX(),
				newPositionY - connectionMidpointPositionInDiagram.getY()));

		return InteractionState.IN_PROGRESS;
	}

	@Override
	protected Interaction.InteractionState onMouseReleased(final MouseEvent e) {
		if (e.getButton() != MouseButton.PRIMARY) {
			return super.onMousePressed(e);
		}

		// Move diagram elements by updating the diagram to reflect the current scene graph
		editor.getDiagram().modify("Move", m -> editor.getGefDiagram().updateDiagramFromSceneGraph());

		return InteractionState.COMPLETE;
	}
}

/**
 * Interaction for moving selected diagram elements using the mouse.
 * Dragging the primary mouse button moves the selected diagram elements.
 * The diagram is updated when the primary mouse button is released.
 */
class MouseMoveSelectedElementsInteraction extends BaseInteraction {
	private final AgeEditor editor;
	private final SelectedElementsMover mover;
	private final Point2D initialClickLocationInDiagram;

	/**
	 * Creates a new instance
	 * @param editor the editor containing the diagram being modified.
	 * @param initialClickLocationInDiagram the location in diagram coordinates of the click which started the interaction.
	 */
	public MouseMoveSelectedElementsInteraction(final AgeEditor editor, final Point2D initialClickLocationInDiagram) {
		this.editor = editor;
		this.mover = new SelectedElementsMover(editor);
		this.initialClickLocationInDiagram = initialClickLocationInDiagram;
	}

	@Override
	public void close() {
		mover.close();
	}

	@Override
	public Cursor getCursor() {
		return Cursor.MOVE;
	}

	@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 totalDelta = eventInDiagram.subtract(initialClickLocationInDiagram);
		final boolean snapToGrid = !e.isAltDown();

		mover.updateSceneGraph(totalDelta, snapToGrid);

		return InteractionState.IN_PROGRESS;
	}

	@Override
	protected Interaction.InteractionState onMouseReleased(final MouseEvent e) {
		if (e.getButton() != MouseButton.PRIMARY) {
			return super.onMousePressed(e);
		}

		mover.apply();

		return InteractionState.COMPLETE;
	}
}

/**
 * Interaction for moving selected diagram elements using the keyboard.
 * The position of selected nodes are changed using the arrow keys.
 * The diagram is updated when the user pressed "Enter"
 */
class KeyboardMoveSelectedElementsInteraction extends BaseInteraction {
	private static final double NORMAL_STEP_SIZE = 10;
	private static final double SMALL_STEP_SIZE = 1;

	private final SelectedElementsMover mover;
	private Point2D totalDelta = Point2D.ZERO;

	/**
	 * Creates a new instance
	 * @param editor the editor containing the diagram being modified.
	 */
	public KeyboardMoveSelectedElementsInteraction(final AgeEditor editor) {
		this.mover = new SelectedElementsMover(editor);
	}

	@Override
	public void close() {
		mover.close();
	}

	@Override
	public Cursor getCursor() {
		return Cursor.MOVE;
	}

	@Override
	protected Interaction.InteractionState onMousePressed(final MouseEvent e) {
		return InteractionState.COMPLETE;
	}

	@Override
	protected Interaction.InteractionState onKeyPressed(final KeyEvent e) {
		final boolean snapToGrid = !e.isAltDown();
		final double stepSize = snapToGrid ? NORMAL_STEP_SIZE : SMALL_STEP_SIZE;

		if (e.getCode() == KeyCode.ENTER) {
			mover.apply();
			return InteractionState.COMPLETE;
		} else if (e.getCode() == KeyCode.LEFT) {
			shiftSelectedDiagramElements(-stepSize, 0.0, snapToGrid);
			return InteractionState.IN_PROGRESS;
		} else if (e.getCode() == KeyCode.RIGHT) {
			shiftSelectedDiagramElements(stepSize, 0.0, snapToGrid);
			return InteractionState.IN_PROGRESS;
		} else if (e.getCode() == KeyCode.UP) {
			shiftSelectedDiagramElements(0.0, -stepSize, snapToGrid);
			return InteractionState.IN_PROGRESS;
		} else if (e.getCode() == KeyCode.DOWN) {
			shiftSelectedDiagramElements(0.0, stepSize, snapToGrid);
			return InteractionState.IN_PROGRESS;
		} else {
			return super.onKeyPressed(e);
		}
	}

	private void shiftSelectedDiagramElements(final double dx, final double dy, final boolean snapToGrid) {
		totalDelta = totalDelta.add(dx, dy);
		mover.updateSceneGraph(totalDelta, snapToGrid);
	}
}

/**
 * Class which handles moving the diagram elements which are selected at the time of instantiation.
 */
class SelectedElementsMover implements AutoCloseable {
	/**
	 * Maximum number of connections to show while moving. This avoids performance issues related to updating affected
	 * connections.
	 */
	private static final int MAX_SHOWN_AFFECTED_CONNECTIONS = 5;

	private final AgeEditor editor;
	private final List<DiagramElementSnapshot> elementsToMove;
	private final GuideOverlay guides;

	// Flag which indicates whether affected connections will be shown while moving. If false then the connections are hidden
	// and the control points are updated after movement is complete.
	private final boolean showAffectedConnections;

	/**
	 * Creates a new instance. This instance will move diagram elements which are selected at the time the
	 * object is instantiated.
	 * @param editor the editor which contain the diagram.
	 */
	public SelectedElementsMover(final AgeEditor editor) {
		this.editor = Objects.requireNonNull(editor, "editor must not be null");
		this.elementsToMove = createMoveElementSnapshotsForSelection(editor);
		this.guides = new GuideOverlay(editor,
				elementsToMove.stream().map(s -> s.diagramElement).collect(Collectors.toSet()));
		this.showAffectedConnections = elementsToMove.stream()
				.flatMap(s -> s.affectedConnections.stream())
				.filter(DiagramElementPredicates::isRegularConnection)
				.count() <= MAX_SHOWN_AFFECTED_CONNECTIONS;
		if (!showAffectedConnections) {
			setAffectedConnectionsVisible(false);
		}
	}

	@Override
	public void close() {
		this.guides.close();

		if (!showAffectedConnections) {
			setAffectedConnectionsVisible(true);
		}

		// Update scene graph based on diagram elements. This is needed to revert any scene changes that have been made
		// during the interaction and to ensure that the scene node reflects the diagram elements after modification.
		editor.getGefDiagram().updateSceneGraph();
	}

	/**
	 * Updates the positions of scene graph nodes.
	 * @param totalPositionDelta the total movement since the beginning of the interaction.
	 * @param snapToGrid whether the positions should be snapped to grid. Ignored for connection labels.
	 */
	public void updateSceneGraph(final Point2D totalPositionDelta, final boolean snapToGrid) {
		final Transform sceneToDiagramTransform = editor.getGefDiagram().getSceneNode().getSceneToLocalTransform();

		// Reset guide
		guides.reset();

		// Move nodes
		for (final DiagramElementSnapshot snapshot : elementsToMove) {
			if (snapshot.sceneNode instanceof LabelNode) {
				final BaseConnectionNode cn = InputEventHandlerUtil.getClosestConnection(snapshot.sceneNode);

				// Secondary connection label
				if (cn != null) {
					// Determine the new position in diagram coordinates
					final double newPositionX = InputEventHandlerUtil.snapX(editor,
							snapshot.boundsInDiagram.getMinX() + totalPositionDelta.getX(), false);
					final double newPositionY = InputEventHandlerUtil.snapX(editor,
							snapshot.boundsInDiagram.getMinY() + totalPositionDelta.getY(), false);

					// Set the new position relative to the midpoint
					final Point2D connectionMidpointPositionInDiagram = sceneToDiagramTransform
							.transform(cn.getLocalToSceneTransform().transform(cn.getMidpointAnchorPosition()));
					PreferredPosition.set(snapshot.sceneNode,
							new Point2D(newPositionX - connectionMidpointPositionInDiagram.getX(),
									newPositionY - connectionMidpointPositionInDiagram.getY()));
				}
			} else {
				// Determine snapped position
				double newPositionX = snapshot.positionInLocal.getX() + (InputEventHandlerUtil.snapX(editor,
						snapshot.boundsInDiagram.getMinX() + totalPositionDelta.getX(), snapToGrid)
						- snapshot.boundsInDiagram.getMinX());
				double newPositionY = snapshot.positionInLocal.getY() + (InputEventHandlerUtil.snapY(editor,
						snapshot.boundsInDiagram.getMinY() + totalPositionDelta.getY(), snapToGrid)
						- snapshot.boundsInDiagram.getMinY());

				// Constrain the position to the container
				final Node container = InputEventHandlerUtil.getLogicalShapeContainer(snapshot.sceneNode);
				if (container instanceof ContainerShape) {
					final Bounds parentBounds = container.getLayoutBounds();
					newPositionX = Math.max(0,
							Math.min(newPositionX, parentBounds.getWidth() - snapshot.boundsInDiagram.getWidth()));
					newPositionY = Math.max(0,
							Math.min(newPositionY, parentBounds.getHeight() - snapshot.boundsInDiagram.getHeight()));
				}

				// Update guide overlay
				if (guides.shouldUpdate()) {
					guides.update(sceneToDiagramTransform.transform(snapshot.sceneNode.getLocalToSceneTransform()
							.transform(snapshot.sceneNode.getLayoutBounds())));
				}

				// Adjust the position and size
				final Point2D currentPreferredPosition = PreferredPosition.get(snapshot.sceneNode);
				if (currentPreferredPosition == null || currentPreferredPosition.getX() != newPositionX
						|| currentPreferredPosition.getY() != newPositionY) {
					PreferredPosition.set(snapshot.sceneNode, new Point2D(newPositionX, newPositionY));

					if (showAffectedConnections) {
						final double dx = currentPreferredPosition == null ? 0
								: (newPositionX - currentPreferredPosition.getX());
						final double dy = currentPreferredPosition == null ? 0
								: (newPositionY - currentPreferredPosition.getY());
						shiftAffectedConnections(snapshot, dx, dy);
					}

					// Update side for docked shapes
					if (snapshot.sceneNode instanceof DockedShape) {
						final DockedShape ds = (DockedShape) snapshot.sceneNode;
						if (container instanceof ContainerShape) {
							final ContainerShape cs = (ContainerShape) container;
							final Bounds containerBounds = cs.getLayoutBounds();
							final DockSide side = GefAgeDiagramUtil
									.toDockSide(DockArea.fromDockingPosition(AgeDiagramUtil.determineDockingPosition(
											containerBounds.getWidth(), containerBounds.getHeight(), newPositionX,
											newPositionY, snapshot.boundsInDiagram.getWidth(),
											snapshot.boundsInDiagram.getHeight())));
							cs.addOrUpdateDockedChild(ds, side);
						}
					}
				}
			}
		}
	}

	/**
	 * Finalizes movement and updates the diagram to match. Should only be called once.
	 */
	public void apply() {
		// Move connection bendpoints
		if (!this.showAffectedConnections) {
			for (final DiagramElementSnapshot snapshot : elementsToMove) {
				final Point2D currentPreferredPosition = PreferredPosition.get(snapshot.sceneNode);
				if (currentPreferredPosition != null) {
					final double dx = currentPreferredPosition.getX() - snapshot.positionInLocal.getX();
					final double dy = currentPreferredPosition.getY() - snapshot.positionInLocal.getY();
					shiftAffectedConnections(snapshot, dx, dy);
				}
			}
		}

		// Move diagram elements by updating the diagram to reflect the current scene graph
		editor.getDiagram().modify("Move", m -> editor.getGefDiagram().updateDiagramFromSceneGraph());
	}

	private void shiftAffectedConnections(final DiagramElementSnapshot snapshot, final double dx, final double dy) {
		if (dx != 0.0 || dy != 0.0) {
			for (final DiagramElement affectedConnectionDiagramElement : snapshot.affectedConnections) {
				final Node affectedConnectionSceneNode = editor.getGefDiagram()
						.getSceneNode(affectedConnectionDiagramElement);
				// Shift regular (non flow-indicator) connections.
				if (affectedConnectionSceneNode instanceof ConnectionNode) {
					final BaseConnectionNode cn = (BaseConnectionNode) affectedConnectionSceneNode;
					cn.getInnerConnection()
							.setControlPoints(cn.getInnerConnection()
									.getControlPoints()
									.stream()
									.map(cp -> new org.eclipse.gef.geometry.planar.Point(cp.x + dx, cp.y + dy))
									.collect(Collectors.toList()));
				}
			}
		}
	}

	private void setAffectedConnectionsVisible(final boolean value) {
		for (final DiagramElementSnapshot snapshot : elementsToMove) {
			for (final DiagramElement affectedConnectionDiagramElement : snapshot.affectedConnections) {
				final Node affectedConnectionSceneNode = editor.getGefDiagram()
						.getSceneNode(affectedConnectionDiagramElement);
				if (affectedConnectionSceneNode != null) {
					affectedConnectionSceneNode.setVisible(value);
				}
			}
		}
	}

	/**
	 * Creates a list of snapshots for diagram elements which will be moved by the interaction.
	 * @param editor the editor containing the scene nodes for the elements.
	 * @return the snapshots
	 */
	private static List<DiagramElementSnapshot> createMoveElementSnapshotsForSelection(final AgeEditor editor) {
		final Set<DiagramElement> selectedDiagramElements = editor.getSelectedDiagramElementSet();
		final List<DiagramElementSnapshot> results = new ArrayList<>(selectedDiagramElements.size());
		addSnapshots(editor, editor.getDiagram(), selectedDiagramElements, results);
		return results;
	}

	/**
	 * Creates and adds snapshots for selected descendant elements to be moved to the specified result list.
	 * Does not create snapshots for descendants of elements which have snapshots created.
	 * @param editor the editor containing the diagram
	 * @param parent the node whose children will have snapshots created.
	 * @param selectedDiagramElements the currently selected diagram elements.
	 * @param results the list to which created snapshots are added.
	 */
	private static void addSnapshots(final AgeEditor editor, final DiagramNode parent,
			final Set<DiagramElement> selectedDiagramElements, final List<DiagramElementSnapshot> results) {
		for (final DiagramElement childDiagramElement : parent.getChildren()) {
			if (selectedDiagramElements.contains(childDiagramElement)
					&& DiagramElementPredicates.isMoveable(childDiagramElement)) {
				DiagramElementSnapshot.create(editor, childDiagramElement).ifPresent(results::add);
			} else {
				addSnapshots(editor, childDiagramElement, selectedDiagramElements, results);
			}
		}
	}
}