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