/******************************************************************************* * 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.handlers; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.graphiti.ui.platform.GraphitiConnectionEditPart; import org.eclipse.graphiti.ui.platform.GraphitiShapeEditPart; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.handlers.HandlerUtil; import com.buildml.eclipse.MainEditor; import com.buildml.eclipse.bobj.UIAction; import com.buildml.eclipse.bobj.UIDirectory; import com.buildml.eclipse.bobj.UIFile; import com.buildml.eclipse.bobj.UIFileGroup; import com.buildml.eclipse.packages.PackageDiagramEditor; import com.buildml.eclipse.packages.layout.LayoutAlgorithm; 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.handlers.AbstractHandlerWithProgress; import com.buildml.model.IBuildStore; import com.buildml.model.IFileMgr; import com.buildml.model.IPackageMemberMgr; import com.buildml.model.IPackageMemberMgr.MemberDesc; import com.buildml.model.IPackageRootMgr; import com.buildml.model.undo.IUndoOp; import com.buildml.model.undo.MultiUndoOp; import com.buildml.refactor.CanNotRefactorException; import com.buildml.refactor.CanNotRefactorException.Cause; import com.buildml.refactor.IImportRefactorer; import com.buildml.utils.errors.ErrorCode; /** * An Eclipse UI Handler for managing the "Move to Package" UI command. * * @author Peter Smith <psmith@arapiki.com> */ public class HandlerMoveToPackage extends AbstractHandlerWithProgress { /*=====================================================================================* * NESTED CLASSES *=====================================================================================*/ /** * A private IUndoOp for laying out a package. We encapsulate this as an IUndoOp so * that the layout operation can be placed into a MultiUndoOp and performed in the same * operation as the import. Layout requires the import to be complete before it can * compute the layout information, so this operation must be sequenced appropriately. * * @author Peter Smith <psmith@arapiki.com> */ private class LayoutOp implements IUndoOp { /** * This is null the first time "redo" is called, or points to a valid MultiUndoOp * if the layout strategy has already been computed. That is, the layout algorithm * is only executed the first time redo() is called. */ private MultiUndoOp layoutOp = null; /** ID of the package we're laying out */ private int pkgId; /** The layoutAlgorithm to use for laying out the package */ private LayoutAlgorithm layoutAlgorithm; /** * Create a new LayoutOp object. * @param pkgId ID of the package to lay out. * @param layoutAlgorithm The layout algorithm to use. */ public LayoutOp(LayoutAlgorithm layoutAlgorithm, int pkgId) { this.pkgId = pkgId; this.layoutAlgorithm = layoutAlgorithm; } /** * Undo the layout operation. */ @Override public boolean undo() { return layoutOp.undo(); } /** * Perform the operation for the first time, or redo an operation that has previously * been undone. If this is the first time, we must execute the layout algorithm. If * this is a "redo", we just replay the operation we already have. */ @Override public boolean redo() { /* Schedule the individual layout steps - first time only */ if (layoutOp == null) { layoutOp = new MultiUndoOp(); layoutAlgorithm.autoLayoutPackage(layoutOp, pkgId); } /* now actually perform the steps */ return layoutOp.redo(); } } /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** Our IBuildStore */ private IBuildStore buildStore; /** This editor's layout algorithm */ private LayoutAlgorithm layoutAlgorithm; /*=====================================================================================* * PUBLIC METHODS *=====================================================================================*/ /* (non-Javadoc) * @see com.buildml.eclipse.utils.handlers.AbstractHandlerWithProgress#getCommandName() */ @Override public String getCommandName() { return "Moving to Package"; } /*-------------------------------------------------------------------------------------*/ /** * Execute the "Move to Package" command. */ @Override public Object executeWithProgress(ExecutionEvent event) { buildStore = EclipsePartUtils.getActiveBuildStore(); List<MemberDesc> members = getSelectedObjects((IStructuredSelection) HandlerUtil.getCurrentSelection(event)); /* * Show the "which package to move to" Dialog in the UI-thread. */ final Integer pkgIdReturn[] = new Integer[1]; pkgIdReturn[0] = null; Display.getDefault().syncExec(new Runnable() { @Override public void run() { final MoveToPackageDialog dialog = new MoveToPackageDialog(buildStore); int status = dialog.open(); if (status == MoveToPackageDialog.OK) { pkgIdReturn[0] = dialog.getPackageId(); } } }); if (pkgIdReturn[0] != null) { final int pkgId = pkgIdReturn[0]; /* all changes will be packaged in a multiOp */ MultiUndoOp multiOp = new MultiUndoOp(); /* ask the refactorer to perform the move */ final MainEditor editor = EclipsePartUtils.getActiveMainEditor(); if (editor != null) { IImportRefactorer refactorer = editor.getImportRefactorer(); try { /* plan the move to the new package (multiOp will be populated) */ refactorer.moveMembersToPackage(multiOp, pkgId, members); /* * Open the package diagram so the user can see the results. Note * that we do this before the layout operation, since the layout algorithm * is obtained from the currently opened editor. */ Display.getDefault().syncExec(new Runnable() { @Override public void run() { editor.openPackageDiagram(pkgId); PackageDiagramEditor pde = EclipsePartUtils.getActivePackageDiagramEditor(); layoutAlgorithm = pde.getLayoutAlgorithm(); } }); /* schedule the layout operation to happen */ multiOp.add(new LayoutOp(layoutAlgorithm, pkgId)); /* invoke the changes, and record in undo/redo history */ new UndoOpAdapter("Move to Package", multiOp).invoke(); } catch (CanNotRefactorException e) { displayErrorMessage(pkgId, e.getCauseCode(), e.getCauseIDs()); } } } return null; } /*-------------------------------------------------------------------------------------*/ /** * Determine whether this handler is enabled. To do so, all the selected elements must * be valid/recognized business objects, such as UIAction, UIFile, etc. */ @Override public boolean isEnabled() { /* * Get the list of business objects that are selected, return false if an unhandled * object is selected. */ List<MemberDesc> objectList = getSelectedObjects(EclipsePartUtils.getSelection()); return (objectList != null); } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * @param selection The current Eclipse selection * @return A list of valid business objects (UIAction, UIFileGroup, etc) that have been * selected by the user. Returns null if any non-valid objects were selected. Note that * connection arrows are silently ignored, rather than flagged as invalid. */ private List<MemberDesc> getSelectedObjects(IStructuredSelection selection) { /* we'll return a list of valid business objects */ List<MemberDesc> result = new ArrayList<MemberDesc>(); Iterator<Object> iter = selection.iterator(); while (iter.hasNext()) { Object obj = iter.next(); /* Graphiti shapes */ if (obj instanceof GraphitiShapeEditPart) { GraphitiShapeEditPart shape = (GraphitiShapeEditPart)obj; Object bo = GraphitiUtils.getBusinessObject(shape.getPictogramElement()); if (bo instanceof UIAction) { result.add(new MemberDesc(IPackageMemberMgr.TYPE_ACTION, ((UIAction)bo).getId(), 0, 0)); } else if (bo instanceof UIFileGroup) { result.add(new MemberDesc(IPackageMemberMgr.TYPE_FILE_GROUP, ((UIFileGroup)bo).getId(), 0, 0)); } } /* silently ignore connections */ else if (obj instanceof GraphitiConnectionEditPart) { /* silently do nothing - not an error */ } /* Other objects, selectable from TreeViewers (rather than from Graphiti diagrams) */ else if (obj instanceof UIAction){ result.add(new MemberDesc(IPackageMemberMgr.TYPE_ACTION, ((UIAction)obj).getId(), 0, 0)); } else if (obj instanceof UIFile) { result.add(new MemberDesc(IPackageMemberMgr.TYPE_FILE, ((UIFile)obj).getId(), 0, 0)); } else if (obj instanceof UIDirectory) { // TODO: expand this into files? result.add(new MemberDesc(IPackageMemberMgr.TYPE_FILE, ((UIFile)obj).getId(), 0, 0)); } /* else, anything else is invalid */ else { return null; } } /* finally, there must be at least one valid thing selected */ if (result.size() > 0) { return result; } return null; } /*-------------------------------------------------------------------------------------*/ /** * Display a meaningful error message to the user. They need to understand why their * "move to package" operation failed. * * @param pkgId * @param causeCode The exception's cause code. * @param causeIDs The list of ID (actions, files, etc) that caused the problem. */ private void displayErrorMessage(int pkgId, Cause causeCode, Integer[] causeIDs) { IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr(); /* we'll populate this StringBuffer with the error message */ StringBuffer sb = new StringBuffer(); /* * Based on the cause, construct a meaningful message. */ switch (causeCode) { case PATH_OUT_OF_RANGE: sb.append("The following files are not enclosed within the destination package's roots:\n\n"); displayPaths(sb, causeIDs); sb.append("\nThe package's root path is:\n\n"); int pkgRootId = pkgRootMgr.getPackageRoot(pkgId, IPackageRootMgr.SOURCE_ROOT); if (pkgRootId == ErrorCode.NOT_FOUND) { sb.append("<invalid>"); } else { displayPaths(sb, new Integer[] { pkgRootId }); } sb.append("\nTo resolve this issue, consider the following solutions:\n"); sb.append("- For system headers and libraries, delete the files so they won't\n" + " be explicitly imported onto the package diagram.\n"); sb.append("- For temporary files which are shared between two actions,\n" + " merge the actions together.\n"); sb.append("- First, move the files into a different package (with more\n" + " appropriate roots), from where they can be referenced.\n"); sb.append("- Modify this package's roots so they encompass these files.\n"); break; case FILE_IS_MODIFIED: sb.append("The following files are written-to by multiple actions:\n\n"); displayPaths(sb, causeIDs); break; default: sb.append("An unexpected error has occurred while trying to move to a new package."); } /* * Display the message. */ AlertDialog.displayErrorDialog("Unable To Move To Package", sb.toString()); } /*-------------------------------------------------------------------------------------*/ /** * Display a list of paths, by appending them to a String buffer. * @param sb The StringBuffer to append path names to. * @param pathIds The array of path IDs. */ private void displayPaths(StringBuffer sb, Integer[] pathIds) { IFileMgr fileMgr = buildStore.getFileMgr(); for (int i = 0; i < pathIds.length; i++) { String path = fileMgr.getPathName(pathIds[i]); if (path != null) { sb.append(path); sb.append('\n'); } } } /*-------------------------------------------------------------------------------------*/ }