/*******************************************************************************
* Copyright (c) 2013 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;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.graphiti.dt.IDiagramTypeProvider;
import org.eclipse.graphiti.features.context.IDoubleClickContext;
import org.eclipse.graphiti.features.context.IPictogramElementContext;
import org.eclipse.graphiti.features.custom.ICustomFeature;
import org.eclipse.graphiti.mm.algorithms.GraphicsAlgorithm;
import org.eclipse.graphiti.mm.pictograms.PictogramElement;
import org.eclipse.graphiti.tb.DefaultToolBehaviorProvider;
import org.eclipse.graphiti.tb.IContextButtonPadData;
import com.buildml.eclipse.bobj.UIAction;
import com.buildml.eclipse.bobj.UIFileActionConnection;
import com.buildml.eclipse.bobj.UIFileGroup;
import com.buildml.eclipse.bobj.UIMergeFileGroupConnection;
import com.buildml.eclipse.bobj.UISubPackage;
import com.buildml.eclipse.packages.features.PackageDiagramDoubleClickFeature;
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.IPackageMgr;
import com.buildml.model.ISlotTypes;
import com.buildml.model.ISlotTypes.SlotDetails;
import com.buildml.model.ISubPackageMgr;
import com.buildml.utils.print.PrintUtils;
/**
* A "provider" class that implements diagram-viewing/editing behaviour for the BuildML
* "package diagram". This includes such things as context menus, double-click behaviour
* and tool tips.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class PackageToolBehaviorProvider extends DefaultToolBehaviorProvider {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** The BuildStore associated with this diagram */
private IBuildStore buildStore;
/** The IActionMgr associated with this BuildStore */
private IActionMgr actionMgr;
/** The IActionTypeMgr associated with this BuildStore */
private IActionTypeMgr actionTypeMgr;
/** The IFileGroupMgr associated with this BuildStore */
private IFileGroupMgr fileGroupMgr;
/** The IFileMgr associated with this BuildStore */
private IFileMgr fileMgr;
/** The ISubPackageMgr associated with this BuildStore */
private ISubPackageMgr subPkgMgr;
/** The IPackageMgr associated with this BuildStore */
private IPackageMgr pkgMgr;
/** The maximum number of characters wide that a tooltip should be */
private final int toolTipWrapWidth = 120;
/** The maximum number of file group members we should show in the tool-tip */
private final int toolTipMaxFileGroupLines = 20;
/** The custom graphiti feature for handling double-clicks */
private ICustomFeature doubleClickFeature = null;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new PackageToolBehaviorProvider.
* @param dtp The diagram type provider that owns this provider.
*/
public PackageToolBehaviorProvider(IDiagramTypeProvider dtp) {
super(dtp);
/* figure out the IBuildStore associated with this editor */
PackageDiagramEditor pde = (PackageDiagramEditor)dtp.getDiagramEditor();
buildStore = pde.getBuildStore();
actionMgr = buildStore.getActionMgr();
actionTypeMgr = buildStore.getActionTypeMgr();
fileGroupMgr = buildStore.getFileGroupMgr();
fileMgr = buildStore.getFileMgr();
subPkgMgr = buildStore.getSubPackageMgr();
pkgMgr = buildStore.getPackageMgr();
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* For a specific business object on the diagram, compute the tool-tip that the user
* will see if they hover their mouse for long enough. This is only computed once for
* each business object. The method used to determine the tool-tip is dependent on
* object type.
*/
@Override
public String getToolTip(GraphicsAlgorithm ga) {
/* Determine the business object that we're computing the tool-tip for */
PictogramElement pe = ga.getPictogramElement();
Object bo = getFeatureProvider().getBusinessObjectForPictogramElement(pe);
/*
* For actions (UIAction), fetch the command string from the BuildStore.
*/
if (bo instanceof UIAction) {
UIAction action = (UIAction)bo;
int actionId = action.getId();
/* fetch the action's command string, and directory path */
String actionString = (String) actionMgr.getSlotValue(actionId, IActionMgr.COMMAND_SLOT_ID);
Object dirSlotValue = actionMgr.getSlotValue(actionId, IActionMgr.DIRECTORY_SLOT_ID);
String dirString = null;
if (dirSlotValue != null) {
int dirId = (Integer)dirSlotValue;
dirString = fileMgr.getPathName(dirId);
}
/* format the command string nicely, wrapping the command if it's long */
if (actionString != null) {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(outStream);
if (dirString != null) {
printStream.println("Directory:\n" + dirString);
}
printStream.println("\nShell command:");
PrintUtils.indentAndWrap(printStream, actionString, 0, toolTipWrapWidth);
String toolTip = outStream.toString();
printStream.close();
return toolTip;
}
}
/*
* For file groups, show the content of the file group.
*/
else if (bo instanceof UIFileGroup) {
UIFileGroup fileGroup = (UIFileGroup)bo;
int fileGroupId = fileGroup.getId();
StringBuilder sb = new StringBuilder();
displayFileGroupMembers(fileGroupId, sb);
return sb.toString();
}
/*
* For UIFileActionConnection, we can be going into, or going out of an action.
*/
else if (bo instanceof UIFileActionConnection) {
UIFileActionConnection connection = (UIFileActionConnection)bo;
SlotDetails details = actionTypeMgr.getSlotByID(connection.getSlotId());
if (details == null) {
return null;
}
/* for output connections, just show the slot we come out of */
if (connection.getDirection() == UIFileActionConnection.OUTPUT_FROM_ACTION) {
return "\n Output from action slot \"" + details.slotName + "\" \n";
}
/* for input connections, show the slot and the (optional) filter group content */
else {
int filterGroupId = connection.getFilterGroupId();
StringBuilder sb = new StringBuilder();
sb.append("\n Input to action slot \"");
sb.append(details.slotName);
sb.append("\" \n");
if (connection.hasFilter()) {
displayFilterPatterns(filterGroupId, sb);
displayFileGroupMembers(filterGroupId, sb);
}
return sb.toString();
}
}
/*
* for UIMergeFileGroupConnection, show the sub group's 1-based position inside the
* merge group. If there's a filter on the connection, also show the output of
* the filter group.
*/
else if (bo instanceof UIMergeFileGroupConnection) {
UIMergeFileGroupConnection connection = (UIMergeFileGroupConnection)bo;
int subGroupId = connection.getSourceFileGroupId();
int mergeGroupId = connection.getTargetFileGroupId();
int myIndex = connection.getIndex() + 1;
StringBuilder sb = new StringBuilder();
/* display this subgroup's various positions in the merge group */
displayMergeGroupIndicies(subGroupId, mergeGroupId, myIndex, sb);
/* if there's a filter attached, so the patterns and resulting output */
if (connection.hasFilter()) {
int filterGroupId = connection.getFilterGroupId();
displayFilterPatterns(filterGroupId, sb);
displayFileGroupMembers(filterGroupId, sb);
}
return sb.toString();
}
/*
* For UISubPackage, so the full name of the package type.
*/
else if (bo instanceof UISubPackage) {
UISubPackage subPkg = (UISubPackage)bo;
int subPkgId = subPkg.getId();
/* compute the sub-package's type */
int pkgTypeId = subPkgMgr.getSubPackageType(subPkgId);
if (pkgTypeId < 0) {
return null;
}
/* display the type name (which is itself a package name) */
StringBuilder sb = new StringBuilder();
String pkgTypeName = pkgMgr.getName(pkgTypeId);
if (pkgTypeName != null) {
sb.append("\n Sub-Package: ");
sb.append(pkgTypeName);
sb.append(" \n\n");
}
/* Display the parameter values for this sub-package */
SlotDetails slots[] = pkgMgr.getSlots(pkgTypeId, ISlotTypes.SLOT_POS_PARAMETER);
if (slots != null) {
for (SlotDetails details : slots) {
Object value = subPkgMgr.getSlotValue(subPkgId, details.slotId);
sb.append(" - ");
sb.append(details.slotName);
sb.append(": ");
/* null values can't be displayed */
if (value == null) {
sb.append("<undefined>");
/* directories/files should be expanded into their path */
} else if ((details.slotType == ISlotTypes.SLOT_TYPE_DIRECTORY) ||
(details.slotType == ISlotTypes.SLOT_TYPE_FILE)) {
int pathId = (Integer)value;
String pathName = fileMgr.getPathName(pathId);
if (pathName == null) {
sb.append("<invalid>");
} else {
sb.append(pathName);
}
}
/* other types can be displayed directly */
else {
sb.append(value.toString());
}
sb.append(" \n");
}
}
return sb.toString();
}
/* else, return the default tooltip */
return (String) super.getToolTip(ga);
}
/*-------------------------------------------------------------------------------------*/
/**
* We don't want to show the fly-in palette, since the user can't use it anyway.
*/
@Override
public boolean isShowFlyoutPalette() {
return false;
}
/*-------------------------------------------------------------------------------------*/
/**
* Determine which context buttons are displayed around the UI icons.
*/
@Override
public IContextButtonPadData getContextButtonPad(
IPictogramElementContext context) {
IContextButtonPadData data = super.getContextButtonPad(context);
PictogramElement pe = context.getPictogramElement();
/* for now, disable all context buttons */
setGenericContextButtons(data, pe, 0);
return data;
}
/*-------------------------------------------------------------------------------------*/
/**
* Handle double-clicking on a UI icon.
*/
@Override
public ICustomFeature getDoubleClickFeature(IDoubleClickContext context) {
/* if we don't already have a custom feature for double-clicking, create one */
if (doubleClickFeature == null) {
doubleClickFeature =
new PackageDiagramDoubleClickFeature(getFeatureProvider(), buildStore);
}
/* can our custom feature handle this event? */
if (doubleClickFeature.canExecute(context)) {
return doubleClickFeature;
}
/* else, default to standard handler */
return super.getDoubleClickFeature(context);
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Helper function for appending the list of a file group's members onto a StringBuilder.
*
* @param fileGroupId ID of the file group to display the content of.
* @param stringBuffer The StringBuilder to append the member names to.
*/
private void displayFileGroupMembers(int fileGroupId, StringBuilder stringBuffer) {
String files[] = fileGroupMgr.getExpandedGroupFiles(fileGroupId);
int fileGroupType = fileGroupMgr.getGroupType(fileGroupId);
stringBuffer.append("\n ");
if (fileGroupType == IFileGroupMgr.FILTER_GROUP) {
stringBuffer.append("Files passing through filter: \n");
} else {
String typeName = (fileGroupType == IFileGroupMgr.SOURCE_GROUP) ? "Source" :
((fileGroupType == IFileGroupMgr.GENERATED_GROUP) ? "Generated" : "Merge");
stringBuffer.append(typeName);
stringBuffer.append(" File Group: \n");
}
int halfMaxLines = toolTipMaxFileGroupLines / 2;
for (int i = 0; i < files.length; i++) {
if ((i < halfMaxLines) || (i >= (files.length - halfMaxLines))) {
stringBuffer.append(' ');
stringBuffer.append(files[i]);
stringBuffer.append(" \n");
}
else if (i == halfMaxLines) {
stringBuffer.append(" ...\n");
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper function for displaying the patterns in a filter file group.
* @param filterGroupId ID of the filter group to display the patterns from.
* @param sb The StringBuilder to append output to.
*/
private void displayFilterPatterns(int filterGroupId, StringBuilder sb) {
String filterStrings[] = fileGroupMgr.getPathStrings(filterGroupId);
sb.append("\n Filter Patterns:\n");
for (int i = 0; i < filterStrings.length; i++) {
sb.append(" ");
String pattern = filterStrings[i];
String[] parts = pattern.split(":");
if (parts.length != 2) {
sb.append("Invalid:");
sb.append(pattern);
} else if (parts[0].equals("ia")) {
sb.append("Include: ");
sb.append(parts[1]);
} else if (parts[0].equals("ea")) {
sb.append("Exclude: ");
sb.append(parts[1]);
} else {
sb.append("Invalid Prefix:");
sb.append(parts[0]);
}
sb.append(" \n");
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper method for displaying a connection's index positions within a merge file group.
* Any particular sub group may appear in multiple places in the merge group, so we
* provide a meaningful list of all index positions.
*
* @param subGroupId The ID of the sub group at one end of the connection.
* @param mergeGroupId The ID of the merge group at the other end.
* @param myIndex The merge group's index for this connection.
* @param sb The StringBuilder to append content onto.
*/
private void displayMergeGroupIndicies(
int subGroupId, int mergeGroupId, int myIndex, StringBuilder sb) {
/*
* Find the index (or indicies) where the sub group appears in the merge group, skipping
* over filters if they exist.
*/
Integer subGroups[] = fileGroupMgr.getSubGroups(mergeGroupId);
if (subGroups == null) {
return;
}
List<Integer> indexList = new ArrayList<Integer>();
for (int i = 0; i < subGroups.length; i++) {
int thisSubGroupId = subGroups[i];
/* skip over filters - important with multiple connections from the same sub group */
int thisSubGroupType = fileGroupMgr.getGroupType(thisSubGroupId);
if (thisSubGroupType == IFileGroupMgr.FILTER_GROUP) {
thisSubGroupId = fileGroupMgr.getPredId(thisSubGroupId);
}
/* if our subgroup is in this index position, record */
if (thisSubGroupId == subGroupId) {
indexList.add(i + 1);
}
}
/*
* Now pretty-print the message, providing a list of all indicies. Our connection's
* position is highlighted with "(this link)".
*/
sb.append("\n File group merged into position");
if (indexList.size() > 1) {
sb.append('s');
}
int size = indexList.size();
for (int i = 0; i < size; i++) {
if (i == 0) {
sb.append(' ');
}
else if (i == (size - 1)) {
sb.append(" and ");
}
else {
sb.append(", ");
}
int index = indexList.get(i);
sb.append(index);
if ((size > 1) && (index == myIndex)) {
sb.append(" (this link)");
}
}
sb.append(" \n");
}
/*-------------------------------------------------------------------------------------*/
}