/******************************************************************************* * 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; } }); } /*-------------------------------------------------------------------------------------*/ }