PaletteCommandInputEventHandler.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.Objects;
import java.util.Optional;

import org.eclipse.gef.fx.anchors.IAnchor;
import org.eclipse.gef.fx.anchors.StaticAnchor;
import org.eclipse.gef.fx.nodes.Connection;
import org.eclipse.gef.fx.nodes.GeometryNode;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.DockingPosition;
import org.osate.ge.gef.AgeGefRuntimeException;
import org.osate.ge.gef.ui.diagram.GefAgeDiagramUtil;
import org.osate.ge.gef.ui.editor.overlays.Overlays;
import org.osate.ge.internal.diagram.runtime.AgeDiagramUtil;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.operations.OperationExecutor;
import org.osate.ge.internal.services.ActionExecutor.ExecutionMode;
import org.osate.ge.internal.services.AgeAction;
import org.osate.ge.internal.ui.OperationResultsProcessor;
import org.osate.ge.operations.Operation;
import org.osate.ge.palette.CanStartConnectionContext;
import org.osate.ge.palette.CreateConnectionPaletteCommand;
import org.osate.ge.palette.GetCreateConnectionOperationContext;
import org.osate.ge.palette.GetTargetedOperationContext;
import org.osate.ge.palette.PaletteCommand;
import org.osate.ge.palette.TargetedPaletteCommand;

import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.input.InputEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.transform.NonInvertibleTransformException;

/**
 * {@link InputEventHandler} for executing palette commands.
 */
public class PaletteCommandInputEventHandler implements InputEventHandler {
	private final AgeEditor editor;

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

	@Override
	public Cursor getCursor(final MouseEvent mouseMoveEvent) {
		final PaletteCommand cmd = editor.getPaletteModel().getActivePaletteCommand();
		if (cmd instanceof TargetedPaletteCommand) {
			return getTargetedCommandOperation(mouseMoveEvent).isPresent() ? Cursors.CREATE : Cursors.NO;
		} else if (cmd instanceof CreateConnectionPaletteCommand) {
			final CreateConnectionPaletteCommand createCmd = (CreateConnectionPaletteCommand) cmd;
			final boolean canCreate = createCanStartConnectionContext(mouseMoveEvent).map(createCmd::canStartConnection)
					.orElse(false);
			return canCreate ? Cursors.PLUG : Cursors.PLUG_NO;
		}
		return null;
	}

	@Override
	public HandledEvent handleEvent(final InputEvent e) {
		if (e.getEventType() != MouseEvent.MOUSE_PRESSED) {
			return null;
		}

		// Retrieve the active palette command
		final PaletteCommand cmd = editor.getPaletteModel().getActivePaletteCommand();
		if (cmd == null) {
			return null;
		}

		final MouseEvent me = (MouseEvent) e;

		if (me.getButton() == MouseButton.PRIMARY) {
			if (cmd instanceof TargetedPaletteCommand) {
				final TargetedPaletteCommand tc = (TargetedPaletteCommand) cmd;
				createGetTargetedOperationContext((MouseEvent) e).ifPresent(c -> {
					final Node sceneNode = editor.getSceneNode((DiagramNode) c.getTarget());
					final Point2D p = getTargetPosition(sceneNode, me.getSceneX(), me.getSceneY());

					class CreateAction implements AgeAction {
						@Override
						public AgeAction execute() {
							final DiagramNode targetNode = (DiagramNode) c.getTarget();
							tc.getOperation(c).ifPresent(operation -> {
								// Perform modification
								final OperationExecutor opExecutor = new OperationExecutor(
										editor.getAadlModificationService(), editor.getReferenceService());
								OperationResultsProcessor.processResults(editor, targetNode,
										GefAgeDiagramUtil.toAgePoint(p), opExecutor.execute(operation));
							});

							return null;
						}
					}

					final CreateAction createAction = new CreateAction();
					editor.getActionExecutor().execute("Create " + cmd.getLabel(), ExecutionMode.NORMAL, createAction);

					// Deactivate the current palette item and select the "Select" item
					editor.getPaletteModel().deactivateNonSelectItem();
				});

				return HandledEvent.consumed();
			} else if (cmd instanceof CreateConnectionPaletteCommand) {
				final CreateConnectionPaletteCommand createCmd = (CreateConnectionPaletteCommand) cmd;
				final CanStartConnectionContext ctx = createCanStartConnectionContext(me).orElse(null);
				if (ctx == null || !createCmd.canStartConnection(ctx)) {
					return null;
				}

				return HandledEvent.newInteraction(
						new CreateConnectionInteraction(createCmd, (DiagramElement) ctx.getSource(), editor, me));
			}
		} else if (me.getButton() == MouseButton.SECONDARY) {
			editor.getPaletteModel().deactivateNonSelectItem();
		}

		return null;
	}

	private Optional<CanStartConnectionContext> createCanStartConnectionContext(final MouseEvent event) {
		final DiagramElement sourceDiagramElement = InputEventHandlerUtil
				.getTargetDiagramElement(editor.getGefDiagram(), event.getTarget());
		return sourceDiagramElement == null ? Optional.empty()
				: Optional.of(new CanStartConnectionContext(sourceDiagramElement, editor.getQueryService()));
	}

	private Optional<GetTargetedOperationContext> createGetTargetedOperationContext(final MouseEvent event) {
		// Find the closest diagram node
		final DiagramNode targetDiagramNode = InputEventHandlerUtil.getTargetDiagramNode(editor.getGefDiagram(),
				event.getTarget());
		if (targetDiagramNode == null) {
			return Optional.empty();
		}

		final Node targetSceneNode = editor.getGefDiagram().getSceneNode(targetDiagramNode);
		final Point2D targetPosition = getTargetPosition(targetSceneNode, event.getSceneX(), event.getSceneY());
		final DockingPosition dockingPostion = AgeDiagramUtil.determineDockingPosition(targetDiagramNode,
				targetPosition.getX(), targetPosition.getY(), 0, 0);
		return Optional
				.of(new GetTargetedOperationContext(targetDiagramNode, dockingPostion, editor.getQueryService()));
	}

	/**
	 * Gets the operation for the active {@link TargetedPaletteCommand} based on the current moue event.
	 * @param event the latest mouse even which contains the target.
	 * @return an empty optional if the operation could not be retrieved or the current command is not a {@link TargetedPaletteCommand}
	 */
	private Optional<Operation> getTargetedCommandOperation(final MouseEvent event) {
		final PaletteCommand cmd = editor.getPaletteModel().getActivePaletteCommand();
		if (cmd instanceof TargetedPaletteCommand) {
			final TargetedPaletteCommand tc = (TargetedPaletteCommand) cmd;
			return createGetTargetedOperationContext(event).flatMap(tc::getOperation);
		} else {
			return Optional.empty();
		}
	}

	/**
	 * Gets the position relative to the specified target scene node
	 * @param targetSceneNode the node to which the returned position is relative
	 * @param sceneX the X value in the scene coordinate system
	 * @param sceneY the Y value in the scene coordinate system
	 * @return the position relative to the target scene node
	 */
	private static Point2D getTargetPosition(final Node targetSceneNode, final double sceneX, final double sceneY) {
		try {
			return targetSceneNode.getLocalToSceneTransform().createInverse().transform(sceneX, sceneY);
		} catch (final NonInvertibleTransformException e) {
			throw new AgeGefRuntimeException("Unable to get scene to local transform for target scene node", e);
		}
	}
}

/**
 * An interaction which creates a connection using a {@link CreateConnectionPaletteCommand}
 *
 */
class CreateConnectionInteraction extends BaseInteraction {
	private static final Color CONNECTION_COLOR = new Color(1.0, 0.518, 0.0, 1.0);

	private final CreateConnectionPaletteCommand cmd;
	private final BusinessObjectContext sourceBoc;
	private final AgeEditor editor;
	private Cursor cursor = null;

	/**
	 * Visualization for connection being created
	 */
	private final Connection connection;
	private final StaticAnchor mouseAnchor;

	/**
	 * Creates a new instance
	 * @param cmd the palette command to use to create the connection
	 * @param sourceDiagramElement the start of the connection
	 * @param editor the editor containing the diagram being edited
	 * @param e the mouse pressed event that started the interaction
	 */
	public CreateConnectionInteraction(final CreateConnectionPaletteCommand cmd,
			final DiagramElement sourceDiagramElement, final AgeEditor editor, final MouseEvent e) {
		this.cmd = Objects.requireNonNull(cmd, "cmd must not be null");
		this.sourceBoc = Objects.requireNonNull(sourceDiagramElement, "sourceDiagramElement must not be null");
		this.editor = Objects.requireNonNull(editor, "editor must not be null");

		// Create connection for visualizing the connection being created
		connection = new Connection();
		connection.setMouseTransparent(true);
		final IAnchor sourceAnchor = GefAgeDiagramUtil.getAnchor(editor.getGefDiagram(), sourceDiagramElement, null);
		if (sourceAnchor == null) {
			throw new AgeGefRuntimeException("Unable to get anchor");
		}
		connection.setStartAnchor(sourceAnchor);

		// Set end anchor
		final Overlays overlays = editor.getOverlays();
		final Point2D mousePosition = overlays.sceneToLocal(e.getSceneX(), e.getSceneY());
		mouseAnchor = new StaticAnchor(overlays,
				new org.eclipse.gef.geometry.planar.Point(mousePosition.getX(), mousePosition.getY()));
		connection.setEndAnchor(mouseAnchor);

		// Set style of connection
		final double zoom = editor.getZoom();
		final GeometryNode<?> gn = (GeometryNode<?>) connection.getCurve();
		gn.setStrokeLineCap(StrokeLineCap.BUTT);
		gn.setStrokeWidth(2.0 * zoom);
		gn.getStrokeDashArray().setAll(6.0 * zoom, 2.0 * zoom);
		gn.setStroke(CONNECTION_COLOR);
		overlays.getChildren().add(connection);
	}

	@Override
	public void close() {
		((Group) connection.getParent()).getChildren().remove(connection);
	}

	@Override
	public final Cursor getCursor() {
		return cursor;
	}

	@Override
	protected Interaction.InteractionState onMouseMoved(final MouseEvent e) {
		updateMouseAnchorPosition(e);
		this.cursor = createGetCreateConnectionOperationContext(e).flatMap(cmd::getOperation).isPresent() ? Cursors.PLUG
				: Cursors.PLUG_NO;

		return InteractionState.IN_PROGRESS;
	}

	@Override
	protected Interaction.InteractionState onMouseDragged(final MouseEvent e) {
		updateMouseAnchorPosition(e);
		return InteractionState.IN_PROGRESS;
	}

	private void updateMouseAnchorPosition(final MouseEvent e) {
		final Point2D overlayPosition = editor.getOverlays().sceneToLocal(e.getSceneX(), e.getSceneY());
		mouseAnchor.setReferencePosition(
				new org.eclipse.gef.geometry.planar.Point(overlayPosition.getX(), overlayPosition.getY()));
	}

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

		// Get and execute the operation for creating the connection
		createGetCreateConnectionOperationContext(e).ifPresent(c -> {
			class CreateAction implements AgeAction {
				@Override
				public AgeAction execute() {
					cmd.getOperation(c).ifPresent(operation -> {
						// Perform modification
						final OperationExecutor opExecutor = new OperationExecutor(editor.getAadlModificationService(),
								editor.getReferenceService());
						OperationResultsProcessor.processResults(editor, opExecutor.execute(operation));
					});

					return null;
				}
			}

			final CreateAction createAction = new CreateAction();
			editor.getActionExecutor().execute("Create " + cmd.getLabel(), ExecutionMode.NORMAL, createAction);
		});

		return InteractionState.COMPLETE;
	}

	private Optional<GetCreateConnectionOperationContext> createGetCreateConnectionOperationContext(
			final MouseEvent event) {
		final DiagramElement destinationDiagramElement = InputEventHandlerUtil
				.getTargetDiagramElement(editor.getGefDiagram(), event.getTarget());
		if (destinationDiagramElement == null) {
			return Optional.empty();
		}
		return Optional.of(new GetCreateConnectionOperationContext(sourceBoc, destinationDiagramElement,
				editor.getQueryService()));
	}
}