AlignmentHelper.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.internal.ui.handlers;
import java.util.List;
import java.util.stream.Collectors;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.graphics.Dimension;
import org.osate.ge.graphics.Point;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramModification;
import org.osate.ge.internal.diagram.runtime.DockArea;
import org.osate.ge.internal.diagram.runtime.layout.DiagramElementLayoutUtil;
/**
* Helper class for alignment handlers
*
*/
class AlignmentHelper {
private final Axis axis;
/**
* Private constructor to prevent direct instantiation.
* @param axis the axis on which this instance aligns elements.
* @see #create()
*/
private AlignmentHelper(final Axis axis) {
this.axis = axis;
}
/**
* Creates an instance for the horizontal axis
* @return an instance for the horizontal axis
*/
public static AlignmentHelper createHorizontal() {
return new AlignmentHelper(HORIZONTAL_AXIS);
}
/**
* Creates an instance for the vertical axis
* @return an instance for the vertical axis
*/
public static AlignmentHelper createVertical() {
return new AlignmentHelper(VERTICAL_AXIS);
}
/**
* Aligns the specified element to the specified location
* @param m the diagram modification to use to modify the diagram
* @param alignmentElement the diagram element to align
* @param alignLocation the absolute position along the axis to which align the element.
* @param elementOffset the offset from the alignment element's position to the position of the element to align.
*/
public void alignElement(
final DiagramModification m,
AlignmentElement alignmentElement, final double alignLocation,
final double elementOffset) {
// Alignment location for element relative to diagram
double newLocation = alignLocation - elementOffset;
// Check if new shape location fits within parent(s)
while (alignmentElement.getDiagramElement().getParent() instanceof DiagramElement) {
final DiagramElement parentDe = (DiagramElement) alignmentElement.getDiagramElement().getParent();
final double parentAbsoluteLocation = axis.getParentAbsoluteLocation(alignmentElement);
// If new location is to the left of parent for horizontal alignment or above parent for vertical alignment,
// parent will have to shift and resize
if (parentAbsoluteLocation > newLocation) {
// Amount that the children need to shift to stay in same place after parent resize
final double childOffset = parentAbsoluteLocation - newLocation;
shiftChildren(m, parentDe, childOffset);
// Move shape to top or left edge of parent depending on axis alignment
final DiagramElement de = alignmentElement.getDiagramElement();
DiagramElementLayoutUtil.moveElement(m, de, axis.getEdgeLocation(de), false, true);
// Set parent size to accommodate for the new alignment element location
m.setSize(parentDe, axis.getParentSize(parentDe, childOffset));
// The element ancestor that will be moving to the alignment location
alignmentElement = new AlignmentElement(parentDe, parentAbsoluteLocation);
} else {
break;
}
}
final DiagramElement de = alignmentElement.getDiagramElement();
if (de.getParent() instanceof DiagramElement) {
// Set location relative to parent
newLocation = axis.getLocationRelativeToParent(de,
alignmentElement.getAbsoluteLocation() - alignLocation) - elementOffset;
// Ports cannot overlap, check if any collide and shift if necessary
if (de.getDockArea() != null) {
shiftCollidingPorts(m, de, newLocation);
}
}
// Set the element new location and update bendpoints
DiagramElementLayoutUtil.moveElement(m, de, axis.getAlignmentPosition(de, newLocation), false, true);
}
private void shiftCollidingPorts(final DiagramModification m,
final DiagramElement de, final double newLocation) {
// Check for colliding ports
for (final BusinessObjectContext q : de.getParent().getChildren()) {
if (q instanceof DiagramElement && ((DiagramElement) q).getDockArea() == de.getDockArea()) {
final DiagramElement dockedChild = (DiagramElement) q;
if (axis.isPortCollision(dockedChild, newLocation)) {
// Adjust colliding port
DiagramElementLayoutUtil.moveElement(
m,
dockedChild,
axis.getNewPortLocation(dockedChild, newLocation + 1), false, true);
break;
}
}
}
}
// Shift eligible children
private void shiftChildren(final DiagramModification m,
final DiagramElement parentDe, final double childOffset) {
for (final BusinessObjectContext q : parentDe.getChildren()) {
if (q instanceof DiagramElement && axis.isValidDockArea(((DiagramElement) q).getDockArea())) {
final DiagramElement childDe = (DiagramElement) q;
DiagramElementLayoutUtil.moveElement(m, childDe, axis.getShiftPostion(childDe, childOffset),
false,
true);
}
}
}
/**
* Returns whether the alignment command should be enabled. Any selected element must not be a descendant of any other selected element.
* Must be docked to appropriate area
* @return whether the alignment command should be enabled
*/
public boolean getEnabled() {
final List<DiagramElement> selectedElements = AgeHandlerUtil.getSelectedDiagramElements();
// More than one diagram elements must be selected
if (selectedElements.size() < 2) {
return false;
}
for (final DiagramElement de : selectedElements) {
if (!axis.isValidDockArea(de.getDockArea()) || isAncestorSelected(de, selectedElements)) {
return false;
}
}
final List<DiagramElement> dockedElements = selectedElements.stream().filter(de -> de.getDockArea() != null)
.collect(Collectors.toList());
if (dockedElements.size() > 1) {
return validDockedElement(dockedElements);
}
return true;
}
// Docked elements must be docked to opposite sides if they have the same parent
private static boolean validDockedElement(final List<DiagramElement> dockedElements) {
for (int i = 0; i < (dockedElements.size() - 1); i++) {
final DiagramElement de = dockedElements.get(i);
if (hasSameParentAndDockArea(dockedElements, de, i)) {
return false;
}
}
return true;
}
private static boolean hasSameParentAndDockArea(final List<DiagramElement> dockedElements, final DiagramElement de,
int i) {
while (i < (dockedElements.size() - 1)) {
final DiagramElement tmp = dockedElements.get(++i);
if (de.getDockArea() == tmp.getDockArea() && de.getParent() == tmp.getParent()) {
return true;
}
}
return false;
}
private static boolean isAncestorSelected(final DiagramElement de, final List<DiagramElement> selectedElements) {
BusinessObjectContext parent = de.getParent();
while (parent != null) {
if (selectedElements.contains(parent)) {
return true;
}
parent = parent.getParent();
}
return false;
}
/**
* Returns the first alignment element in the specified collection. This is the alignment element that other elements will align with
* @param elements the alignment elements
* @return the first alignment element in the specified collection.
*/
public static AlignmentElement getPrimaryAlignmentElement(final List<AlignmentElement> elements) {
if (elements.size() == 0) {
return null;
}
return elements.get(elements.size() - 1);
}
/**
* Contains a diagram element and alignment position
*
*/
public static class AlignmentElement {
private final DiagramElement de;
private double absoluteLocation; // Element's location relative to the diagram
private AlignmentElement(final DiagramElement de, final double absoluteLocation) {
this.de = de;
this.absoluteLocation = absoluteLocation;
}
/**
* Returns the position along the axis
* @return the position along the axis
*/
public double getAbsoluteLocation() {
return absoluteLocation;
}
/**
* Returns the diagram element for which this instance contains a location
* @return diagram element
*/
public DiagramElement getDiagramElement() {
return de;
}
}
/**
* Helper methods for aligning elements on an axis
*/
private interface Axis {
Dimension getParentSize(final DiagramElement de, final double offset);
Point getShiftPostion(final DiagramElement de, final double offset);
double getLocationRelativeToParent(final DiagramElement de, final double alignLocation);
double getParentAbsoluteLocation(final AlignmentElement alignmentElement);
Point getEdgeLocation(final DiagramElement de);
Point getAlignmentPosition(final DiagramElement de, final double newLoc);
double getAxisLocation(final DiagramElement de);
boolean isPortCollision(final DiagramElement dockedChild, final double location);
Point getNewPortLocation(final DiagramElement dockedChild, final double location);
boolean isValidDockArea(DockArea dockArea);
}
/**
* Horizontal axis
*/
private static final Axis HORIZONTAL_AXIS = new Axis() {
@Override
public Dimension getParentSize(final DiagramElement de, final double offset) {
return new Dimension(de.getWidth() + offset, de.getHeight());
}
@Override
public Point getShiftPostion(final DiagramElement de, final double offset) {
return new Point(de.getX() + offset, de.getY());
}
@Override
public double getLocationRelativeToParent(final DiagramElement de, final double alignLocation) {
return de.getX() - alignLocation;
}
@Override
public double getParentAbsoluteLocation(final AlignmentElement alignmentElement) {
return alignmentElement.getAbsoluteLocation() - alignmentElement.getDiagramElement().getX();
}
@Override
public Point getEdgeLocation(final DiagramElement de) {
return new Point(0, de.getY());
}
@Override
public Point getAlignmentPosition(final DiagramElement de, final double newLoc) {
return new Point(newLoc, de.getY());
}
@Override
public double getAxisLocation(final DiagramElement de) {
return de.getX();
}
@Override
public Point getNewPortLocation(final DiagramElement dockedChild, final double location) {
return new Point(location + 1, dockedChild.getY());
}
@Override
public boolean isPortCollision(final DiagramElement dockedChild, final double location) {
return location >= dockedChild.getX()
&& location <= dockedChild.getX() + dockedChild.getWidth();
}
@Override
public boolean isValidDockArea(DockArea dockArea) {
return dockArea == null || dockArea == DockArea.TOP || dockArea == DockArea.BOTTOM;
}
};
/**
* Vertical axis
*/
private static final Axis VERTICAL_AXIS = new Axis() {
@Override
public Dimension getParentSize(final DiagramElement de, final double offset) {
return new Dimension(de.getWidth(), de.getHeight() + offset);
}
@Override
public Point getShiftPostion(final DiagramElement de, final double offset) {
return new Point(de.getX(), de.getY() + offset);
}
@Override
public double getLocationRelativeToParent(final DiagramElement de, final double alignLocation) {
return de.getY() - alignLocation;
}
@Override
public double getParentAbsoluteLocation(final AlignmentElement alignmentElement) {
return alignmentElement.getAbsoluteLocation() - alignmentElement.getDiagramElement().getY();
}
@Override
public Point getEdgeLocation(final DiagramElement de) {
return new Point(de.getX(), 0);
}
@Override
public Point getAlignmentPosition(final DiagramElement de, final double newLoc) {
return new Point(de.getX(), newLoc);
}
@Override
public double getAxisLocation(final DiagramElement de) {
return de.getY();
}
@Override
public Point getNewPortLocation(final DiagramElement dockedChild, final double location) {
return new Point(dockedChild.getX(), location);
}
@Override
public boolean isPortCollision(final DiagramElement dockedChild, final double location) {
return location >= dockedChild.getY()
&& location <= dockedChild.getY() + dockedChild.getHeight();
}
@Override
public boolean isValidDockArea(DockArea dockArea) {
return dockArea == null || dockArea == DockArea.LEFT || dockArea == DockArea.RIGHT;
}
};
/**
* Creates an {@link AlignmentElement} for the specified diagram element
* @param de the diagram element for which an {@link AlignmentElement} will be created
* @return the new {@link AlignmentElement} instance
*/
public AlignmentElement createAlignmentElement(final DiagramElement de) {
if (de == null) {
return null;
}
return new AlignmentElement(de, getDiagramAbsoluteLocation(de, axis));
}
private static double getDiagramAbsoluteLocation(DiagramElement de, final Axis axis) {
double loc = axis.getAxisLocation(de);
while (de.getParent() instanceof DiagramElement) {
de = (DiagramElement) de.getParent();
loc += axis.getAxisLocation(de);
}
return loc;
}
}