/*******************************************************************************
* 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
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.mm.algorithms.Polygon;
import org.eclipse.graphiti.mm.algorithms.Polyline;
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 org.eclipse.swt.widgets.Display;
import com.buildml.eclipse.actions.dialogs.SlotSelectionDialog;
import com.buildml.eclipse.bobj.UIAction;
import com.buildml.eclipse.bobj.UIFileGroup;
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.EclipsePartUtils;
import com.buildml.eclipse.utils.GraphitiUtils;
import com.buildml.eclipse.utils.UndoOpAdapter;
import com.buildml.eclipse.utils.errors.FatalError;
import com.buildml.model.IActionMgr;
import com.buildml.model.IActionTypeMgr;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileGroupMgr;
import com.buildml.model.IFileMgr;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.ISlotTypes;
import com.buildml.model.IPackageMemberMgr.MemberDesc;
import com.buildml.model.IPackageMemberMgr.MemberLocation;
import com.buildml.model.ISlotTypes.SlotDetails;
import com.buildml.model.undo.ActionUndoOp;
import com.buildml.model.undo.FileGroupUndoOp;
import com.buildml.model.undo.MultiUndoOp;
import com.buildml.utils.errors.ErrorCode;
/**
* A Graphiti pattern for managing the "FileGroup" graphical element in a BuildML diagram.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class FileGroupPattern 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 IPackageMemberMgr pkgMemberMgr;
private IFileMgr fileMgr;
private IFileGroupMgr fileGroupMgr;
private IActionMgr actionMgr;
private IActionTypeMgr actionTypeMgr;
/**
* If we're adding multiple files in one drag operation, which file group
* are we adding to?
*/
private UIFileGroup multiAddFileGroup = null;
/**
* The initial set of members of this file group (possibly empty).
*/
private ArrayList<Integer> initialMembers = null;
/**
* The final set of members of this file group (after a drag
* operation has completed).
*/
private ArrayList<Integer> currentMembers = null;
/**
* 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(141, 166, 190);
private static final IColorConstant FILL_COLOUR = new ColorConstant(187, 218, 247);
private static final IColorConstant FILL_CORNER_COLOUR = new ColorConstant(160, 190, 220);
/*
* Size of this element (in pixels).
*/
private static final int FILE_GROUP_WIDTH = 50;
private static final int FILE_GROUP_HEIGHT = 50;
private static final int FILE_GROUP_CORNER_SIZE = 20;
private static final int FILE_GROUP_OVERLAP = 3;
/** Font type for labels */
private static final String LABEL_FONT = "courier";
/** Font size */
private static final int LABEL_FONT_SIZE = 9;
/** Gap between consecutive label lines */
private static final int LABEL_FONT_GAP = 2;
/** The maximum number of file names to show under an icon */
private static final int MAX_LABELS_TO_SHOW = 4;
/** Polygon coordinates for drawing the file's front page (the box with the bent corner) */
private static final int OFF_X = FILE_GROUP_WIDTH / 2;
int coordsPage1[] = new int[] {
OFF_X, 0,
OFF_X + FILE_GROUP_WIDTH - FILE_GROUP_CORNER_SIZE, 0,
OFF_X + FILE_GROUP_WIDTH, FILE_GROUP_CORNER_SIZE,
OFF_X + FILE_GROUP_WIDTH, FILE_GROUP_HEIGHT,
OFF_X, FILE_GROUP_HEIGHT
};
/** Polygon coordinates for drawing the bent corner */
int coordsCorner[] = new int[] {
OFF_X + FILE_GROUP_WIDTH - FILE_GROUP_CORNER_SIZE, 0,
OFF_X + FILE_GROUP_WIDTH, FILE_GROUP_CORNER_SIZE,
OFF_X + FILE_GROUP_WIDTH - FILE_GROUP_CORNER_SIZE, FILE_GROUP_CORNER_SIZE
};
/** Polygon coordinates for drawing the file group's second page (if one is drawn) */
int coordsPage2[] = new int[] {
OFF_X + FILE_GROUP_OVERLAP, FILE_GROUP_OVERLAP,
OFF_X + FILE_GROUP_OVERLAP + FILE_GROUP_WIDTH, FILE_GROUP_OVERLAP,
OFF_X + FILE_GROUP_OVERLAP + FILE_GROUP_WIDTH, FILE_GROUP_OVERLAP + FILE_GROUP_HEIGHT,
OFF_X + FILE_GROUP_OVERLAP, FILE_GROUP_OVERLAP + FILE_GROUP_HEIGHT
};
/**
* Coordinates for the three additional lines that are shown inside a merge group box.
*/
int coordsMergeLines[][] = new int[][] {
{
OFF_X + 5, FILE_GROUP_CORNER_SIZE,
OFF_X + FILE_GROUP_WIDTH - 5, FILE_GROUP_CORNER_SIZE + 10,
},
{
OFF_X + 5, FILE_GROUP_CORNER_SIZE + 13,
OFF_X + FILE_GROUP_WIDTH - 5, FILE_GROUP_CORNER_SIZE + 13,
},
{
OFF_X + 5, FILE_GROUP_CORNER_SIZE + 26,
OFF_X + FILE_GROUP_WIDTH - 5, FILE_GROUP_CORNER_SIZE + 16,
},
};
/** The (static) maximum size of a file group pictogram, in pixels */
private static PictogramSize FILE_GROUP_MAX_SIZE =
new PictogramSize(2 * FILE_GROUP_WIDTH,
FILE_GROUP_HEIGHT + FILE_GROUP_OVERLAP +
((MAX_LABELS_TO_SHOW + 1) * (LABEL_FONT_SIZE + LABEL_FONT_GAP)));
/*=====================================================================================*
* 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 FILE_GROUP_MAX_SIZE;
}
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new {@link FileGroupPattern} object.
*/
public FileGroupPattern() {
super(null);
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* Return the name of this element, as will appears in the Diagram's palette.
*/
@Override
public String getCreateName() {
return "File Group";
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.graphiti.pattern.AbstractPattern#isMainBusinessObjectApplicable(java.lang.Object)
*/
@Override
public boolean isMainBusinessObjectApplicable(Object mainBusinessObject) {
return (mainBusinessObject instanceof UIFileGroup) || isFileClass(mainBusinessObject);
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine whether a specific business object can be added to the diagram.
*/
@Override
public boolean canAdd(IAddContext context) {
Object newObject = context.getNewObject();
/*
* A UIFileGroup or an IFile can both be added to a Diagram (creating
* a new UIFileGroup, or can be added to an existing UIFileGroup.
*/
if ((newObject instanceof UIFileGroup) || isFileClass(newObject)) {
/* what are we adding it to? */
Object target = context.getTargetContainer();
if (target instanceof Diagram) {
return true;
}
Object bo = GraphitiUtils.getBusinessObject(target);
if (bo != null) {
return true;
}
}
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* Create the visual representation of a UIFileGroup on the parent Diagram.
*/
@Override
public PictogramElement add(IAddContext context) {
editor = (PackageDiagramEditor)getDiagramEditor();
buildStore = editor.getBuildStore();
pkgMemberMgr = buildStore.getPackageMemberMgr();
fileMgr = buildStore.getFileMgr();
fileGroupMgr = buildStore.getFileGroupMgr();
actionMgr = buildStore.getActionMgr();
actionTypeMgr = buildStore.getActionTypeMgr();
/*
* Case handled:
* 1) UIFileGroup added (programatically) or dragged onto Diagram - make it appear on Diagram.
* 2) IFile dragged onto Diagram - create a new UIFileGroup, make it appear.
* 3) IFile dragged onto UIFileGroup - add to existing UIFileGroup.
*
* Possible future cases:
* - UIFileGroup dragged onto UIFileGroup - merge the two.
*/
Object addedObject = context.getNewObject();
Object targetObject = context.getTargetContainer();
int x = context.getX();
int y = context.getY();
/* Case #1 - add/drag an existing UIFileGroup onto the Diagram */
if ((addedObject instanceof UIFileGroup) && (targetObject instanceof Diagram)) {
return renderPictogram((Diagram)targetObject, (UIFileGroup)addedObject, x, y);
}
/* Case #2 - drag IFile onto Diagram */
if ((targetObject instanceof Diagram) && isFileClass(addedObject)) {
String pathName = getPathOf(addedObject);
/*
* If this is the first (of possibly many) files being added to this file
* group, we start by recording the "initial" content. Given that we're
* dragging directly onto the Diagram, there initial file group is empty.
* The "current" set of members is also empty, but we'll shortly be
* adding members to it.
*/
if (multiAddFileGroup == null) {
initialMembers = new ArrayList<Integer>();
currentMembers = new ArrayList<Integer>();
/*
* Create a brand new UIFileGroup in the database - initially empty,
* and won't be displayed.
*/
int pkgId = editor.getPackageId();
final int fileGroupId = fileGroupMgr.newSourceGroup(pkgId);
if (fileGroupId == ErrorCode.NOT_FOUND) {
return null; /* invalid pkgId */
}
multiAddFileGroup = new UIFileGroup(fileGroupId);
getDiagram().eResource().getContents().add(multiAddFileGroup);
/*
* If the user is dragging multiple files onto the Diagram, then we need
* to do some clever work to make sure they all end up in the same file group.
* We end up seeing multiple add() calls from the graphiti framework, so record
* the UIFileGroup that we added the first file to, and reuse it for each
* successive call to add(). However, once the Eclipse UI is "idle" again, make
* sure we stop using that file group.
*/
Display.getCurrent().asyncExec(new Runnable() {
@Override
public void run() {
multiAddFileGroup = null;
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
op.recordMembershipChange(initialMembers, currentMembers);
new UndoOpAdapter("Create New File Group", op).invoke();
}
});
/* set the x and y coordinates correctly */
pkgMemberMgr.setMemberLocation(IPackageMemberMgr.TYPE_FILE_GROUP, fileGroupId, x, y);
}
/*
* For all dragged files (not just the first), add the dragged file into our "currentMembers"
* array. Once our drag operation is complete, we'll add them all to the file group itself.
* Note: this might fail, but we'll silently ignore the error.
*/
addToFileGroup(pathName);
/*
* Nothing to display as a result of this call - it'll be displayed when we refresh
* the whole diagram.
*/
return null;
}
/* Case #3 - drag IFile (or many) onto existing UIFileGroup */
if (isFileClass(addedObject)) {
Object bo = GraphitiUtils.getBusinessObject(targetObject);
if (bo instanceof UIFileGroup) {
UIFileGroup fileGroup = (UIFileGroup)bo;
final int fileGroupId = fileGroup.getId();
/*
* We may have multiple files being added to an existing file group. If this is
* the first file, then we need to record the "initialMembers" of the group
* so that we can roll back after an "undo".
*/
if (multiAddFileGroup == null) {
multiAddFileGroup = fileGroup;
initialMembers = getFileGroupAsArrayList(fileGroupId);
currentMembers = (ArrayList<Integer>) initialMembers.clone();
/* once the add operation is complete... */
Display.getCurrent().asyncExec(new Runnable() {
@Override
public void run() {
multiAddFileGroup = null;
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
op.recordMembershipChange(initialMembers, currentMembers);
new UndoOpAdapter("Modify File Group", op).invoke();
}
});
}
/* for all invocations, not just the first, add the new path into currentMembers */
addToFileGroup(getPathOf(addedObject));
/*
* Nothing to display as a result of this call - it'll be displayed when we refresh
* the whole diagram.
*/
return null;
}
}
/* other cases are not handled */
return null;
}
/*-------------------------------------------------------------------------------------*/
/**
* We can't add FileGroups from the palette.
*/
@Override
public boolean canCreate(ICreateContext context) {
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* The user isn't allowed to resize the object.
*/
@Override
public boolean canResizeShape(IResizeShapeContext context) {
return false;
}
/*=====================================================================================*
* PROTECTED METHODS
*=====================================================================================*/
/*
* (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);
}
/*-------------------------------------------------------------------------------------*/
/* (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 UIFileGroup */
Object sourceBo = GraphitiUtils.getBusinessObject(context.getShape());
if (!(sourceBo instanceof UIFileGroup)) {
return false;
}
int fileGroupId = ((UIFileGroup)sourceBo).getId();
/*
* Validate where the UIFileGroup is moving to. We can't move UIFileGroups
* off the left/top of the window, and they can't be moved left of their
* left neighbours, or right of their right neighbours.
*/
Object targetContainer = context.getTargetContainer();
int x = context.getX();
int y = context.getY();
if (targetContainer instanceof Diagram) {
/* 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_FILE_GROUP, fileGroupId);
if ((x < bounds.leftBound) || (x > bounds.rightBound)) {
return false;
}
}
/* check that we've moved a single UIFileGroup object */
PictogramElement pe = context.getPictogramElement();
PictogramLink pl = pe.getLink();
EList<EObject> bos = pl.getBusinessObjects();
if (bos.size() != 1) {
return false;
}
/*
* Finally, what are we moving onto? Moving within the Diagram is allowed,
* and moving onto a UIAction is allowed.
*/
if (targetContainer instanceof Diagram) {
return true;
}
Object targetBo = GraphitiUtils.getBusinessObject(targetContainer);
if ((targetBo instanceof UIAction) || (targetBo instanceof UIFileGroup)) {
return true;
}
/* all other moves are illegal */
return false;
}
/*-------------------------------------------------------------------------------------*/
/* (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 fileGroupId. Note that all error checking was done by canMoveShape().
*/
int x = context.getX();
int y = context.getY();
UIFileGroup fileGroup =
(UIFileGroup)GraphitiUtils.getBusinessObject(context.getPictogramElement());
int fileGroupId = fileGroup.getId();
/*
* Are we moving a UIFileGroup around the Diagram?
*/
Object targetObject = context.getTargetContainer();
if (targetObject instanceof Diagram) {
/* determine the UIFileGroups's old location */
MemberLocation oldXY = pkgMemberMgr.getMemberLocation(IPackageMemberMgr.TYPE_FILE_GROUP,
fileGroupId);
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 */
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
op.recordLocationChange(oldXY.x, oldXY.y, x, y);
new UndoOpAdapter("Move File Group", op).invoke();
}
else {
Object targetBo = GraphitiUtils.getBusinessObject(context.getTargetContainer());
if (targetBo instanceof UIAction) {
/* create a connection arrow between the file group and the action */
int actionId = ((UIAction)targetBo).getId();
moveOntoAction(fileGroupId, actionId);
}
else if (targetBo instanceof UIFileGroup) {
/* create merge file groups */
int targetFileGroupId = ((UIFileGroup)targetBo).getId();
moveOntoFileGroup(fileGroupId, targetFileGroupId);
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Yes, we can delete UIFileGroups.
*/
@Override
public boolean canDelete(IDeleteContext context) {
return true;
}
/*-------------------------------------------------------------------------------------*/
/**
* Invoked when the user initiates a "delete" operation on a UIFileGroup.
*/
@Override
public void delete(IDeleteContext context) {
/* determine the business object that related to the pictogram being deleted */
PictogramLink pl = context.getPictogramElement().getLink();
UIFileGroup fileGroup = (UIFileGroup)(pl.getBusinessObjects().get(0));
int fileGroupId = fileGroup.getId();
/* add the "delete" operation to our redo/undo stack */
MultiUndoOp multiOp = new MultiUndoOp();
/* first, delete all slots that refer to this file group */
MemberDesc neighbours[] = pkgMemberMgr.getNeighboursOf(
IPackageMemberMgr.TYPE_FILE_GROUP, fileGroupId, IPackageMemberMgr.NEIGHBOUR_ANY, false);
for (int i = 0; i < neighbours.length; i++) {
removeReferenceFromNeighbour(multiOp, fileGroupId, neighbours[i].memberType, neighbours[i].memberId);
}
/* now delete the file group itself */
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId);
op.recordMembershipChange(getFileGroupAsArrayList(fileGroupId), new ArrayList<Integer>());
multiOp.add(op);
/* invoke all changes in one step... */
new UndoOpAdapter("Delete File Group", multiOp).invoke();
}
/*-------------------------------------------------------------------------------------*/
/**
* Create a new merge file group, based off one or more existing file groups. This method
* assumes that creation operation was invoked via a GUI operation, and therefore the
* change is added to the undo/redo stack.
*
* @param pkgId The package that the file group will be placed in.
* @param subGroupIds The list of sub file group IDs to add into the merge list.
*/
public void createMergeFileGroup(int pkgId, List<Integer> subGroupIds) {
/* must have at least one member */
if (subGroupIds.size() < 1) {
return;
}
/*
* Create a new merge group. This will allocate an empty group, providing
* us with the group ID.
*/
int mergeFileGroupId = fileGroupMgr.newMergeGroup(pkgId);
if (mergeFileGroupId == ErrorCode.NOT_FOUND) {
AlertDialog.displayErrorDialog("Error Creating Merge Group",
"Unable to create a new merge group in the database.");
return;
}
/*
* Position the new merge group at the y-axis mid point of all the
* existing sub groups, but to the right of the right-most sub group.
* First, find the "average" (y-axis) and "maximum" (x-axis) of all sub
* file groups.
*/
int totalY = 0, maxX = 0;
for (Iterator<Integer> iterator = subGroupIds.iterator(); iterator.hasNext();) {
int subGroupId = (Integer) iterator.next();
MemberLocation subGroupLocation = pkgMemberMgr.getMemberLocation(
IPackageMemberMgr.TYPE_FILE_GROUP, subGroupId);
if (subGroupLocation == null) {
AlertDialog.displayErrorDialog("Error Positioning Merge Group",
"Unable to determine the current position of a source file group");
return;
}
totalY += subGroupLocation.y;
if (subGroupLocation.x > maxX) {
maxX = subGroupLocation.x;
}
}
int averageY = totalY / subGroupIds.size();
/*
* Now position the new merge group to the right of the right-most sub group.
*/
maxX += layoutAlgorithm.getSizeOfPictogram(IPackageMemberMgr.TYPE_FILE_GROUP).getWidth();
maxX += layoutAlgorithm.getXPadding();
if (pkgMemberMgr.setMemberLocation(
IPackageMemberMgr.TYPE_FILE_GROUP, mergeFileGroupId,
maxX, averageY) != ErrorCode.OK) {
AlertDialog.displayErrorDialog("Error Positioning Merge Group",
"Unable to set position for new merge group");
return;
}
/*
* Finally, generate an undo/redo operation to add the members to the group.
* If this operation is "undoned" then the group will still exist in the
* database, but will have no members and therefore won't be shown.
*/
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, mergeFileGroupId);
op.recordMembershipChange(new ArrayList<Integer>(), subGroupIds);
new UndoOpAdapter("Create Merge Group", op).invoke();
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Given that a UIFileGroup is being deleted, we need to go through and first remove the
* connection to any neighbours who reference this file group.
*
* @param multiOp The undo/redo multi-operation to add our "delete connections" to.
* @param fileGroupId The ID of the file group that has been removed.
* @param memberType The type of neighbour (IPackageMemberMgr.TYPE_ACTION, etc).
* @param memberId The ID of the neighbour (e.g. actionId).
*/
private void removeReferenceFromNeighbour(MultiUndoOp multiOp,
int fileGroupId, int memberType, int memberId) {
/*
* For actions...
*/
if (memberType == IPackageMemberMgr.TYPE_ACTION) {
/* get the list of slots associated with this action */
int actionTypeId = actionMgr.getActionType(memberId);
if (actionTypeId == ErrorCode.NOT_FOUND) {
return; /* invalid action type - ignore */
}
SlotDetails slots[] = actionTypeMgr.getSlots(actionTypeId, ISlotTypes.SLOT_POS_ANY);
/* for each slot that's a file group, see if it references our file group */
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(memberId, slotId);
if (slotValue instanceof Integer) {
if (slotValue.equals(Integer.valueOf(fileGroupId))) {
ActionUndoOp op = new ActionUndoOp(buildStore, memberId);
op.recordSlotRemove(slotId, fileGroupId);
multiOp.add(op);
}
}
}
}
}
/*
* Else if this UIFileGroup is somehow embedded in a neighbouring file group.
*/
else if (memberType == IPackageMemberMgr.TYPE_FILE_GROUP) {
int fgType = fileGroupMgr.getGroupType(memberId);
/* for merge groups, remove any/all references we have to the deleted group */
if (fgType == IFileGroupMgr.MERGE_GROUP) {
Integer members[] = fileGroupMgr.getSubGroups(memberId);
for (int i = 0; i < members.length; i++) {
if (members[i] == fileGroupId) {
List<Integer> oldMembers = Arrays.asList(members);
List<Integer> newMembers = new ArrayList<Integer>(Arrays.asList(members));
while (newMembers.remove(Integer.valueOf(fileGroupId))) { /* remove delete group multiple times? */};
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, memberId);
op.recordMembershipChange(oldMembers, newMembers);
multiOp.add(op);
break;
}
}
}
else if (fgType == IFileGroupMgr.FILTER_GROUP) {
// TODO: implement this.
}
}
/*
* For everything else...
*/
else {
throw new FatalError("Unhandled memberType");
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Fetch the members of a file group (path IDs, or sub-group IDs) and return them as
* an ArrayList. This is used for recording history changes.
*
* @param fileGroupId The ID of the file group to get the content of.
* @return An ArrayList<Integer> containing the members.
*/
private ArrayList<Integer> getFileGroupAsArrayList(int fileGroupId) {
ArrayList<Integer> result = new ArrayList<Integer>();
int groupSize = fileGroupMgr.getGroupSize(fileGroupId);
if (groupSize < 0) {
return result;
}
int groupType = fileGroupMgr.getGroupType(fileGroupId);
for (int i = 0; i != groupSize; i++) {
int pathId;
if (groupType == IFileGroupMgr.MERGE_GROUP) {
pathId = fileGroupMgr.getSubGroup(fileGroupId, i);
} else {
pathId = fileGroupMgr.getPathId(fileGroupId, i);
}
if (pathId < 0) {
return result;
}
result.add(pathId);
}
return result;
}
/*-------------------------------------------------------------------------------------*/
/**
* Append a file path onto the end of the "currentMembers" array. After the "drag"
* operation is complete, we'll add all of these files into the file group.
*
* @param fullPath The absolute path to add to the file group.
* @return ErrorCode.OK on success, or a relevant error code.
*/
private int addToFileGroup(String fullPath) {
/* convert the path into an absolute path (not workspace-relative) */
fullPath = EclipsePartUtils.workspaceRelativeToAbsolutePath(fullPath);
/* now append the path to the array of members for this drag operation */
int newPathId = fileMgr.addFile(fullPath);
if (newPathId == ErrorCode.BAD_PATH) {
return newPathId; /* couldn't add the path - return an error */
}
currentMembers.add(newPathId);
/* all is good */
return ErrorCode.OK;
}
/*-------------------------------------------------------------------------------------*/
/**
* Add a new pictogram, representing a UIFileGroup, onto a Graphiti Diagram.
*
* @param targetDiagram The Diagram to add the pictogram to.
* @param addedFileGroup The UIFileGroup to add a pictogram for.
* @param x The X location within the Diagram.
* @param y The Y location within the Diagram.
* @return The ContainerShape representing the FileGroup pictogram.
*/
private PictogramElement renderPictogram(Diagram targetDiagram, UIFileGroup addedFileGroup,
int x, int y) {
IFileGroupMgr fileGroupMgr = buildStore.getFileGroupMgr();
int fileGroupId = addedFileGroup.getId();
int fileGroupType = fileGroupMgr.getGroupType(fileGroupId);
/*
* How many boxes will be shown? This helps us distinguish between file groups
* containing a single file, versus multiple files. With multiple files, we
* draw a second page that appears behind the front page.
*/
int groupSize = fileGroupMgr.getGroupSize(addedFileGroup.getId());
if (groupSize <= 0) {
return null; /* an empty file group (or an errored file group) shouldn't be shown */
}
/*
* We can show a limited number of file names in the pictogram. If more than our
* maximum, display the last name as "...". For merge file groups, we don't show file
* names underneath the pictogram.
*/
int fileNamesToShow;
if (fileGroupType == IFileGroupMgr.MERGE_GROUP) {
fileNamesToShow = 0;
} else {
fileNamesToShow = (groupSize <= MAX_LABELS_TO_SHOW) ? groupSize : MAX_LABELS_TO_SHOW;
}
/* create a container that holds the pictogram */
IPeCreateService peCreateService = Graphiti.getPeCreateService();
IGaService gaService = Graphiti.getGaService();
ContainerShape containerShape =
peCreateService.createContainerShape(targetDiagram, true);
/*
* Create an invisible outer rectangle. The smaller polygons and labels will be placed
* inside this. The width is always twice that of the polygon, to allow for long file
* names to be shown underneath the polygons. The height is carefully selected to allow
* for enough labels, as well as for the possible "second page" polygon.
*/
Rectangle invisibleRectangle =
gaService.createInvisibleRectangle(containerShape);
gaService.setLocationAndSize(invisibleRectangle, x, y,
2 * FILE_GROUP_WIDTH,
FILE_GROUP_HEIGHT + ((groupSize < 2) ? 0 : FILE_GROUP_OVERLAP) +
((fileNamesToShow + 1) * (LABEL_FONT_SIZE + LABEL_FONT_GAP)));
/*
* Create the visible file icon within the outer shape. First, consider whether
* a second page (with no corner bend) should be shown in the background.
*/
if (groupSize > 1) {
Polygon box = gaService.createPolygon(invisibleRectangle, coordsPage2);
box.setForeground(manageColor(LINE_COLOUR));
box.setBackground(manageColor(FILL_COLOUR));
box.setLineWidth(2);
}
/* now the front page, with the corner bent */
Polygon box = gaService.createPolygon(invisibleRectangle, coordsPage1);
box.setForeground(manageColor(LINE_COLOUR));
box.setBackground(manageColor(FILL_COLOUR));
box.setLineWidth(2);
Polygon boxCorner = gaService.createPolygon(invisibleRectangle, coordsCorner);
boxCorner.setForeground(manageColor(LINE_COLOUR));
boxCorner.setBackground(manageColor(FILL_CORNER_COLOUR));
boxCorner.setLineWidth(2);
/*
* For source/generated groups, display the file group's content (or at least part
* of it) under the file box. We can only show a limited number of file names. If
* there are too many to show, the last label must be "...".
*/
if (fileGroupType != IFileGroupMgr.MERGE_GROUP) {
for (int i = 0; i != fileNamesToShow; i++) {
String value;
if ((i == (MAX_LABELS_TO_SHOW - 1)) && (groupSize > MAX_LABELS_TO_SHOW)) {
value = "...";
} else {
/* fetch this particular file's base name */
int pathId = fileGroupMgr.getPathId(fileGroupId, i);
if (pathId < 0) {
value = "";
} else {
value = fileMgr.getBaseName(pathId);
}
}
/* draw the label underneath the main "page" polygon */
Text fileNames = gaService.createText(getDiagram(), invisibleRectangle,
value, LABEL_FONT, LABEL_FONT_SIZE);
fileNames.setFilled(false);
fileNames.setForeground(manageColor(TEXT_FOREGROUND));
fileNames.setHorizontalAlignment(Orientation.ALIGNMENT_CENTER);
gaService.setLocationAndSize(fileNames,
0, FILE_GROUP_HEIGHT + ((1 + i) * (LABEL_FONT_SIZE + LABEL_FONT_GAP)),
FILE_GROUP_WIDTH * 2, (LABEL_FONT_SIZE + LABEL_FONT_GAP));
}
}
/*
* For merge groups, we draw three extra lines inside the file group box as
* an indication to the user that it's a merge group.
*/
else {
for (int i = 0; i < coordsMergeLines.length; i++) {
Polyline mergeLine = gaService.createPolyline(invisibleRectangle, coordsMergeLines[i]);
mergeLine.setForeground(manageColor(LINE_COLOUR));
mergeLine.setBackground(manageColor(FILL_COLOUR));
mergeLine.setLineWidth(1);
}
}
/*
* Add anchors that reside to the immediate left/right of the file group box.
* There are for drawing connection arrows.
* UIFileActionConnection.INPUT_TO_ACTION (0) - right anchor
* UIFileActionConnection.OUTPUT_FROM_ACTION (1) - left anchor
*/
FixPointAnchor rightAnchor = peCreateService.createFixPointAnchor(containerShape);
rightAnchor.setLocation(
gaService.createPoint(OFF_X + FILE_GROUP_WIDTH + (FILE_GROUP_OVERLAP * 2), FILE_GROUP_HEIGHT / 2));
gaService.createInvisibleRectangle(rightAnchor);
FixPointAnchor leftAnchor = peCreateService.createFixPointAnchor(containerShape);
leftAnchor.setLocation(
gaService.createPoint(OFF_X, FILE_GROUP_HEIGHT / 2));
gaService.createInvisibleRectangle(leftAnchor);
/* create a link between the shape and the business object, and display it. */
link(containerShape, addedFileGroup);
layoutPictogramElement(containerShape);
return containerShape;
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine whether the specified object is something in the Eclipse IDE that represents
* a "disk file" (but not a directory).
*
* @param fileObject An object to be tested.
* @return true if this object represents some type of disk file in the Eclipse IDE.
*/
private boolean isFileClass(Object fileObject) {
return getPathOf(fileObject) != null;
}
/*-------------------------------------------------------------------------------------*/
/**
* Given something that represents a file within the Eclipse IDE, return the full OS-specific
* path to that file.
*
* @param fileObject The object that represents a file.
* @return The full OS-specific path to the file, or null if this is not a file.
*/
private String getPathOf(Object fileObject) {
/* all IFile objects are files (not directories) */
if (fileObject instanceof IFile) {
IFile file = (IFile)fileObject;
return file.getFullPath().toOSString();
}
/*
* Anything that adapts to IResource might be a file, but we need to double check
* that it's not a directory.
*/
else if (fileObject instanceof IAdaptable) {
Object iResource = ((IAdaptable)fileObject).getAdapter(IResource.class);
if (iResource != null) {
IPath location = ((IResource)iResource).getLocation();
if ((location != null) && (location.toFile().isFile())) {
return location.toOSString();
}
}
}
return null;
}
/*-------------------------------------------------------------------------------------*/
/**
* If necessary, bump the pictograms to the right. That is, if the fileGroup was just
* connected to an action's input, and that fileGroup is to the right (higher x-coord)
* of the action, then bump the action (and all other downstream members) to the right,
* so their x-coordinates will be higher than the fileGroup. If connected to the
* action's output, but it's left of the action, bump the file group (and downstream
* members) to the right.
*
* @param multiOp The multi-undo/redo operation to append "move" actions to.
* @param fileGroupId The ID of the file group that just got connected.
* @param actionId The ID of the action it was connected to.
* @param slotId The slot (within the action) it was connected to.
* @param moveNow True if this method should invoke the moves (false simply
* adds the details to operation).
*/
private void bumpPictogramsRight(MultiUndoOp multiOp, int fileGroupId,
int actionId, int slotId, boolean moveNow) {
/* determine whether the slot was INPUT or OUTPUT */
SlotDetails details = actionTypeMgr.getSlotByID(slotId);
if (details == null) {
return; /* shouldn't happen */
}
/*
* Where do we start bumping from? The file group, or the action? It really
* depends on what type of slot it is.
*/
int memberType, memberId;
if (details.slotPos == ISlotTypes.SLOT_POS_INPUT) {
memberType = IPackageMemberMgr.TYPE_FILE_GROUP;
memberId = fileGroupId;
} else {
memberType = IPackageMemberMgr.TYPE_ACTION;
memberId = actionId;
}
/* start the bumping (recursively) */
layoutAlgorithm.bumpPictogramsRight(multiOp, memberType, memberId, moveNow);
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper method for moveShape() that handles the case where a file group is moved on
* top of an action, in which case a connection arrow must be added.
*
* @param fileGroupId ID of the file group that was moved.
* @param actionId ID of the action that the file group was moved onto.
*/
private void moveOntoAction(int fileGroupId, int actionId) {
/*
* We're connecting a file group to an action's slot. First, pop up a dialog
* to ask the user which slot, then proceed to set it.
*/
SlotSelectionDialog dialog = new SlotSelectionDialog(buildStore, actionId, true, true);
int status = dialog.open();
if (status == SlotSelectionDialog.OK) {
int slotId = dialog.getSlotId();
Object currentValue = actionMgr.getSlotValue(actionId, slotId);
/* make the change - checking for cycles in the diagram */
int error = actionMgr.setSlotValue(actionId, slotId, fileGroupId);
if (error == ErrorCode.LOOP_DETECTED) {
AlertDialog.displayErrorDialog("Can't Connect File Group",
"Adding this connection would cause a cycle in the package diagram.");
return;
}
/* record an undo/redo operation that will actually change the slot value */
MultiUndoOp multiOp = new MultiUndoOp();
ActionUndoOp op = new ActionUndoOp(buildStore, actionId);
op.recordSlotChange(slotId, currentValue, Integer.valueOf(fileGroupId));
multiOp.add(op);
/* if necessary, bump pictograms to the right */
bumpPictogramsRight(multiOp, fileGroupId, actionId, slotId, true);
/* record these operations for future undo/redo operations */
new UndoOpAdapter("Connect File Group", multiOp).record();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper method for moveShape() that handles the case where a file group is dragged
* on top of another file group. Either of the file groups can be SOURCE, GENERATED
* or MERGE groups. The following cases are handled:
* 1) SOURCE/GENERATED/MERGE dropped on SOURCE/GENERATED -> creates a new MERGE group that
* they both feed into.
* 2) SOURCE/GENERATED/MERGE dropped on MERGE GROUP -> adds source group into MERGE GROUP.
*
* @param sourceFileGroupId ID of the file group that was dragged.
* @param targetFileGroupId ID of the file group it was dragged onto.
*/
private void moveOntoFileGroup(int sourceFileGroupId, int targetFileGroupId) {
/* determine type of each file group (SOURCE, GENERATED, MERGE) */
int sourceGroupType = fileGroupMgr.getGroupType(sourceFileGroupId);
int targetGroupType = fileGroupMgr.getGroupType(targetFileGroupId);
if ((sourceGroupType == ErrorCode.NOT_FOUND) || (targetGroupType == ErrorCode.NOT_FOUND)) {
return; /* error - just ignore the move */
}
/*
* Handle case where we drag onto an existing MERGE file group. The
* source is simply added into the destination.
*/
if (targetGroupType == IFileGroupMgr.MERGE_GROUP) {
ArrayList<Integer> currentMembers = getFileGroupAsArrayList(targetFileGroupId);
int error = fileGroupMgr.addSubGroup(targetFileGroupId, sourceFileGroupId);
if (error == ErrorCode.LOOP_DETECTED) {
AlertDialog.displayErrorDialog("Can't Connect File Group",
"Adding this connection would cause a cycle in the package diagram.");
}
else if (error < 0) {
AlertDialog.displayErrorDialog("Error Adding to Merge Group",
"For some reason, the file group could not be added to the merge group");
}
/*
* Now record the membership change in an operation, so we can undo/redo it later. This
* may also involve bumping pictograms to the right.
*/
MultiUndoOp multiOp = new MultiUndoOp();
FileGroupUndoOp op = new FileGroupUndoOp(buildStore, targetFileGroupId);
ArrayList<Integer> newMembers = getFileGroupAsArrayList(targetFileGroupId);
op.recordMembershipChange(currentMembers, newMembers);
multiOp.add(op);
/* bump to the right (now), and record changes in undo/redo stack */
layoutAlgorithm.bumpPictogramsRight(multiOp, IPackageMemberMgr.TYPE_FILE_GROUP, sourceFileGroupId, true);
/* record, but don't execute now, since we've already added the members and bumped the pictograms */
new UndoOpAdapter("Add to Merge Group", multiOp).record();
return;
}
/*
* Handle case where we dragged a SOURCE/GENERATED/MERGE group onto an existing SOURCE
* or GENERATED group. This results in the creation of a new MERGE group.
*/
else {
List<Integer> subGroupIds = new ArrayList<Integer>();
subGroupIds.add(sourceFileGroupId);
subGroupIds.add(targetFileGroupId);
createMergeFileGroup(editor.getPackageId(), subGroupIds);
}
}
/*-------------------------------------------------------------------------------------*/
}