/*******************************************************************************
* Copyright (c) 2011 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:
* "Peter Smith <psmith@arapiki.com>" - initial API and
* implementation and/or initial documentation
*******************************************************************************/
package com.buildml.eclipse.actions;
import java.lang.reflect.InvocationTargetException;
import org.eclipse.core.runtime.IProgressMonitor;
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.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.progress.IProgressService;
import com.buildml.eclipse.EditorOptions;
import com.buildml.eclipse.ImportSubEditor;
import com.buildml.eclipse.bobj.UIAction;
import com.buildml.eclipse.utils.AlertDialog;
import com.buildml.eclipse.utils.EclipsePartUtils;
import com.buildml.eclipse.utils.VisibilityTreeViewer;
import com.buildml.model.IActionMgr;
import com.buildml.model.IActionMgrListener;
import com.buildml.model.IActionTypeMgr;
import com.buildml.model.IBuildStore;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.IPackageMemberMgrListener;
import com.buildml.model.ISlotTypes.SlotDetails;
import com.buildml.model.impl.ActionTypeMgr;
import com.buildml.model.types.PackageSet;
import com.buildml.model.types.ActionSet;
import com.buildml.utils.types.IntegerTreeSet;
/**
* Implements an sub-editor for browsing a BuildStore's actions.
*
* @author "Peter Smith <psmith@arapiki.com>"
*/
public class ActionsEditor extends ImportSubEditor
implements IPackageMemberMgrListener, IActionMgrListener {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** This editor's main control is a TreeViewer, for displaying the list of files */
VisibilityTreeViewer actionsTreeViewer = null;
/** The column that displays the action tree */
private TreeViewerColumn treeColumn;
/** The column that displays the package name */
private TreeViewerColumn pkgColumn;
/** The Action manager object that contains all the file information for this BuildStore */
private IActionMgr actionMgr = null;
/** The ArrayContentProvider object providing this editor's content */
private ActionsEditorContentProvider contentProvider;
/**
* The set of actions (within the ActionMgr) that are currently visible.
*/
private ActionSet visibleActions = null;
/**
* The object that provides visible/non-visible information about each
* element in the action tree.
*/
private ActionsEditorVisibilityProvider visibilityProvider;
/**
* The previous set of option bits. The refreshView() method uses this value to
* determine which aspects of the TreeViewer must be redrawn.
*/
private int previousEditorOptionBits = 0;
/**
* The TreeViewer's parent control.
*/
private Composite filesEditorComposite;
/**
* The current tree item that has its text expanded. When a new tree item
* is selected, this item's text will be contracted again.
*/
UIAction previousSelection = null;
/** True if we currently have a TreeViewer refresh scheduled to occur, else false */
private boolean currentlyRefreshing = false;
/*=====================================================================================*
* CONSTRUCTOR
*=====================================================================================*/
/**
* Create a new ActionsEditor instance, using the specified BuildStore as input
* @param buildStore The BuildStore to display/edit.
* @param tabTitle The text to appear on the editor's tab.
*/
public ActionsEditor(IBuildStore buildStore, String tabTitle) {
super(buildStore, tabTitle);
actionMgr = buildStore.getActionMgr();
actionMgr.addListener(this);
/* listen to changes in package content (for all packages) */
IPackageMemberMgr pkgMemberMgr = buildStore.getPackageMemberMgr();
pkgMemberMgr.addListener(this);
/* initially, all paths are visible */
visibleActions = buildStore.getReportMgr().reportAllActions();
}
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.EditorPart#init(org.eclipse.ui.IEditorSite, org.eclipse.ui.IEditorInput)
*/
@Override
public void init(IEditorSite site, IEditorInput input)
throws PartInitException {
/* we can only handle files as input */
if (! (input instanceof IURIEditorInput)) {
throw new PartInitException("Invalid Input: Must be IURIEditorInput");
}
/* save our site and input data */
setSite(site);
setInput(input);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
*/
@Override
public void createPartControl(final Composite parent) {
/* initiate functionality that's common to all editors */
super.createPartControl(parent);
/* enable the "actionseditor" context, used for keyboard acceleration */
IContextService contextService =
(IContextService) getSite().getService(IContextService.class);
contextService.activateContext("com.buildml.eclipse.contexts.actionseditor");
/* create the main Tree control that the user will view/manipulate */
Tree actionEditorTree = new Tree(parent, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL |
SWT.MULTI | SWT.FULL_SELECTION);
actionEditorTree.setHeaderVisible(true);
actionEditorTree.setLinesVisible(true);
/*
* The main control in this editor is a TreeViewer that allows the user to
* browse the structure of the BuildStore's actions. It has two columns:
* 1) The file system path (shown as a tree).
* 2) The path's package (shown as a fixed-width column);
*/
actionsTreeViewer = new VisibilityTreeViewer(actionEditorTree);
treeColumn = new TreeViewerColumn(actionsTreeViewer, SWT.LEFT);
treeColumn.getColumn().setAlignment(SWT.LEFT);
treeColumn.getColumn().setText("Action Command");
pkgColumn = new TreeViewerColumn(actionsTreeViewer, SWT.RIGHT);
pkgColumn.getColumn().setAlignment(SWT.LEFT);
pkgColumn.getColumn().setText("Package");
filesEditorComposite = parent;
/*
* Set the initial column widths so that the path column covers the full editor
* window, and the package column is empty. Setting the path column
* to a non-zero pixel width causes it to be expanded to the editor's full width.
*/
treeColumn.getColumn().setWidth(1);
pkgColumn.getColumn().setWidth(0);
/*
* Add the tree/table content and label providers.
*/
contentProvider = new ActionsEditorContentProvider(this, actionMgr);
ActionsEditorLabelCol1Provider labelProviderCol1 =
new ActionsEditorLabelCol1Provider(this, actionMgr);
ActionsEditorLabelCol2Provider labelProviderCol2 =
new ActionsEditorLabelCol2Provider(this, buildStore);
actionsTreeViewer.setContentProvider(contentProvider);
treeColumn.setLabelProvider(labelProviderCol1);
pkgColumn.setLabelProvider(labelProviderCol2);
ColumnViewerToolTipSupport.enableFor(actionsTreeViewer);
/*
* Set up a visibility provider so we know which paths should be visible (at
* least to start with).
*/
visibilityProvider = new ActionsEditorVisibilityProvider(visibleActions);
visibilityProvider.setSecondaryFilterSet(null);
actionsTreeViewer.setVisibilityProvider(visibilityProvider);
/*
* Record the initial set of option bits so that we can later determine
* which bits have been modified (this is used in refreshView()).
*/
previousEditorOptionBits = getOptions();
/* double-clicking on an expandable node will expand/contract that node */
actionsTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
public void doubleClick(DoubleClickEvent event) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
UIAction node = (UIAction)selection.getFirstElement();
if (actionsTreeViewer.isExpandable(node)){
actionsTreeViewer.setExpandedState(node,
!actionsTreeViewer.getExpandedState(node));
}
}
});
/* create the context menu */
MenuManager menuMgr = new MenuManager("#PopupMenu");
menuMgr.setRemoveAllWhenShown(true);
menuMgr.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(IMenuManager manager) {
manager.add(new Separator("buildmlactions"));
manager.add(new Separator("additions"));
}
});
Menu menu = menuMgr.createContextMenu(actionsTreeViewer.getControl());
actionsTreeViewer.getControl().setMenu(menu);
getSite().registerContextMenu(menuMgr, actionsTreeViewer);
getSite().setSelectionProvider(actionsTreeViewer);
/*
* When the tree viewer needs to compare its elements, this class
* (ActionsEditor) provides the equals() and hashcode() methods.
*/
actionsTreeViewer.setComparer(this);
/* start by displaying from the root (which changes, depending on our options). */
actionsTreeViewer.setInput(contentProvider.getRootElements());
/* based on the size of the set to be displayed, auto-size the tree output */
int outputSize = getVisibilityFilterSet().size();
if (outputSize < AUTO_EXPAND_THRESHOLD) {
actionsTreeViewer.expandAll();
} else {
actionsTreeViewer.expandToLevel(2);
}
/*
* Now that we've created all the widgets, force options to take effect. Note
* that these setters have side effects that wouldn't have taken effect if
* there were no widgets.
*/
setOptions(getOptions());
setFilterPackageSet(getFilterPackageSet());
setVisibilityFilterSet(getVisibilityFilterSet());
/*
* Add a "drag source" handler so that we can copy/move actions around.
*/
new ActionsEditorDragSource(actionsTreeViewer, buildStore);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.WorkbenchPart#setFocus()
*/
@Override
public void setFocus() {
/* if we focus on this editor, we actually focus on the TreeViewer control */
if (actionsTreeViewer != null){
actionsTreeViewer.getControl().setFocus();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Our parent (multi-part editor) has just switched to our tab. We should update the UI
* state in response.
*/
public void pageChange()
{
ICommandService service =
(ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
/*
* Make sure that each of these toolbar buttons (and menu items) are updated
* appropriately to match the settings of *this* editor, instead of the previous
* editor.
*/
service.refreshElements("com.buildml.eclipse.commands.showPackages", null);
service.refreshElements("com.buildml.eclipse.commands.showHiddenPaths", null);
service.refreshElements("com.buildml.eclipse.commands.showPathRoots", null);
}
/*-------------------------------------------------------------------------------------*/
/**
* Set the visibility state of the specified action. A visible action is rendered as usual,
* but a non-visible action will either be greyed out, or not rendered at all (depending
* on the current setting of the grey-visibility mode). Making a previously visible action
* invisible will also make all child actions invisible. Making a previously invisible
* action visible will ensure that all parent actions are also made visible.
*
* @param item The action to be hidden or revealed.
* @param state True if the action should be made visible, else false.
*/
public void setItemVisibilityState(Object item, boolean state) {
actionsTreeViewer.setVisibility(item, state);
}
/*-------------------------------------------------------------------------------------*/
/**
* Set the complete set of actions that this editor's tree viewer will show. After
* calling this method, it will be necessary to also call refreshView() to actually
* update the view.
* @param visibleActions The subset of actions that should be visible in the editor.
*/
@Override
public void setVisibilityFilterSet(IntegerTreeSet visibleActions) {
this.visibleActions = (ActionSet) visibleActions;
if (visibilityProvider != null) {
visibilityProvider.setPrimaryFilterSet(this.visibleActions);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* @return The set of actions that are currently visible in this editor's tree viewer.
*/
@Override
public IntegerTreeSet getVisibilityFilterSet() {
return visibilityProvider.getPrimaryFilterSet();
}
/*-------------------------------------------------------------------------------------*/
/**
* Given an item in the editor, expand all the descendants of that item so
* that they're visible in the tree viewer.
* @param node The tree node representing the item in the tree to be expanded.
*/
public void expandSubtree(Object node) {
actionsTreeViewer.expandToLevel(node, AbstractTreeViewer.ALL_LEVELS);
}
/*-------------------------------------------------------------------------------------*/
/**
* Set this editor's package filter set. This set is used by the viewer when
* deciding which files should be displayed (versus being filtered out).
* @param newSet This editor's new package filter set.
*/
public void setFilterPackageSet(PackageSet newSet) {
super.setFilterPackageSet(newSet);
/* if the editor is in an initialized state, we can refresh the filters */
if (visibilityProvider != null) {
ActionSet pkgActionSet =
buildStore.getReportMgr().reportActionsFromPackageSet(newSet);
pkgActionSet.populateWithParents();
visibilityProvider.setSecondaryFilterSet(pkgActionSet);
refreshView(true);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Refresh the editor's content. This is typically called when some type of display
* option changes (e.g. packages have been added), and the content is now
* different, or if the user resizes the main Eclipse shell. We use a progress monitor,
* since a redraw operation might take a while.
* @param forceRedraw true if we want to force a complete redraw of the viewer.
*/
public void refreshView(boolean forceRedraw) {
/* compute the set of option bits that have changed since we were last called */
int currentOptions = getOptions();
int changedOptions = previousEditorOptionBits ^ currentOptions;
previousEditorOptionBits = currentOptions;
/*
* Determine whether the packages columns should be shown. Setting the
* width appropriately is important, especially if the shell was recently resized.
* TODO: figure out why subtracting 20 pixels is important for matching the column
* size with the size of the parent composite.
*/
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
int editorWidth = filesEditorComposite.getClientArea().width - 20;
int pkgWidth = isOptionSet(EditorOptions.OPT_SHOW_PACKAGES) ? 100 : 0;
treeColumn.getColumn().setWidth(editorWidth - 2 * pkgWidth);
pkgColumn.getColumn().setWidth(pkgWidth);
}
});
/*
* Has the content of the tree changed, or just the visibility of columns? If
* it's just the columns, then we don't need to re-query the model in order to redisplay.
* Unless our caller explicitly requested a redraw.
*/
if (!forceRedraw) {
return;
}
/*
* We need to re-query the model and redisplay some (or all) of the tree items.
* Create a new job that will be run in the background, and monitored by the
* progress monitor. Note that only the portions of this job that update the
* UI should be run as the UI thread. Otherwise the job appears to block the
* whole UI.
*/
IRunnableWithProgress redrawJob = new IRunnableWithProgress() {
@Override
public void run(IProgressMonitor monitor) {
monitor.beginTask("Redrawing editor content...", 2);
monitor.worked(1);
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
Object[] expandedElements = actionsTreeViewer.getExpandedElements();
actionsTreeViewer.setInput(contentProvider.getRootElements());
actionsTreeViewer.refresh();
/*
* Ensure that all previously-expanded items are now expanded again.
* Note: we can't use setExpandedElements(), as that won't always
* open all the parent elements as well.
*/
for (int i = 0; i < expandedElements.length; i++) {
actionsTreeViewer.expandToLevel(expandedElements[i], 1);
}
}
});
monitor.worked(1);
monitor.done();
}
};
/* start up the progress monitor service so that it monitors the job */
IProgressService service = PlatformUI.getWorkbench().getProgressService();
try {
service.busyCursorWhile(redrawJob);
} catch (InvocationTargetException e) {
// TODO: what to do here?
} catch (InterruptedException e) {
// TODO: what to do here?
}
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.eclipse.SubEditor#hasFeature(java.lang.String)
*/
@Override
public boolean hasFeature(String feature) {
if (feature.equals("removable")) {
return isRemovable();
} else if (feature.equals("actions") || feature.equals("package-names") ||
feature.equals("search-by-name")) {
return true;
}
return false;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.eclipse.SubEditor#getEditorImagePath()
*/
@Override
public String getEditorImagePath() {
return "images/action_icon.gif";
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.eclipse.SubEditor#doCopyCommand(org.eclipse.core.commands.ExecutionEvent)
*/
public void doCopyCommand(Clipboard clipboard, ISelection selection) {
/* Determine the set of actions that are currently selected */
if (selection instanceof TreeSelection) {
ActionSet actionSet = EclipsePartUtils.
getActionSetFromSelection(buildStore, (TreeSelection)selection);
/*
* All commands will be concatenated together into a single string (with an appropriate
* EOL character).
*/
String eol = System.getProperty("line.separator");
StringBuffer sb = new StringBuffer();
for (int actionId : actionSet) {
String cmd = (String) actionMgr.getSlotValue(actionId, IActionMgr.COMMAND_SLOT_ID);
sb.append(cmd);
sb.append(eol);
}
/*
* Copy the string to the clipboard(s). There's a separate clipboard for
* Eclipse versus Linux, so copy to both of them.
*/
try {
clipboard.setContents(
new Object[] { sb.toString(), },
new Transfer[] { TextTransfer.getInstance(), },
DND.CLIPBOARD | DND.SELECTION_CLIPBOARD);
} catch (SWTError error) {
AlertDialog.displayErrorDialog("Unable to copy",
"The selected information could not be copied to the clipboard.");
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Called by pkgMgr when package membership changes. If we're currently showing packages,
* we'll need to refresh (after all, we're showing all actions and their packages, so
* there's a good chance our content changed).
*/
@Override
public void packageMemberChangeNotification(int pkgId, int how, int memberType, int memberId) {
if (how == IPackageMemberMgrListener.CHANGED_MEMBERSHIP) {
scheduleRefresh();
}
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IActionMgrListener#actionChangeNotification(int, int, int)
*/
@Override
public void actionChangeNotification(int actionId, int how, int changeId) {
if ((how == IActionMgrListener.TRASHED_ACTION) ||
(how == IActionMgrListener.CHANGED_SLOT)) {
scheduleRefresh();
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Invoked whenever one of this editor's options are changed. We may need to react
* to the option change in some way.
* @param optionBits The option(s) that were modified.
* @param enable True if the options were added, else false.
*/
protected void updateEditorWithNewOptions(int optionBits, boolean enable) {
/* pass some of the options onto onto parts of the system */
if ((actionsTreeViewer != null) && (optionBits & EditorOptions.OPT_SHOW_HIDDEN) != 0) {
actionsTreeViewer.setGreyVisibilityMode(enable);
}
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Schedule the editor content (TreeViewer) to be refreshed with the new
* content. Note that we deliberately schedule this to happen later,
* since our current thread is quite possibly doing a number of updates
* that will result in multiple notifications. We don't want to refresh
* for each individual update.
*/
private void scheduleRefresh() {
if (currentlyRefreshing) {
return;
}
currentlyRefreshing = true;
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
refreshView(true);
currentlyRefreshing = false;
}
});
}
/*-------------------------------------------------------------------------------------*/
}