/*******************************************************************************
* 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.files;
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.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.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.swt.widgets.TreeColumn;
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.UIInteger;
import com.buildml.eclipse.utils.AlertDialog;
import com.buildml.eclipse.utils.EclipsePartUtils;
import com.buildml.eclipse.utils.VisibilityTreeViewer;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileMgr;
import com.buildml.model.IFileMgrListener;
import com.buildml.model.IPackageMemberMgr;
import com.buildml.model.IPackageMemberMgrListener;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageMgrListener;
import com.buildml.model.IPackageRootMgr;
import com.buildml.model.types.FileSet;
import com.buildml.model.types.PackageSet;
import com.buildml.utils.types.IntegerTreeSet;
/**
* A BuildML editor that displays the set of files within a BuildStore.
*
* @author "Peter Smith <psmith@arapiki.com>"
*/
public class FilesEditor extends ImportSubEditor
implements IPackageMgrListener, IFileMgrListener, IPackageMemberMgrListener {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** This editor's main control is a TreeViewer, for displaying the list of files */
private VisibilityTreeViewer filesTreeViewer = null;
/** The column that displays the path tree */
private TreeColumn treeColumn;
/** The column that displays the package name */
private TreeColumn pkgColumn;
/** The column that displays the path's scope */
private TreeColumn scopeColumn;
/** The FileMgr object that contains all the file information for this BuildStore */
private IFileMgr fileMgr = null;
/** The PackageMgr object that contains the package information */
private IPackageMgr pkgMgr = null;
/** The PackageRootMgr object that contains the package root information */
private IPackageRootMgr pkgRootMgr = null;
/** The PackageMemberMgr that tracks file membership */
private IPackageMemberMgr pkgMemberMgr = null;
/** The ArrayContentProvider object providing this editor's content */
private FilesEditorContentProvider contentProvider;
/** The set of paths (within the FileMgr) that are currently visible. */
private FileSet visiblePaths = null;
/**
* The object that provides visible/non-visible information about each
* element in the file tree.
*/
private FilesEditorVisibilityProvider 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;
/** True if we currently have a TreeViewer refresh scheduled to occur, else false */
private boolean currentlyRefreshing = false;
/*=====================================================================================*
* CONSTRUCTORS
*=====================================================================================*/
/**
* Create a new FilesEditor 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 FilesEditor(IBuildStore buildStore, String tabTitle) {
super(buildStore, tabTitle);
fileMgr = buildStore.getFileMgr();
pkgMgr = buildStore.getPackageMgr();
pkgRootMgr = buildStore.getPackageRootMgr();
pkgMemberMgr = buildStore.getPackageMemberMgr();
pkgMgr.addListener(this);
pkgRootMgr.addListener(this);
fileMgr.addListener(this);
pkgMemberMgr.addListener(this);
/* initially, all paths are visible */
visiblePaths = buildStore.getReportMgr().reportAllFiles();
}
/*=====================================================================================*
* 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 "fileseditor" context, used for keyboard acceleration */
IContextService contextService =
(IContextService) getSite().getService(IContextService.class);
contextService.activateContext("com.buildml.eclipse.contexts.fileseditor");
/* create the main Tree control that the user will view/manipulate */
Tree fileEditorTree = new Tree(parent, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL |
SWT.MULTI | SWT.FULL_SELECTION);
fileEditorTree.setHeaderVisible(true);
fileEditorTree.setLinesVisible(true);
/*
* The main control in this editor is a TreeViewer that allows the user to
* browse the structure of the BuildStore's file system. It has three columns:
* 1) The file system path (shown as a tree).
* 2) The path's package (shown as a fixed-width column);
* 3) The path's scope (private, public etc).
*/
filesTreeViewer = new VisibilityTreeViewer(fileEditorTree);
treeColumn = new TreeColumn(fileEditorTree, SWT.LEFT);
treeColumn.setAlignment(SWT.LEFT);
treeColumn.setText("Path");
pkgColumn = new TreeColumn(fileEditorTree, SWT.RIGHT);
pkgColumn.setAlignment(SWT.LEFT);
pkgColumn.setText("Package");
scopeColumn = new TreeColumn(fileEditorTree, SWT.RIGHT);
scopeColumn.setAlignment(SWT.LEFT);
scopeColumn.setText("Scope");
filesEditorComposite = parent;
/*
* Set the initial column widths so that the path column covers the full editor
* window, and the package/scope columns are empty. Setting the path column
* to a non-zero pixel width causes it to be expanded to the editor's full width.
*/
treeColumn.setWidth(1);
pkgColumn.setWidth(0);
scopeColumn.setWidth(0);
/*
* Add the tree/table content and label providers.
*/
contentProvider = new FilesEditorContentProvider(this, fileMgr, pkgRootMgr);
FilesEditorLabelProvider labelProvider =
new FilesEditorLabelProvider(this, buildStore);
FilesEditorViewerSorter viewerSorter = new FilesEditorViewerSorter(this, fileMgr);
filesTreeViewer.setContentProvider(contentProvider);
filesTreeViewer.setLabelProvider(labelProvider);
filesTreeViewer.setSorter(viewerSorter);
/*
* Set up a visibility provider so we know which paths should be visible (at
* least to start with).
*/
visibilityProvider = new FilesEditorVisibilityProvider(visiblePaths);
visibilityProvider.setSecondaryFilterSet(null);
filesTreeViewer.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, whereas
* double-clicking on a file will open it in an editor.
*/
filesTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
@Override
public void doubleClick(DoubleClickEvent event) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
UIInteger node = (UIInteger)selection.getFirstElement();
if (filesTreeViewer.isExpandable(node)){
filesTreeViewer.setExpandedState(node,
!filesTreeViewer.getExpandedState(node));
}
/*
* else, try to open file in an appropriate editor.
* A dialog box will be displayed (by openNewEditor)
* if there's a problem.
*/
else {
String filePath = fileMgr.getPathName(node.getId());
EclipsePartUtils.openNewEditor(filePath);
}
}
});
/* 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(filesTreeViewer.getControl());
filesTreeViewer.getControl().setMenu(menu);
getSite().registerContextMenu(menuMgr, filesTreeViewer);
getSite().setSelectionProvider(filesTreeViewer);
/*
* When the tree viewer needs to compare its elements, this class
* (FilesEditor) provides the equals() and hashcode() methods.
*/
filesTreeViewer.setComparer(this);
/* start by displaying from the root (which changes, depending on our options). */
filesTreeViewer.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) {
filesTreeViewer.expandAll();
} else {
filesTreeViewer.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());
}
/*-------------------------------------------------------------------------------------*/
/* (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 (filesTreeViewer != null){
filesTreeViewer.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 path. A visible path is rendered as per usual,
* but a non-visible path will either be greyed out, or not rendered at all (depending
* on the current setting of the grey-visibility mode). Making a previously visible path
* invisible will also make all child paths invisible. Making a previously invisible
* path visible will ensure that all parent paths are also made visible.
*
* @param item The path to be hidden or revealed.
* @param state True if the path should be made visible, else false.
*/
public void setItemVisibilityState(Object item, boolean state) {
filesTreeViewer.setVisibility(item, state);
}
/*-------------------------------------------------------------------------------------*/
/**
* Set the complete set of paths 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 visiblePaths The subset of paths that should be visible in the editor.
*/
@Override
public void setVisibilityFilterSet(IntegerTreeSet visiblePaths) {
this.visiblePaths = (FileSet) visiblePaths;
if (visibilityProvider != null) {
visibilityProvider.setPrimaryFilterSet(this.visiblePaths);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* @return The set of files that are currently visible in this editor's tree viewer.
*/
@Override
public IntegerTreeSet getVisibilityFilterSet() {
return visibilityProvider.getPrimaryFilterSet();
}
/*-------------------------------------------------------------------------------------*/
/**
* 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.
*/
@Override
public void setFilterPackageSet(PackageSet newSet) {
super.setFilterPackageSet(newSet);
/* if the editor is in an initialized state, we can fresh the filters */
if (visibilityProvider != null) {
FileSet pkgFileSet =
buildStore.getReportMgr().reportFilesFromPackageSet(newSet);
pkgFileSet.populateWithParents();
visibilityProvider.setSecondaryFilterSet(pkgFileSet);
refreshView(true);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Refresh the editor's content. This is typically called when some type of display
* option changes (e.g. roots or 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/scope 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.setWidth(editorWidth - 2 * pkgWidth);
pkgColumn.setWidth(pkgWidth);
scopeColumn.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 && ((changedOptions & (EditorOptions.OPT_COALESCE_DIRS |
EditorOptions.OPT_SHOW_ROOTS) |
EditorOptions.OPT_SHOW_PACKAGES) == 0)) {
return;
}
if ((changedOptions & EditorOptions.OPT_COALESCE_DIRS) != 0) {
filesTreeViewer.setInput(contentProvider.getRootElements());
}
/*
* 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 = filesTreeViewer.getExpandedElements();
filesTreeViewer.setInput(contentProvider.getRootElements());
filesTreeViewer.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++) {
filesTreeViewer.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("paths") || feature.equals("path-roots") ||
feature.equals("filter-packages-by-scope") || feature.equals("package-names") ||
feature.equals("search-by-name")) {
return true;
}
return false;
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.eclipse.SubEditor#getIcon()
*/
@Override
public String getEditorImagePath() {
return "images/files_icon.gif";
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.eclipse.SubEditor#doCopyCommand(org.eclipse.core.commands.ExecutionEvent)
*/
@Override
public void doCopyCommand(Clipboard clipboard, ISelection selection) {
/* Determine the set of files that are currently selected */
if (selection instanceof TreeSelection) {
FileSet fileSet = EclipsePartUtils.
getFileSetFromSelection(buildStore, (TreeSelection)selection);
/*
* All paths 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 pathId : fileSet) {
String path = fileMgr.getPathName(pathId);
sb.append(path);
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 or pkgRootMgr when a package root changes.
*/
@Override
public void packageChangeNotification(int pkgId, int how) {
/* if this FilesEditor is displaying roots, and the roots have changed, refresh the tree */
if (how == IPackageMgrListener.CHANGED_ROOTS) {
if ((getOptions() & EditorOptions.OPT_SHOW_ROOTS) != 0) {
scheduleRefresh();
}
}
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IPackageMemberMgrListener#packageMemberChangeNotification(int, int, int, int)
*/
@Override
public void packageMemberChangeNotification(int pkgId, int how, int memberType, int memberId) {
if (how == IPackageMemberMgrListener.CHANGED_MEMBERSHIP) {
if (isOptionSet(EditorOptions.OPT_SHOW_PACKAGES)) {
scheduleRefresh();
}
}
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.model.IFileMgrListener#pathChangeNotification(int, int)
*/
@Override
public void pathChangeNotification(int pathId, int how) {
scheduleRefresh();
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see org.eclipse.ui.part.WorkbenchPart#dispose()
*/
@Override
public void dispose() {
super.dispose();
/* we no longer want to hear about package changes */
pkgMgr.removeListener(this);
pkgRootMgr.removeListener(this);
fileMgr.removeListener(this);
pkgMemberMgr.removeListener(this);
}
/*=====================================================================================*
* PRIVATE/PROTECTED METHODS
*=====================================================================================*/
/* (non-Javadoc)
* @see com.buildml.eclipse.SubEditor#expandSubtree(java.lang.Object)
*/
@Override
public void expandSubtree(Object node) {
filesTreeViewer.expandToLevel(node, AbstractTreeViewer.ALL_LEVELS);
}
/*-------------------------------------------------------------------------------------*/
/* (non-Javadoc)
* @see com.buildml.eclipse.SubEditor#updateEditorWithNewOptions(int, boolean)
*/
@Override
protected void updateEditorWithNewOptions(int optionBits, boolean enable) {
/* pass some of the options onto onto parts of the system */
if ((filesTreeViewer != null) && (optionBits & EditorOptions.OPT_SHOW_HIDDEN) != 0) {
filesTreeViewer.setGreyVisibilityMode(enable);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* 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;
}
});
}
/*-------------------------------------------------------------------------------------*/
}