MarqueeSelectInputEventHandler.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 org.osate.ge.gef.ContainerShape;
import org.osate.ge.gef.DockedShape;
import org.osate.ge.gef.ui.editor.overlays.Handle;
import org.osate.ge.gef.ui.editor.overlays.Overlays;
import org.osate.ge.internal.diagram.runtime.DiagramElement;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.input.InputEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeType;
import javafx.scene.transform.Transform;

/**
 *  {@link InputEventHandler} for marquee selection. The marquee selection allows selecting {@link ContainerShape} and {@link DockedShape} nodes.
 *  Connections cannot be selected using the marquee tool.
 */
public class MarqueeSelectInputEventHandler implements InputEventHandler {
	private final AgeEditor editor;

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

	@Override
	public Cursor getCursor(final MouseEvent mouseMoveEvent) {
		return editor.getPaletteModel().isMarqueeToolActive()
				&& !InputEventHandlerUtil.isScrollBar(mouseMoveEvent.getTarget())
				? Cursor.CROSSHAIR
				: null;
	}

	@Override
	public HandledEvent handleEvent(final InputEvent e) {
		if (e.getEventType() != MouseEvent.MOUSE_PRESSED || ((MouseEvent) e).getButton() != MouseButton.PRIMARY
				|| InputEventHandlerUtil.isScrollBar(e.getTarget())) {
			return null;
		}

		final AgeEditorPaletteModel paletteModel = editor.getPaletteModel();
		final MouseEvent mouseEvent = (MouseEvent) e;
		final DiagramElement clickedDiagramElement = InputEventHandlerUtil
				.getTargetDiagramElement(editor.getGefDiagram(), e.getTarget());
		if (paletteModel.isMarqueeToolActive() || (paletteModel.isSelectToolActive() && clickedDiagramElement == null
				&& !(e.getTarget() instanceof Handle))) {
			final MarqueeSelectInteraction newInteraction = new MarqueeSelectInteraction(editor, mouseEvent);
			return HandledEvent.newInteraction(newInteraction);
		} else {
			return null;
		}
	}
}

/**
 * Interaction for marquee selection
 *
 */
class MarqueeSelectInteraction extends BaseInteraction {
	private final AgeEditor editor;

	/**
	 * Start of the selection in diagram coordinates.
	 */
	private final Point2D selectionStart;

	private final Rectangle selectionBoundsOverlay = new Rectangle();

	private final ChangeListener<Transform> diagramToOverlayChangeListener = (o, oldValue,
			newValue) -> selectionBoundsOverlay.getTransforms().setAll(newValue);

	/**
	 * Creates a new instance
	 * @param editor the editor for the interaction
	 * @param e the mouse pressed event that triggered the interaction
	 */
	public MarqueeSelectInteraction(final AgeEditor editor, final MouseEvent e) {
		this.editor = editor;
		this.selectionStart = editor.getGefDiagram().getSceneNode().getSceneToLocalTransform().transform(e.getSceneX(),
				e.getSceneY());

		// Update transforms so that the local coordinate system matches diagram coordinates
		final Overlays overlays = editor.getOverlays();
		overlays.diagramToLocalTransformProperty().addListener(new WeakChangeListener<>(diagramToOverlayChangeListener));
		selectionBoundsOverlay.getTransforms().setAll(overlays.diagramToLocalTransformProperty().get());

		// Create the overlay node which indicates the selection bounds
		selectionBoundsOverlay.setX(selectionStart.getX());
		selectionBoundsOverlay.setY(selectionStart.getY());
		selectionBoundsOverlay.setFill(null);
		selectionBoundsOverlay.setStrokeLineCap(StrokeLineCap.BUTT);
		selectionBoundsOverlay.setStroke(Color.BLACK);
		selectionBoundsOverlay.setStrokeType(StrokeType.INSIDE);
		selectionBoundsOverlay.setStrokeWidth(1.0);
		selectionBoundsOverlay.getStrokeDashArray().setAll(4.0, 3.0);

		overlays.getChildren().add(selectionBoundsOverlay);
	}

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

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

		// Transform the cursor position to diagram coordinates and update the bounds overlay
		final Point2D p = editor.getGefDiagram().getSceneNode().getSceneToLocalTransform().transform(e.getSceneX(),
				e.getSceneY());
		if (p.getX() < selectionStart.getX()) {
			selectionBoundsOverlay.setX(p.getX());
			selectionBoundsOverlay.setWidth(selectionStart.getX() - p.getX());
		} else {
			selectionBoundsOverlay.setX(selectionStart.getX());
			selectionBoundsOverlay.setWidth(p.getX() - selectionStart.getX());
		}

		if (p.getY() < selectionStart.getY()) {
			selectionBoundsOverlay.setY(p.getY());
			selectionBoundsOverlay.setHeight(selectionStart.getY() - p.getY());
		} else {
			selectionBoundsOverlay.setY(selectionStart.getY());
			selectionBoundsOverlay.setHeight(p.getY() - selectionStart.getY());
		}

		return InteractionState.IN_PROGRESS;
	}

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

		// Select container and docked shapes
		final Bounds selectionBounds = selectionBoundsOverlay.getLocalToSceneTransform()
				.transform(selectionBoundsOverlay.getBoundsInLocal());
		final List<DiagramElement> newSelection = new ArrayList<>();
		addChildrenToNewSelection(editor.getGefDiagram().getSceneNode(), selectionBounds, newSelection);
		editor.selectDiagramNodes(newSelection);

		return InteractionState.COMPLETE;
	}

	/**
	 * Adds diagram elements to the list of diagram elements to be selected based on the children of the specified node.
	 * @param parent the scene graph node for which children will be checked.
	 * @param selectionBounds the bounds in scene coordinates which is being selected.
	 * @param newSelection the list of diagram elements to be selected. If an appropriate child is in the selection bounds, the diagram element
	 * for it will be added to this list.
	 */
	private void addChildrenToNewSelection(final Parent parent, final Bounds selectionBounds,
			final List<DiagramElement> newSelection) {
		for (final Node child : parent.getChildrenUnmodifiable()) {
			final Bounds childBounds = child.getLocalToSceneTransform().transform(child.getBoundsInLocal());

			// Only container and docked shapes are selectable using the marquee tool
			if ((child instanceof ContainerShape || child instanceof DockedShape)
					&& selectionBounds.contains(childBounds)) {
				// Ignore objects which do not have an associated diagram element
				final DiagramElement childDiagramElement = editor.getGefDiagram().getDiagramElement(child);
				if (childDiagramElement != null) {
					newSelection.add(childDiagramElement);
				}
			} else if (child instanceof Parent && selectionBounds.intersects(childBounds)) {
				addChildrenToNewSelection((Parent) child, selectionBounds, newSelection);
			}
		}
	}
}