/*******************************************************************************
* 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.outline;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationListener;
import org.eclipse.jface.viewers.ColumnViewerEditorDeactivationEvent;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TextCellEditor;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.operations.RedoActionHandler;
import org.eclipse.ui.operations.UndoActionHandler;
import org.eclipse.ui.views.contentoutline.ContentOutlinePage;
import com.buildml.eclipse.MainEditor;
import com.buildml.eclipse.bobj.UIInteger;
import com.buildml.eclipse.bobj.UIPackage;
import com.buildml.eclipse.bobj.UIPackageFolder;
import com.buildml.eclipse.outline.dialogs.ChangeRootsDialog;
import com.buildml.eclipse.utils.AlertDialog;
import com.buildml.eclipse.utils.UndoOpAdapter;
import com.buildml.model.FatalBuildStoreError;
import com.buildml.model.IBuildStore;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageMgrListener;
import com.buildml.model.IPackageRootMgr;
import com.buildml.model.undo.PackageUndoOp;
import com.buildml.utils.errors.ErrorCode;
/**
* An Eclipse view, providing content for the "Outline View" associated with the BuildML
* editor.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class OutlinePage extends ContentOutlinePage implements IPackageMgrListener {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** The SWT TreeViewer that is displayed within this outline view page */
private TreeViewer treeViewer;
/** The main BuildML editor that we're showing the outline of */
private MainEditor mainEditor;
/** The IBuildStore associated with this content view */
private IBuildStore buildStore;
/** The IPackageMgr that we'll be displaying information from */
private IPackageMgr pkgMgr;
/** The IPackageRootMgr that we'll be displaying information from */
private IPackageRootMgr pkgRootMgr;
/** The tree element that's currently selected */
private UIInteger selectedNode = null;
/** Based on the current tree selection, can the selected node be removed? */
private boolean removeEnabled = false;
/** Based on the current tree selection, can the selected node be renamed? */
private boolean renameEnabled = false;
/** Based on the current tree selection, does the selected package have roots? */
private boolean changeRootsEnabled = false;
/** Based on the current tree selection, can the package be opened? */
private boolean openEnabled = false;
/** The undo handler from our main BuildML editor */
private UndoActionHandler undoAction;
/** The redo handler from our main BuildML editor */
private RedoActionHandler redoAction;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new OutlinePage object. There should be exactly one of these objects for
* each BuildML MainEditor object.
*
* @param mainEditor The associate MainEditor object.
* @param redoAction The MainEditor's redo action (for redoing operations).
* @param undoAction The MainEditor's undo action (for undoing operations).
*
*/
public OutlinePage(MainEditor mainEditor, UndoActionHandler undoAction,
RedoActionHandler redoAction) {
super();
/*
* Save these handlers for later. We'll apply them to our action bar in
* the createControl method.
*/
this.undoAction = undoAction;
this.redoAction = redoAction;
/* our outline view will display information from this IPackageMgr object. */
this.mainEditor = mainEditor;
buildStore = mainEditor.getBuildStore();
pkgMgr = buildStore.getPackageMgr();
pkgRootMgr = buildStore.getPackageRootMgr();
/* add ourselves as a listener for package changes */
pkgMgr.addListener(this);
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/* (non-Javadoc)
* @see org.eclipse.ui.views.contentoutline.ContentOutlinePage#createControl(org.eclipse.swt.widgets.Composite)
*/
@Override
public void createControl(Composite parent) {
super.createControl(parent);
/*
* Configure the view's (pre-existing) TreeViewer with necessary helper objects that
* will display the BuildML editor's package structure.
*/
treeViewer = getTreeViewer();
treeViewer.setContentProvider(new OutlineContentProvider(pkgMgr, true));
treeViewer.setLabelProvider(new OutlineLabelProvider(pkgMgr));
treeViewer.addSelectionChangedListener(this);
treeViewer.setInput(new UIPackageFolder[] { new UIPackageFolder(pkgMgr.getRootFolder()) });
treeViewer.expandToLevel(2);
/*
* Create the context menu. It'll be populated by the rules in plugin.xml.
*/
MenuManager menuMgr = new MenuManager();
menuMgr.setRemoveAllWhenShown(true);
menuMgr.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(IMenuManager manager) {
manager.add(new Separator("buildmladditions"));
}
});
Menu menu = menuMgr.createContextMenu(treeViewer.getControl());
treeViewer.getControl().setMenu(menu);
getSite().registerContextMenu("org.eclipse.ui.views.ContentOutline", menuMgr, treeViewer);
getSite().setSelectionProvider(treeViewer);
/*
* When the user double-clicks on a folder name, automatically expand the content
* of that folder. If they double-click on a package name, open that package
* as a new Diagram in the main editor.
*/
treeViewer.addDoubleClickListener(new IDoubleClickListener() {
@Override
public void doubleClick(DoubleClickEvent event) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
Object node = selection.getFirstElement();
if (treeViewer.isExpandable(node)){
treeViewer.setExpandedState(node,
!treeViewer.getExpandedState(node));
}
/* else, open the package diagram editor */
else {
if (node instanceof UIPackage) {
int selectedPkgId = ((UIPackage)node).getId();
if (selectedPkgId != pkgMgr.getImportPackage()) {
mainEditor.openPackageDiagram(selectedPkgId);
}
}
}
}
});
/*
* Configure the ability to edit cells in the package/folder tree. The
* OutlineContentCellModifier class does most of the hard work.
*/
treeViewer.setColumnProperties(new String[] { "NAME" });
treeViewer.setCellModifier(new OutlineContentCellModifier(mainEditor));
treeViewer.setCellEditors(new CellEditor [] { new TextCellEditor(treeViewer.getTree()) });
/*
* Arrange it so that cell editing is only possible when we call editElement(), rather
* than when the user clicks on the label.
*/
treeViewer.getColumnViewerEditor().addEditorActivationListener(
new ColumnViewerEditorActivationListener() {
public void beforeEditorActivated(ColumnViewerEditorActivationEvent event) {
if (event.eventType != ColumnViewerEditorActivationEvent.PROGRAMMATIC) {
event.cancel = true;
}
}
public void beforeEditorDeactivated(ColumnViewerEditorDeactivationEvent event) {}
public void afterEditorDeactivated(ColumnViewerEditorDeactivationEvent event) {}
public void afterEditorActivated(ColumnViewerEditorActivationEvent event) {}
});
/*
* Listen to our own selection events, which is necessary to learn which element is
* selected when we want to add or delete elements.
*/
addSelectionChangedListener(this);
/*
* Add the DragSource and DropTarget so that we can copy/move packages around.
*/
new OutlineDragSource(treeViewer, this);
new OutlineDropTarget(treeViewer, this);
/*
* Add the undo/redo actions from the main editor to our action bar. This allows
* the user to use Ctrl-Z etc. to undo/redo while focused on our window.
*/
getSite().getActionBars().setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction);
getSite().getActionBars().setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);
}
/*-------------------------------------------------------------------------------------*/
/**
* Add a new package or folder to the BuildML build system. This is a UI method which will
* update the view and report necessary error messages. The new package or folder will be
* added at the same level in the tree as the currently selected element (or under
* the root, if there's no selection).
*
* @param createFolder If true, create a folder, else create a package.
*/
public void newPackageOrFolder(boolean createFolder) {
/* figure out where in the tree we'll add the new node */
int parentId = getParentForNewNode();
String newName = getNameForNewNode();
/* add the new package/folder; it'll be positioned under the top root (for now) */
int id;
if (createFolder) {
id = pkgMgr.addFolder(newName);
} else {
id = pkgMgr.addPackage(newName);
}
/* these errors should never occur */
if ((id == ErrorCode.INVALID_NAME) || (id == ErrorCode.ALREADY_USED)) {
throw new FatalBuildStoreError("Unable to create new package/folder: " + newName);
}
/*
* Move the new node underneath its destined parent. Error cases have already
* been handled, so if we see an error, that's a coding problem.
*/
if (pkgMgr.setParent(id, parentId) != ErrorCode.OK) {
throw new FatalBuildStoreError("Couldn't move new tree element under parent");
}
/*
* Refresh the tree so that the new folder appears. We also need to make sure that
* the parent node is expanded, since it might not be right now.
*/
UIPackageFolder parentNode = new UIPackageFolder(parentId);
treeViewer.setExpandedState(parentNode, true);
treeViewer.refresh();
/* now mark the new node for editing, to encourage the user to change the name */
UIInteger newNode;
PackageUndoOp op = new PackageUndoOp(buildStore, id);
if (createFolder) {
newNode = new UIPackageFolder(id);
op.recordNewFolder(newName, parentId);
} else {
newNode = new UIPackage(id);
op.recordNewPackage(newName, parentId);
}
treeViewer.editElement(newNode, 0);
/* record the undo/redo operation */
new UndoOpAdapter(createFolder ? "Create Package Folder" : "Create Package", op).record();
}
/*-------------------------------------------------------------------------------------*/
/**
* Remove the currently-selected package or package folder from the BuildML build system.
* This is a UI method which will update the view and report necessary error messages.
* Packages can only be removed if they don't contain any files/actions. Package folders
* can only be removed if they don't contain any sub-packages (or package folders).
*/
public void remove() {
/* if nothing is selected, we can't delete anything */
if (selectedNode == null) {
return;
}
/* determine the name and type of the thing we're deleting */
int id = selectedNode.getId();
String name = pkgMgr.getName(id);
boolean isFolder = pkgMgr.isFolder(id);
int status = AlertDialog.displayOKCancelDialog("Are you sure you want to delete the " +
"\"" + name + "\" " + (isFolder ? "folder?" : "package?"));
if (status == IDialogConstants.CANCEL_ID) {
return;
}
/* record the item's parent, in case we need to undo later */
int parentId = pkgMgr.getParent(id);
/* go ahead and remove it, possibly with an error code being returned */
int rc = pkgMgr.remove(id);
/* An error occured while removing... */
if (rc != ErrorCode.OK) {
/* for some reason, the element couldn't be deleted */
if (rc == ErrorCode.CANT_REMOVE) {
/* give an appropriate error message */
if (selectedNode instanceof UIPackage) {
AlertDialog.displayErrorDialog("Can't Delete Package",
"The selected package couldn't be deleted because it still " +
"contains files and actions.");
} else {
AlertDialog.displayErrorDialog("Can't Delete Package Folder",
"The selected package folder couldn't be deleted because it still " +
"contains sub-packages");
}
} else {
throw new FatalBuildStoreError("Unexpected error when attempting to delete package " +
"or package folder");
}
}
/* Success - element removed. Update view accordingly. */
else {
PackageUndoOp op = new PackageUndoOp(buildStore, id);
if (isFolder) {
op.recordRemoveFolder(name, parentId);
} else {
op.recordRemovePackage(name, parentId);
}
new UndoOpAdapter(isFolder ? "Remove Folder" : "Remove Package", op).record();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Rename the currently-selected element in the content outline view.
*/
public void rename() {
/*
* Initiate the editing of the selected cell. Note that most of the work for this
* operation is performed by the OutlineContentCellModifier class. All we need to
* do here is start the edit in motion.
*/
if (selectedNode != null) {
treeViewer.editElement(selectedNode, 0);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Change the source/generated package roots for the selected package.
*/
public void changeRoots() {
int pkgId = selectedNode.getId();
boolean success = true;
int srcRootPathId, genRootPathId;
/* record the old root value, in case we need to undo */
int oldSrcRootPathId = pkgRootMgr.getPackageRoot(pkgId, IPackageRootMgr.SOURCE_ROOT);
int oldGenRootPathId = pkgRootMgr.getPackageRoot(pkgId, IPackageRootMgr.GENERATED_ROOT);
/*
* Show the dialog, repeating if one or more bad paths were provided. Note that we
* assume the dialog only gives us 'existing' paths, but we still need to check that the
* new roots are in range.
*/
do {
ChangeRootsDialog dialog = new ChangeRootsDialog(buildStore, pkgId);
if (dialog.open() == ChangeRootsDialog.OK) {
srcRootPathId = dialog.getSourceRootPathId();
genRootPathId = dialog.getGeneratedRootPathId();
String errMsg = "The root could not be moved to that location. It must not be above " +
"the @workspace root, and must encompass all the package's existing files.";
int srcRc = pkgRootMgr.setPackageRoot(pkgId, IPackageRootMgr.SOURCE_ROOT, srcRootPathId);
if (srcRc == ErrorCode.OUT_OF_RANGE) {
AlertDialog.displayErrorDialog("Failed to Change Source Root", errMsg);
success = false;
}
int genRc = pkgRootMgr.setPackageRoot(pkgId, IPackageRootMgr.GENERATED_ROOT, genRootPathId);
if (genRc == ErrorCode.OUT_OF_RANGE) {
AlertDialog.displayErrorDialog("Failed to Change Generated Root", errMsg);
success = false;
}
} else {
/* operation cancelled */
return;
}
} while (!success);
/* if anything changed, create a history item so we can undo/redo */
if ((oldSrcRootPathId != srcRootPathId) || (oldGenRootPathId != genRootPathId)) {
PackageUndoOp op = new PackageUndoOp(buildStore, pkgId);
op.recordRootChange(oldSrcRootPathId, oldGenRootPathId, srcRootPathId, genRootPathId);
new UndoOpAdapter("Change Package Root", op).invoke();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Invoke the "open package" command on the currently selected node, opening the package's
* diagram in the main editor.
*/
public void openPackage() {
if (selectedNode instanceof UIPackage) {
mainEditor.openPackageDiagram(selectedNode.getId());
}
}
/*-------------------------------------------------------------------------------------*/
/**
* This method is called whenever the user clicks on a node in the tree viewer. We
* make note of the currently-selected node so that other operations (add, delete, etc)
* know what they're operating on. Based on the selection, we also determine whether
* the "remove" or "rename" operations are currently permitted.
*/
public void selectionChanged(SelectionChangedEvent event) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
Object node = selection.getFirstElement();
if (node instanceof UIInteger) {
selectedNode = (UIInteger)node;
int nodeId = selectedNode.getId();
/* start by assuming the all buttons will be active */
removeEnabled = renameEnabled = changeRootsEnabled = openEnabled = true;
/*
* Based on the selection, determine whether the buttons
* should be disabled. We can't remove/rename the root folder, and we can't
* remove a folder that has children.
*/
if (selectedNode instanceof UIPackageFolder) {
if (nodeId == pkgMgr.getRootFolder()) {
removeEnabled = renameEnabled = false;
} else if (pkgMgr.getFolderChildren(nodeId).length != 0) {
removeEnabled = false;
}
changeRootsEnabled = openEnabled = false;
}
/* else, for the UIPackage, the <import> package can't be touched. */
else {
if (nodeId == pkgMgr.getImportPackage()) {
removeEnabled = renameEnabled = changeRootsEnabled = openEnabled = false;
}
else if (nodeId == pkgMgr.getMainPackage()) {
removeEnabled = renameEnabled = false;
}
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* @return true if the "remove" command should be enabled, based on the current tree
* selection.
*/
public boolean getRemoveEnabled() {
return removeEnabled;
}
/*-------------------------------------------------------------------------------------*/
/**
* @return true if the "rename" command should be enabled, based on the current tree
* selection.
*/
public boolean getRenameEnabled() {
return renameEnabled;
}
/*-------------------------------------------------------------------------------------*/
/**
* @return true if the "change roots" command should be enabled, based on the current tree
* selection.
*/
public boolean getChangeRootsEnabled() {
return changeRootsEnabled;
}
/*-------------------------------------------------------------------------------------*/
/**
* @return true if the "open" menu command should be enabled, based on the current tree
* selection.
*/
public boolean getOpenEnabled() {
return openEnabled;
}
/*-------------------------------------------------------------------------------------*/
/**
* @return The main BuildML editor associated with this outline view.
*/
public MainEditor getMainEditor() {
return mainEditor;
}
/*-------------------------------------------------------------------------------------*/
/**
* Refresh this page's view, due to an external change in the underlying model.
*/
public void refresh() {
treeViewer.refresh();
}
/*=====================================================================================*
* PROTECTED METHODS
*=====================================================================================*/
/**
* Inform our parent class to create a single-selection tree.
*/
protected int getTreeStyle() {
return super.getTreeStyle() | SWT.SINGLE;
}
/*-------------------------------------------------------------------------------------*/
/*
* When the underlying IPackageMgr is changed in some way, we must refresh our outline
* view so it reflects the latest changes.
*/
@Override
public void packageChangeNotification(int pkgId, int how) {
refresh();
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Given that the currently-selected node is used as an indication of where new packages
* (or folders) should be inserted, compute the parent of the node we're about to add.
* If the current selection is a folder, use that. If it's a package, use the package's
* parent. If there's no selection, use the root node.
*
* @return The ID of the parent folder, into which a new package/folder will be added.
*/
private int getParentForNewNode() {
/* no selection => return top root */
if (selectedNode == null) {
return pkgMgr.getRootFolder();
}
/* current selection is a folder => return current selection */
int selectedId = selectedNode.getId();
if (pkgMgr.isFolder(selectedId)) {
return selectedId;
}
/* else, return parent of current selection (or root if there's an error) */
int parentId = pkgMgr.getParent(selectedId);
if (parentId == ErrorCode.NOT_FOUND) {
return pkgMgr.getRootFolder();
}
return parentId;
}
/*-------------------------------------------------------------------------------------*/
/**
* Compute a unique name for a newly added package or folder. The default name will
* be "Untitled", but if that name already exists, return "Untitled-N" where N is the
* lowest integer (starting at 1) that isn't in use.
*
* @return A unique name for a new package or folder.
*/
private String getNameForNewNode() {
String chosenName;
int attemptNum = 0;
/*
* Repeat until we find an available name. The assumption is that we'll find an
* available name before we run out of integers.
*/
while (true) {
chosenName = "Untitled";
if (attemptNum != 0) {
chosenName += "-" + attemptNum;
}
if (pkgMgr.getId(chosenName) == ErrorCode.NOT_FOUND) {
return chosenName;
}
attemptNum++;
}
}
/*-------------------------------------------------------------------------------------*/
}