RenameInputEventHandler.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.Collections;
import java.util.Objects;

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.events.MenuDetectListener;
import org.osate.ge.businessobjecthandling.GetNameContext;
import org.osate.ge.gef.LabelNode;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.ui.editor.EditorRenameUtil;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
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.text.Text;
import javafx.scene.transform.Transform;

/**
 *  {@link InputEventHandler} which handles renaming when a primary label is clicked.
 *  When a label for a selected element is clicked, renaming will be activated when the mouse button is released.
 */
public class RenameInputEventHandler implements InputEventHandler {
	private final AgeEditor editor;
	private boolean wasSelected = false;
	private DiagramElement mousePressDiagramElement = null;

	/**
	 * Creates a new instance
	 * @param editor the editor from which events originate.
	 */
	public RenameInputEventHandler(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) {
		// Only handle primary mouse button releases
		if (e.getEventType() != MouseEvent.MOUSE_PRESSED && e.getEventType() != MouseEvent.MOUSE_RELEASED) {
			return null;
		}

		final MouseEvent mouseEvent = (MouseEvent) e;
		if (mouseEvent.getButton() != MouseButton.PRIMARY) {
			return null;
		}

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

		if (e.getEventType() == MouseEvent.MOUSE_PRESSED) {
			wasSelected = editor.getSelectedDiagramElements().contains(clickedDiagramElement);
			mousePressDiagramElement = clickedDiagramElement;
		} else if (e.getEventType() == MouseEvent.MOUSE_RELEASED && mouseEvent.getButton() == MouseButton.PRIMARY
				&& !mouseEvent.isShiftDown() && !mouseEvent.isControlDown()
				&& wasSelected && clickedDiagramElement == mousePressDiagramElement
				&& editor.getSelectedDiagramElements().contains(clickedDiagramElement)) {
			final LabelNode primaryLabel = editor.getGefDiagram().getPrimaryLabelSceneNode(clickedDiagramElement);
			if (isAncestor(primaryLabel, (Node) e.getTarget()) && EditorRenameUtil.canRename(clickedDiagramElement)) {
				final RenameInteraction newInteraction = new RenameInteraction(clickedDiagramElement, primaryLabel,
						editor);
				return HandledEvent.newInteraction(newInteraction);
			}

			return HandledEvent.consumed();
		}

		return null;
	}

	/**
	 * Returns true if potential ancestor is an ancestor of other.
	 * @param potentialAncestor the node which is being checked to determine if it is an ancestor.
	 * @param other the potential descendant node
	 * @return true if the potential ancestor is an ancestor of the other node.
	 */
	private static boolean isAncestor(final Node potentialAncestor, final Node other) {
		if (potentialAncestor == null) {
			return false;
		}

		for (Node tmp = other; tmp != null; tmp = tmp.getParent()) {
			if (tmp == potentialAncestor) {
				return true;
			}
		}
		return false;
	}
}

/**
 * Interaction which renames a diagram element using a text field shown over the diagram.
 *
 */
class RenameInteraction extends BaseInteraction {
	private static final String ERROR_STYLE = "-fx-control-inner-background: #FBE9EB; -fx-text-fill: #C90017;";

	private final AgeEditor editor;
	private final DiagramElement diagramElement;
	private final TextField editField;
	private final ChangeListener<Transform> labelTransformChangeListener;
	private final String originalName;

	/**
	 * {@link MenuDetectListener} which consumes the event to avoid showing the context menu. Avoids showing the
	 * editor's context menu when interaction is active. Otherwise, multiple context menus will be shown when
	 * right-clicking on {@link #editField}
	 */
	private static final MenuDetectListener supressSwtMenu = e -> e.doit = false;

	/**
	 * Text node used to measure the text size off-screen so that {@link #editField} can be sized appropriately.
	 */
	private Text measuringText = new Text();

	/**
	 * Creates a new instance
	 * @param diagramElement the diagram element being renamed
	 * @param primaryLabel the primary label for the diagram element
	 * @param editor the editor
	 */
	public RenameInteraction(final DiagramElement diagramElement, final LabelNode primaryLabel,
			final AgeEditor editor) {
		// Add the measuring text to a scene so that sizes can be calculated.
		new Scene(new Group(measuringText));

		this.editor = Objects.requireNonNull(editor, "editor must not be null");
		this.diagramElement = Objects.requireNonNull(diagramElement, "diagramElement must not be null");
		this.originalName = diagramElement.getBusinessObjectHandler()
				.getNameForRenaming(new GetNameContext(diagramElement.getBusinessObject()));
		this.editField = new TextField(originalName);
		editor.getOverlays().getChildren().add(editField);

		// Avoid showing the editor's context menu while the interaction is active
		editor.getFxCanvas().addMenuDetectListener(supressSwtMenu);

		// Listen for changes to the label's transform
		this.labelTransformChangeListener = (o, oldValue, newValue) -> layoutEditField(primaryLabel, editField);
		primaryLabel.localToSceneTransformProperty()
				.addListener(new WeakChangeListener<>(labelTransformChangeListener));

		// Set the edit field's position
		editField.applyCss();
		layoutEditField(primaryLabel, editField);

		// Update field size when the contents change
		editField.textProperty().addListener((o, oldValue, newValue) -> {
			editField.resize(calculateWidth(), editField.getHeight());

			// Validate the new value
			if (EditorRenameUtil.validateName(diagramElement, newValue) == null) {
				editField.setStyle("");
			} else {
				editField.setStyle(ERROR_STYLE);
			}
		});
		editField.requestFocus();
	}

	private double calculateWidth() {
		measuringText.setText(editField.getText() + "WW");
		measuringText.applyCss();
		return measuringText.getLayoutBounds().getWidth();
	}

	@Override
	public void close() {
		// Check if the name was changed
		final String newName = editField.getText();
		if (!Objects.equals(originalName, newName)) {
			// Check if the new name is valid
			final String validationError = EditorRenameUtil.validateName(diagramElement, newName);
			if (validationError == null) {
				// Rename the business object
				EditorRenameUtil.rename(diagramElement, newName, editor);
			} else {
				// Show an error
				MessageDialog.openError(editor.getEditorSite().getShell(), "Unable to edit value", validationError);
			}
		}

		// Re-enable the SWT context menu
		editor.getFxCanvas().removeMenuDetectListener(supressSwtMenu);

		// Remove the edit field
		((Group) editField.getParent()).getChildren().remove(editField);
	}

	@Override
	protected Interaction.InteractionState onMousePressed(final MouseEvent e) {
		if (e.getTarget() instanceof Node) {
			for (Node tmp = (Node) e.getTarget(); tmp != null; tmp = tmp.getParent()) {
				if (tmp == editField) {
					return InteractionState.IN_PROGRESS;
				}
			}
		}

		// Select the clicked element and deactivate direct editing
		final DiagramElement clickedDiagramElement = InputEventHandlerUtil
				.getTargetDiagramElement(editor.getGefDiagram(), e.getTarget());
		if (clickedDiagramElement != null) {
			editor.selectDiagramNodes(Collections.singletonList(clickedDiagramElement));
		}

		return InteractionState.COMPLETE;
	}

	@Override
	protected Interaction.InteractionState onKeyPressed(final KeyEvent e) {
		if (e.getCode() == KeyCode.ENTER) {
			return InteractionState.COMPLETE;
		} else if (e.getCode() == KeyCode.ESCAPE) {
			// Revert changes so name will not be saved
			editField.setText(originalName);
			return InteractionState.COMPLETE;
		}

		return super.onKeyPressed(e);
	}

	private void layoutEditField(final LabelNode label, final TextField editField) {
		// Determine the size of the edit field
		final double width = calculateWidth();
		final double height = editField.prefHeight(-1);
		editField.resize(width, height);

		// Position the edit field so it is centered around the label
		final Bounds labelBoundsInOverlay = editor.getOverlays()
				.sceneToLocal(label.getLocalToSceneTransform().transform(label.getBoundsInLocal()));
		editField.setLayoutX(labelBoundsInOverlay.getMinX() + (labelBoundsInOverlay.getWidth() - width) / 2.0);
		editField.setLayoutY(labelBoundsInOverlay.getMinY() + (labelBoundsInOverlay.getHeight() - height) / 2.0);
	}
}