/*******************************************************************************
* Copyright (c) 2012 Arapiki Solutions Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* psmith - initial API and
* implementation and/or initial documentation
*******************************************************************************/
package com.buildml.eclipse.packages.patterns;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.graphiti.features.context.IAddContext;
import org.eclipse.graphiti.features.context.ICreateContext;
import org.eclipse.graphiti.features.context.IDeleteContext;
import org.eclipse.graphiti.features.context.IMoveShapeContext;
import org.eclipse.graphiti.features.context.IResizeShapeContext;
import org.eclipse.graphiti.features.context.IUpdateContext;
import org.eclipse.graphiti.mm.algorithms.Ellipse;
import org.eclipse.graphiti.mm.algorithms.Rectangle;
import org.eclipse.graphiti.mm.algorithms.Text;
import org.eclipse.graphiti.mm.algorithms.styles.Orientation;
import org.eclipse.graphiti.mm.pictograms.ContainerShape;
import org.eclipse.graphiti.mm.pictograms.Diagram;
import org.eclipse.graphiti.mm.pictograms.FixPointAnchor;
import org.eclipse.graphiti.mm.pictograms.PictogramElement;
import org.eclipse.graphiti.mm.pictograms.PictogramLink;
import org.eclipse.graphiti.pattern.AbstractPattern;
import org.eclipse.graphiti.pattern.IPattern;
import org.eclipse.graphiti.services.Graphiti;
import org.eclipse.graphiti.services.IGaService;
import org.eclipse.graphiti.services.IPeCreateService;
import org.eclipse.graphiti.util.ColorConstant;
import org.eclipse.graphiti.util.IColorConstant;
import com.buildml.eclipse.bobj.UIAction;
import com.buildml.eclipse.packages.PackageDiagramEditor;
import com.buildml.eclipse.packages.layout.LayoutAlgorithm;
import com.buildml.eclipse.packages.layout.LeftRightBounds;
import com.buildml.eclipse.packages.layout.PictogramSize;
import com.buildml.eclipse.utils.AlertDialog;
import com.buildml.eclipse.utils.GraphitiUtils;
import com.buildml.eclipse.utils.UndoOpAdapter;
import com.buildml.model.IActionMgr;
import com.buildml.model.IActionMgr.OperationType;
import com.buildml.model.IActionTypeMgr;
import com.buildml.model.IBuildStore;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.ISlotTypes;
import com.buildml.model.IPackageMemberMgr.MemberLocation;
import com.buildml.model.ISlotTypes.SlotDetails;
import com.buildml.model.undo.ActionUndoOp;
import com.buildml.model.undo.MultiUndoOp;
import com.buildml.utils.errors.ErrorCode;
/**
* A Graphiti pattern for managing the "Action" graphical element in a BuildML diagram.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class ActionPattern extends AbstractPattern implements IPattern {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** The PackageDiagramEditor we're part of */
private PackageDiagramEditor editor;
/** The IBuildStore that this diagram represents */
private IBuildStore buildStore;
/** The managers owned by this BuildStore */
private IActionMgr actionMgr;
private IActionTypeMgr actionTypeMgr;
private IPackageMemberMgr pkgMemberMgr;
/**
* The layout algorithm we use for positioning pictograms.
*/
private LayoutAlgorithm layoutAlgorithm = null;
/*
* Various colour constants used in displaying this element.
*/
private static final IColorConstant TEXT_FOREGROUND = IColorConstant.BLACK;
private static final IColorConstant LINE_COLOUR = new ColorConstant(180, 180, 155);
private static final IColorConstant FILL_COLOUR = new ColorConstant(220, 220, 190);
/*
* Size of this element (in pixels).
*/
private static final int ACTION_WIDTH = 150;
private static final int ACTION_HEIGHT = 50;
/** The (static) maximum size of an action's pictogram, in pixels */
private static PictogramSize ACTION_MAX_SIZE =
new PictogramSize(ACTION_WIDTH, ACTION_HEIGHT);
/*=====================================================================================*
* STATIC METHODS
*=====================================================================================*/
/**
* Return the (width, height) in pixel of the file group pictogram. This is used
* for laying-out the package members.
* @return The (width, height) in pixels.
*/
public static PictogramSize getSize() {
return ACTION_MAX_SIZE;
}
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new {@link ActionPattern} object.
*/
public ActionPattern() {
super(null);
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* Return the name of this element.
*/
@Override
public String getCreateName() {
return "Action";
}
/*-------------------------------------------------------------------------------------*/
/*
* We do not want "Action" to appear in the palette.
*/
@Override
public boolean isPaletteApplicable() {
return false;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.graphiti.pattern.AbstractPattern#isMainBusinessObjectApplicable(java.lang.Object)
*/
@Override
public boolean isMainBusinessObjectApplicable(Object mainBusinessObject) {
return mainBusinessObject instanceof UIAction;
}
/*-------------------------------------------------------------------------------------*/
/*
* (non-Javadoc)
* @see org.eclipse.graphiti.pattern.AbstractPattern#isPatternControlled(
* org.eclipse.graphiti.mm.pictograms.PictogramElement)
*/
@Override
protected boolean isPatternControlled(PictogramElement pictogramElement) {
Object domainObject = getBusinessObjectForPictogramElement(pictogramElement);
return isMainBusinessObjectApplicable(domainObject);
}
/*-------------------------------------------------------------------------------------*/
/*
* (non-Javadoc)
* @see org.eclipse.graphiti.pattern.AbstractPattern#isPatternRoot(
* org.eclipse.graphiti.mm.pictograms.PictogramElement)
*/
@Override
protected boolean isPatternRoot(PictogramElement pictogramElement) {
Object domainObject = getBusinessObjectForPictogramElement(pictogramElement);
return isMainBusinessObjectApplicable(domainObject);
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine whether a specific business object can be added to the diagram.
*/
@Override
public boolean canAdd(IAddContext context) {
/* yes, a UIAction can be added to a Diagram */
if (context.getNewObject() instanceof UIAction) {
if (context.getTargetContainer() instanceof Diagram) {
return true;
}
}
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the visual representation of a UIAction on the parent Diagram.
*/
@Override
public PictogramElement add(IAddContext context) {
/* determine our editor and BuildStore */
editor = (PackageDiagramEditor)getDiagramEditor();
buildStore = editor.getBuildStore();
actionMgr = buildStore.getActionMgr();
actionTypeMgr = buildStore.getActionTypeMgr();
pkgMemberMgr = buildStore.getPackageMemberMgr();
/*
* What are we adding, and where are we adding it?
*/
UIAction addedAction = (UIAction) context.getNewObject();
int actionId = addedAction.getId();
Diagram targetDiagram = (Diagram) context.getTargetContainer();
/*
* How many ellipses will be shown? This illustrate whether it's
* a regular action, or a multi-action.
*/
int numEllipses = 1;
IPeCreateService peCreateService = Graphiti.getPeCreateService();
IGaService gaService = Graphiti.getGaService();
ContainerShape containerShape =
peCreateService.createContainerShape(targetDiagram, true);
/*
* Create an invisible outer rectangle. The smaller ellipse(s) will be placed
* (or stacked) inside this.
*/
Rectangle invisibleRectangle =
gaService.createInvisibleRectangle(containerShape);
gaService.setLocationAndSize(invisibleRectangle,
context.getX(), context.getY(), ACTION_WIDTH, ACTION_HEIGHT);
/*
* Create the required number of ellipse(s) within the overall shape. When
* multiple ovals are drawn, we need to position them (and the inner text)
* carefully.
*/
int xOverlap = 3;
int yOverlap = 4;
int xAdjust = (numEllipses - 1) * xOverlap;
int yAdjust = (numEllipses - 1) * yOverlap;
for (int i = 0; i != numEllipses; i++) {
Ellipse ellipse = gaService.createEllipse(invisibleRectangle);
ellipse.setForeground(manageColor(LINE_COLOUR));
ellipse.setBackground(manageColor(FILL_COLOUR));
ellipse.setLineWidth(2);
gaService.setLocationAndSize(ellipse, i * xOverlap, i * yOverlap,
ACTION_WIDTH - xAdjust, ACTION_HEIGHT - yAdjust);
}
/*
* Provide a quick title for the command to be displayed within the oval. This
* takes "Shell:" and appends the shell command's base name.
* TODO: replace this with more generic code, but only after we support more than
* shell commands.
*/
String actionCommand = (String) actionMgr.getSlotValue(actionId, IActionMgr.COMMAND_SLOT_ID);
String actionCommandBase = getShellCommandSummary(actionCommand);
/* draw the action type's name inside the oval(s) */
Text actionTypeNameText = gaService.createPlainText(invisibleRectangle,
"Shell: " + actionCommandBase);
actionTypeNameText.setFilled(false);
actionTypeNameText.setForeground(manageColor(TEXT_FOREGROUND));
actionTypeNameText.setHorizontalAlignment(Orientation.ALIGNMENT_CENTER);
gaService.setLocationAndSize(actionTypeNameText, xAdjust, yAdjust,
ACTION_WIDTH - xAdjust, ACTION_HEIGHT - yAdjust);
/*
* Add a couple of anchors so we can draw connections to this shape.
* UIFileActionConnection.INPUT_TO_ACTION (0) - left anchor
* UIFileActionConnection.OUTPUT_FROM_ACTION (1) - right anchor
*/
FixPointAnchor anchorLeft = peCreateService.createFixPointAnchor(containerShape);
anchorLeft.setLocation(gaService.createPoint(-5, ACTION_HEIGHT / 2));
gaService.createInvisibleRectangle(anchorLeft);
FixPointAnchor anchorRight = peCreateService.createFixPointAnchor(containerShape);
anchorRight.setLocation(gaService.createPoint(ACTION_WIDTH + 5, ACTION_HEIGHT / 2));
gaService.createInvisibleRectangle(anchorRight);
/* create a link between the shape and the business object, and display it. */
link(containerShape, addedAction);
layoutPictogramElement(containerShape);
return containerShape;
}
/*-------------------------------------------------------------------------------------*/
/**
* No, we can't create new actions via the diagram editor.
*/
@Override
public boolean canCreate(ICreateContext context) {
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine whether a business object can be added to the diagram.
*/
public Object[] create(ICreateContext context) {
// TODO: is this method necessary?
/* create new UIAction object */
UIAction newAction = new UIAction(0);
/* Add model element to same resource as the parent Diagram */
getDiagram().eResource().getContents().add(newAction);
/* do the add procedure */
addGraphicalRepresentation(context, newAction);
/* return newly created business object(s) */
return new Object[] { newAction };
}
/*-------------------------------------------------------------------------------------*/
/**
* The user isn't allowed to resize the object.
*/
@Override
public boolean canResizeShape(IResizeShapeContext context) {
return false;
}
/*-------------------------------------------------------------------------------------*/
/*
* The UIAction business object may have changed. Update the pictogram from the model.
*/
@Override
public boolean update(IUpdateContext context) {
editor.getDiagramBehavior().refresh();
return super.update(context);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.graphiti.pattern.AbstractPattern#canMoveShape(org.eclipse.graphiti.features.context.IMoveShapeContext)
*/
@Override
public boolean canMoveShape(IMoveShapeContext context) {
/* what object is being moved? It must be a UIAction */
Object sourceBo = GraphitiUtils.getBusinessObject(context.getShape());
if (!(sourceBo instanceof UIAction)) {
return false;
}
int actionId = ((UIAction)sourceBo).getId();
/*
* Validate where the UIAction is moving to. We can't move UIActions
* off the left/top of the window, and they must be moved within the
* Diagram (not onto other members).
*/
Object targetContainer = context.getTargetContainer();
if (!(targetContainer instanceof Diagram)) {
return false;
}
int x = context.getX();
int y = context.getY();
/* we can never move off the top of the canvas (Y-axis) */
if (y < 0) {
return false;
}
/*
* Determine the acceptable X-axis movement bounds for the object we're moving. This involves
* a database query, which will happen roughly 10-20 times for an average mouse drag.
*/
if (layoutAlgorithm == null) {
layoutAlgorithm = ((PackageDiagramEditor)getDiagramEditor()).getLayoutAlgorithm();
}
LeftRightBounds bounds = layoutAlgorithm.getMemberMovementBounds(IPackageMemberMgr.TYPE_ACTION, actionId);
if ((x < bounds.leftBound) || (x > bounds.rightBound)) {
return false;
}
/* check that we've moved a single UIAction object */
PictogramElement pe = context.getPictogramElement();
PictogramLink pl = pe.getLink();
EList<EObject> bos = pl.getBusinessObjects();
if (bos.size() != 1) {
return false;
}
/*
* Finally, check that this is a UIAction (although we probably wouldn't have
* got here otherwise.
*/
Object bo = bos.get(0);
return (bo instanceof UIAction);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.graphiti.pattern.AbstractPattern#moveShape(org.eclipse.graphiti.features.context.IMoveShapeContext)
*/
@Override
public void moveShape(IMoveShapeContext context) {
super.moveShape(context);
/*
* Fetch the x, y and actionId. Note that all error checking was done by canMoveShape().
*/
int x = context.getX();
int y = context.getY();
PictogramLink pl = context.getPictogramElement().getLink();
UIAction action = (UIAction)(pl.getBusinessObjects().get(0));
int actionId = action.getId();
/* determine the UIAction's old location */
MemberLocation oldXY = pkgMemberMgr.getMemberLocation(IPackageMemberMgr.TYPE_ACTION, actionId);
if (oldXY == null){
/* default, in the case of an error */
oldXY = new MemberLocation();
oldXY.x = 0;
oldXY.y = 0;
}
/* create an undo/redo operation that will invoke the underlying database changes */
ActionUndoOp op = new ActionUndoOp(buildStore, actionId);
op.recordLocationChange(oldXY.x, oldXY.y, x, y);
new UndoOpAdapter("Move Action", op).invoke();
}
/*-------------------------------------------------------------------------------------*/
/**
* Yes, we can delete UIActions.
*/
@Override
public boolean canDelete(IDeleteContext context) {
return true;
}
/*-------------------------------------------------------------------------------------*/
/**
* Invoked when the user initiates a "delete" operation on a UIAction.
*/
@Override
public void delete(IDeleteContext context) {
/* determine the business object that related to the pictogram being deleted */
UIAction action = (UIAction)(GraphitiUtils.getBusinessObject(context.getPictogramElement()));
int actionId = action.getId();
/*
* Sanity checks.
*/
Integer[] children = actionMgr.getChildren(actionId);
if (children.length != 0) {
AlertDialog.displayErrorDialog("Can't Delete", "This action still has children (see the <import> package)");
return;
}
Integer filesAccessed[] = actionMgr.getFilesAccessed(actionId, OperationType.OP_UNSPECIFIED);
if (filesAccessed.length != 0) {
AlertDialog.displayErrorDialog("Can't Delete",
"This action can't be deleted, as it still references files in the <import> package");
return;
}
/* add the "delete" operation to our redo/undo stack */
MultiUndoOp multiOp = new MultiUndoOp();
/*
* first, delete all of this action's slots that refer to file groups.
*/
int actionTypeId = actionMgr.getActionType(actionId);
if (actionTypeId == ErrorCode.NOT_FOUND) {
return; /* invalid action type - ignore */
}
SlotDetails slots[] = actionTypeMgr.getSlots(actionTypeId, ISlotTypes.SLOT_POS_ANY);
for (int i = 0; i < slots.length; i++) {
if (slots[i].slotType == ISlotTypes.SLOT_TYPE_FILEGROUP) {
int slotId = slots[i].slotId;
Object slotValue = actionMgr.getSlotValue(actionId, slotId);
if (slotValue instanceof Integer) {
ActionUndoOp op = new ActionUndoOp(buildStore, actionId);
op.recordSlotRemove(slotId, slotValue);
multiOp.add(op);
}
}
}
/* now delete the action itself */
ActionUndoOp op = new ActionUndoOp(buildStore, actionId);
op.recordMoveToTrash();
multiOp.add(op);
/* invoke all changes in one step... */
new UndoOpAdapter("Delete Action", multiOp).invoke();
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Given a full shell command string, generate a summary of that command that is suitable
* for display with an action's oval. This summary provides the command name (without
* arguments or absolute paths), and possibly combines multiple commands into a single
* summary. For example, "/usr/bin/gcc -c test.c" will be summarized as "gcc". Also,
* "gcc -c test.c\n/usr/bin/ld -o test test.o" is summarized as "gcc,ld".
*
* @param actionCommand The original (full) action shell command.
* @return The summarized String.
*/
private String getShellCommandSummary(String actionCommand) {
/* we'll accumulate the command summary in this StringBuilder */
StringBuilder sb = new StringBuilder();
/* break the action's full (possibly multi-line) command into multiple lines */
String lines[] = actionCommand.split("\n", 0);
/* for each command line in the action's shell command... */
for (int i = 0; i < lines.length; i++) {
/* extract the first part of the command line, up until the first space */
String lineSplit[] = lines[i].split(" ", 2);
if (lineSplit.length > 0) {
String base = lineSplit[0];
/* Fetch the last part of the command. For example, /usr/bin/gcc -> gcc */
int slashIndex = base.lastIndexOf('/');
if (slashIndex != -1){
base = base.substring(slashIndex + 1);
}
/* append the base of this command to the output */
if (i != 0) {
sb.append(',');
}
sb.append(base);
}
}
return sb.toString();
}
/*-------------------------------------------------------------------------------------*/
}