blob: ba10c9bfd702b16e370ac51f5f27ad0e3fd3afc9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2019 THALES GLOBAL SERVICES and others.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.diagram.ui.tools.api.layout.provider;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.Request;
import org.eclipse.gef.commands.Command;
import org.eclipse.gef.commands.CompoundCommand;
import org.eclipse.gef.commands.UnexecutableCommand;
import org.eclipse.gef.requests.ChangeBoundsRequest;
import org.eclipse.gmf.runtime.common.core.service.IOperation;
import org.eclipse.gmf.runtime.diagram.ui.editparts.DiagramEditPart;
import org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart;
import org.eclipse.gmf.runtime.diagram.ui.services.layout.AbstractLayoutEditPartProvider;
import org.eclipse.gmf.runtime.notation.View;
import org.eclipse.sirius.common.tools.DslCommonPlugin;
import org.eclipse.sirius.common.tools.api.profiler.ProfilerTask;
import org.eclipse.sirius.common.ui.SiriusTransPlugin;
import org.eclipse.sirius.diagram.DDiagramElement;
import org.eclipse.sirius.diagram.tools.api.layout.PinHelper;
import org.eclipse.sirius.diagram.ui.provider.Messages;
import org.eclipse.sirius.diagram.ui.tools.internal.layout.provider.LayoutService;
import com.google.common.collect.Iterables;
/**
* An abstract layout provider.
*
* @author ymortier
*/
public abstract class AbstractLayoutProvider extends AbstractLayoutEditPartProvider {
/**
* Modeler Category.
*/
public static final String GENERIC_MODELER_CAT = Messages.AbstractLayoutProvider_arrangeAllProfilerTaskCategory;
/**
* Arrange all.
*/
public static final ProfilerTask ARRANGE_ALL = new ProfilerTask(GENERIC_MODELER_CAT, Messages.AbstractLayoutProvider_arrangeAllProfilerTaskName);
/** Map all views with a its associated {@link ChangeBoundsRequest}. */
protected Map<View, List<Request>> viewsToChangeBoundsRequest;
/**
* Create a new {@link AbstractLayoutProvider}.
*
*/
public AbstractLayoutProvider() {
this.viewsToChangeBoundsRequest = new HashMap<View, List<Request>>();
}
/**
* Set the map that maps all views with a its associated
* {@link ChangeBoundsRequest}.
*
* @param viewsToChangeBoundsRequest
* Map all views with a its associated
* {@link ChangeBoundsRequest}.
*/
public void setViewsToChangeBoundsRequest(final Map<View, List<Request>> viewsToChangeBoundsRequest) {
this.viewsToChangeBoundsRequest = viewsToChangeBoundsRequest;
}
/**
* Return the map that maps all views with a its associated
* {@link ChangeBoundsRequest}.
*
* @return the map that maps all views with a its associated
* {@link ChangeBoundsRequest}.
*/
public Map<View, List<Request>> getViewsToChangeBoundsRequest() {
return viewsToChangeBoundsRequest;
}
@Override
public final Command layoutEditParts(final GraphicalEditPart containerEditPart, final IAdaptable layoutHint) {
DslCommonPlugin.PROFILER.startWork(ARRANGE_ALL);
this.getViewsToChangeBoundsRequest().clear();
/*
* If we are asked to layout a whole diagram, check if a specific
* provider has been registered for it (through the
* org.eclipse.sirius.common.ui.airLayoutProvider extension point), and
* use that provider if there is.
*/
if (containerEditPart instanceof DiagramEditPart) {
final DiagramEditPart diagramEditPart = (DiagramEditPart) containerEditPart;
final LayoutProvider diagramLayoutProvider = getDiagramLayoutProvider(diagramEditPart, layoutHint);
if (diagramLayoutProvider != null) {
final Command command = getCommandFromDiagramLayoutProvider(diagramLayoutProvider, diagramEditPart, layoutHint);
command.setLabel(Messages.AbstractLayoutProvider_arrangeAllCommandLabel);
DslCommonPlugin.PROFILER.stopWork(ARRANGE_ALL);
return command;
}
}
final Command command = buildCommand(containerEditPart, layoutHint);
command.setLabel(Messages.AbstractLayoutProvider_arrangeAllCommandLabel);
this.getViewsToChangeBoundsRequest().clear();
AbstractLayoutProvider.resetWrappedCommand(command);
DslCommonPlugin.PROFILER.stopWork(ARRANGE_ALL);
return command;
}
/**
* Get the the diagram layout provider if there is one.
*
* @param diagramEditPart
* the diagram edit part
* @param layoutHint
* the layout hint
* @return the diagram layout provider if there is one, <code>null</code>
* otherwise
*/
public LayoutProvider getDiagramLayoutProvider(final DiagramEditPart diagramEditPart, final IAdaptable layoutHint) {
final LayoutProvider candidate = LayoutService.getProvider(diagramEditPart);
if (candidate != null && candidate.isDiagramLayoutProvider()) {
return candidate;
} else {
return null;
}
}
private Command getCommandFromDiagramLayoutProvider(final LayoutProvider diagramLayoutProvider, final DiagramEditPart diagramEditPart, final IAdaptable layoutHint) {
Command command = null;
final AbstractLayoutEditPartProvider layoutProvider = diagramLayoutProvider.getLayoutNodeProvider(diagramEditPart);
try {
final Method method = layoutProvider.getClass().getMethod("layoutEditParts", new Class[] { GraphicalEditPart.class, IAdaptable.class }); //$NON-NLS-1$
Assert.isNotNull(method);
/* this code seems to avoid an infinite loop */
if (method.getDeclaringClass() != AbstractLayoutProvider.class) {
command = layoutProvider.layoutEditParts(diagramEditPart, layoutHint);
}
} catch (final SecurityException e) {
SiriusTransPlugin.getPlugin().error(Messages.AbstractLayoutProvider_layoutError, e);
} catch (final NoSuchMethodException e) {
SiriusTransPlugin.getPlugin().error(Messages.AbstractLayoutProvider_layoutError, e);
}
if (command == null) {
final List<?> children = diagramEditPart.getChildren();
command = layoutProvider.layoutEditParts(children, layoutHint);
}
return command;
}
/**
* Build the command which performs the layout of all the edit parts in the
* specified container.
* <p>
* The resulting command is a compound command built recursively by calling
* {@link #layoutEditParts(List, IAdaptable)} on all the descendants of the
* specified container and then on the container itself.
*
* @param containerEditPart
* the container to layout.
* @param layoutHint
* the hint to use.
* @return a command to execute to perform the layout of the container and
* all its descendants.
*/
private Command buildCommand(final GraphicalEditPart containerEditPart, final IAdaptable layoutHint) {
final CompoundCommand cc = new CompoundCommand();
// Depth-first recursion.
for (final IGraphicalEditPart graphicalEditPart : Iterables.filter(containerEditPart.getChildren(), IGraphicalEditPart.class)) {
final Command command = buildCommand(graphicalEditPart, layoutHint);
if (command.canExecute()) {
cc.add(command);
}
}
// Base case: layout the container's direct children.
final Command command = this.layoutEditParts(containerEditPart.getChildren(), layoutHint);
if (command.canExecute()) {
cc.add(command);
}
return cc;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.gmf.runtime.common.core.service.IProvider#provides(org.eclipse.gmf.runtime.common.core.service.IOperation)
*/
@Override
public boolean provides(final IOperation operation) {
return false;
}
/**
* Search a request for the specified edit part and of the specified type.
*
* @param editPart
* the edit part.
* @param requestType
* the type of the request.
* @return the found request.
*/
protected Request findRequest(final IGraphicalEditPart editPart, final Object requestType) {
if (editPart != null) {
return findRequest(editPart.getNotationView(), requestType);
}
return null;
}
/**
* Search a request for the specified view and of the specified type.
*
* @param notationView
* the view.
* @param requestType
* the type of the request.
* @return the found request.
*/
protected Request findRequest(final View notationView, final Object requestType) {
final List<Request> requests = this.getViewsToChangeBoundsRequest().get(notationView);
Request result = null;
if (requests != null) {
final Iterator<Request> iterRequests = requests.iterator();
while (iterRequests.hasNext() && result == null) {
final Request currentRequest = iterRequests.next();
if (currentRequest.getType() != null && currentRequest.getType().equals(requestType)) {
result = currentRequest;
}
}
}
return result;
}
/**
* Create a command with the specified request.
*
* @param request
* the request.
* @param editPart
* the edit part.
* @return an executable command.
*/
protected Command buildCommandWrapper(final Request request, final EditPart editPart) {
if (editPart instanceof IGraphicalEditPart) {
List<Request> requests = this.getViewsToChangeBoundsRequest().get(((IGraphicalEditPart) editPart).getNotationView());
if (requests == null) {
requests = new LinkedList<Request>();
this.getViewsToChangeBoundsRequest().put(((IGraphicalEditPart) editPart).getNotationView(), requests);
}
requests.add(request);
}
return new CommandWrapper(request, editPart);
}
/**
* Tests whether an edit part should be considered as pinned (fixed size and
* location) during the layout.
*
* @param part
* the edit part.
* @return <code>true</code> if the edit part should be considered as
* pinned.
*/
protected boolean isPinned(final IGraphicalEditPart part) {
boolean isPinned = false;
if (part.resolveSemanticElement() instanceof DDiagramElement) {
DDiagramElement dDiagramElement = (DDiagramElement) part.resolveSemanticElement();
isPinned = new PinHelper().isPinned(dDiagramElement);
}
return isPinned;
}
/**
* Wraps a GMF Command.
*
* @author ymortier
*/
protected static class CommandWrapper extends Command {
/**
* The wrapped command, initialized at the first call to
* {@link #getWrappedCommand()}.
*/
private Command wrappedCommand;
/** The request. */
private final Request request;
/** The edit part. */
private final EditPart editPart;
/**
* Create a new Command wrapper.
*
* @param request
* the request.
* @param editPart
* the edit part.
*/
public CommandWrapper(final Request request, final EditPart editPart) {
this.request = request;
this.editPart = editPart;
}
@Override
public void execute() {
this.getWrappedCommand().execute();
}
@Override
public boolean canExecute() {
return this.getWrappedCommand().canExecute();
}
@Override
public boolean canUndo() {
return this.getWrappedCommand().canUndo();
}
@Override
public void undo() {
this.getWrappedCommand().undo();
}
@Override
public void redo() {
this.getWrappedCommand().redo();
}
private Command getWrappedCommand() {
if (wrappedCommand == null) {
final Command cmd = editPart.getCommand(request);
if (cmd == null) {
wrappedCommand = UnexecutableCommand.INSTANCE;
} else {
wrappedCommand = cmd;
}
}
return wrappedCommand;
}
/**
* {@inheritDoc}
*/
@Override
public String getLabel() {
return getWrappedCommand().getLabel();
}
/**
* Returns the request that is causing the command.
*
* @return the request
*/
public Request getRequest() {
return request;
}
/**
* Return the associated edit part.
*
* @return the editPart
*/
public EditPart getEditPart() {
return editPart;
}
}
/**
* Return the relative bounds of the specified edit part.
*
* @param graphicalEditPart
* the edit part.
* @return the bounds of the specified edit part.
*/
protected Rectangle getBounds(final IGraphicalEditPart graphicalEditPart) {
Object existingRequest = this.findRequest(graphicalEditPart, org.eclipse.gef.RequestConstants.REQ_RESIZE);
final Rectangle figureBounds = new Rectangle(graphicalEditPart.getFigure().getBounds());
if (existingRequest instanceof ChangeBoundsRequest) {
final ChangeBoundsRequest changeBoundsRequest = (ChangeBoundsRequest) existingRequest;
if (changeBoundsRequest.getSizeDelta() != null) {
adjustBounds(figureBounds, changeBoundsRequest);
}
}
existingRequest = this.findRequest(graphicalEditPart, org.eclipse.gef.RequestConstants.REQ_MOVE);
if (existingRequest instanceof ChangeBoundsRequest) {
final ChangeBoundsRequest changeBoundsRequest = (ChangeBoundsRequest) existingRequest;
if (changeBoundsRequest.getMoveDelta() != null) {
figureBounds.x += changeBoundsRequest.getMoveDelta().x;
figureBounds.y += changeBoundsRequest.getMoveDelta().y;
}
}
return figureBounds;
}
private void adjustBounds(final Rectangle figureBounds, final ChangeBoundsRequest changeBoundsRequest) {
switch (changeBoundsRequest.getResizeDirection()) {
case PositionConstants.NORTH:
figureBounds.height += changeBoundsRequest.getSizeDelta().height;
figureBounds.y -= changeBoundsRequest.getSizeDelta().height;
break;
case PositionConstants.SOUTH:
figureBounds.height += changeBoundsRequest.getSizeDelta().height;
break;
case PositionConstants.EAST:
figureBounds.width += changeBoundsRequest.getSizeDelta().width;
break;
case PositionConstants.WEST:
figureBounds.width += changeBoundsRequest.getSizeDelta().width;
figureBounds.x -= changeBoundsRequest.getSizeDelta().width;
break;
case PositionConstants.NORTH_EAST:
figureBounds.height += changeBoundsRequest.getSizeDelta().height;
figureBounds.y -= changeBoundsRequest.getSizeDelta().height;
figureBounds.width += changeBoundsRequest.getSizeDelta().width;
break;
case PositionConstants.NORTH_WEST:
figureBounds.height += changeBoundsRequest.getSizeDelta().height;
figureBounds.y -= changeBoundsRequest.getSizeDelta().height;
figureBounds.width += changeBoundsRequest.getSizeDelta().width;
figureBounds.x -= changeBoundsRequest.getSizeDelta().width;
break;
case PositionConstants.SOUTH_WEST:
figureBounds.height += changeBoundsRequest.getSizeDelta().height;
figureBounds.width += changeBoundsRequest.getSizeDelta().width;
figureBounds.x -= changeBoundsRequest.getSizeDelta().width;
break;
default:
figureBounds.height += changeBoundsRequest.getSizeDelta().height;
figureBounds.width += changeBoundsRequest.getSizeDelta().width;
}
}
/**
* Return the first figure of type <code>type</code> that is the figure
* <code>searchRoot</code> or a child of <code>searchRoot</code>.
*
* @param searchRoot
* the root figure to start the search.
* @param type
* the type of the figure to return.
* @return Return the first figure of type <code>type</code> that is the
* figure <code>searchRoot</code> or a child of
* <code>searchRoot</code>.
*/
protected IFigure findChild(final IFigure searchRoot, final Class<?> type) {
IFigure result = null;
if (type.isInstance(searchRoot)) {
result = searchRoot;
}
final Iterator<?> iterChildren = searchRoot.getChildren().iterator();
while (iterChildren.hasNext() && result == null) {
final IFigure child = (IFigure) iterChildren.next();
result = findChild(child, type);
}
return result;
}
/**
* Return all edit parts contained in root + root.
*
* @param root
* the root edit part
* @return all edit parts contained in root + root.
*/
public List<EditPart> getAllEditParts(final EditPart root) {
final Set<EditPart> editParts = new HashSet<EditPart>();
editParts.add(root);
if (root instanceof GraphicalEditPart) {
editParts.addAll(((GraphicalEditPart) root).getSourceConnections());
editParts.addAll(((GraphicalEditPart) root).getTargetConnections());
}
final Iterator<EditPart> iterChildren = root.getChildren().iterator();
while (iterChildren.hasNext()) {
final EditPart next = iterChildren.next();
editParts.addAll(getAllEditParts(next));
}
return new ArrayList<EditPart>(editParts);
}
private static void resetWrappedCommand(final Command command) {
if (command instanceof CommandWrapper) {
((CommandWrapper) command).wrappedCommand = null;
} else if (command instanceof CompoundCommand) {
final Object[] children = ((CompoundCommand) command).getChildren();
for (Object element : children) {
if (element instanceof Command) {
AbstractLayoutProvider.resetWrappedCommand((Command) element);
}
}
}
}
}