/** * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ /* * Author: atotic * Author: fabioz * * Created: Jul 10, 2003 */ package org.python.pydev.outline; import java.util.ArrayList; import java.util.List; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.viewers.AbstractTreeViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Tree; import org.eclipse.ui.IActionBars; import org.eclipse.ui.part.IShowInTarget; import org.eclipse.ui.part.ShowInContext; import org.eclipse.ui.texteditor.IDocumentProvider; import org.python.pydev.core.ExtensionHelper; import org.python.pydev.core.bundle.ImageCache; import org.python.pydev.core.callbacks.CallbackWithListeners; import org.python.pydev.core.callbacks.ICallbackWithListeners; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.log.Log; import org.python.pydev.editor.PyEdit; import org.python.pydev.parser.ErrorDescription; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.plugin.PydevPlugin; import org.python.pydev.ui.IViewCreatedObserver; import org.python.pydev.ui.IViewWithControls; import org.python.pydev.ui.UIConstants; /** * Outline page, displays the structure of the document in the editor window. * * Partition outlining:<p> * PyDocumentProvider already partitions the document into strings/comments/other<p> * RawPartition is the simplest outline that shows this "raw" document partitioning<p> * raw partition was only used as an example, not useful in production<p> * * @note: tests for the outline page are not directly for the outline page, but for its model, * based on ParsedItems. **/ @SuppressWarnings({ "rawtypes", "unchecked" }) public class PyOutlinePage extends ContentOutlinePageWithFilter implements IShowInTarget, IAdaptable, IViewWithControls { PyEdit editorView; IDocument document; IOutlineModel model; ImageCache imageCache; // listeners to rawPartition ISelectionChangedListener selectionListener; private OutlineLinkWithEditorAction linkWithEditor; public final ICallbackWithListeners onControlCreated = new CallbackWithListeners(); public final ICallbackWithListeners onControlDisposed = new CallbackWithListeners(); private List createdCallbacksForControls; public PyOutlinePage(PyEdit editorView) { super(); List<IViewCreatedObserver> participants = ExtensionHelper .getParticipants(ExtensionHelper.PYDEV_VIEW_CREATED_OBSERVER); for (IViewCreatedObserver iViewCreatedObserver : participants) { iViewCreatedObserver.notifyViewCreated(this); } this.editorView = editorView; imageCache = new ImageCache(PydevPlugin.getDefault().getBundle().getEntry("/")); } public void dispose() { onControlDisposed.call(getTreeViewer()); if (createdCallbacksForControls != null) { for (Object o : createdCallbacksForControls) { onControlDisposed.call(o); } createdCallbacksForControls = null; } if (model != null) { model.dispose(); model = null; } if (selectionListener != null) { removeSelectionChangedListener(selectionListener); } if (imageCache != null) { imageCache.dispose(); } if (linkWithEditor != null) { linkWithEditor.dispose(); linkWithEditor = null; } super.dispose(); } /** * Parsed partition creates an outline that shows imports/classes/methods */ private void createParsedOutline() { final TreeViewer tree = getTreeViewer(); IDocumentProvider provider = editorView.getDocumentProvider(); document = provider.getDocument(editorView.getEditorInput()); model = getParsedModel(); tree.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS); tree.setContentProvider(new ParsedContentProvider()); tree.setLabelProvider(new ParsedLabelProvider(imageCache)); tree.setInput(model.getRoot()); } /** * * @return the parsed model, so that it can be used elsewhere (in navigation) */ public ParsedModel getParsedModel() { return new ParsedModel(this, editorView); } public boolean isDisposed() { return getTreeViewer().getTree().isDisposed(); } /** * called when model has structural changes, refreshes all items underneath * @param items: items to refresh, or null for the whole tree * tries to preserve the scrolling */ public void refreshItems(Object[] items) { try { unlinkAll(); TreeViewer viewer = getTreeViewer(); if (viewer != null) { Tree treeWidget = viewer.getTree(); if (isDisposed()) { return; } ScrollBar bar = treeWidget.getVerticalBar(); int barPosition = 0; if (bar != null) { barPosition = bar.getSelection(); } if (items == null) { if (isDisposed()) { return; } viewer.refresh(); } else { if (isDisposed()) { return; } for (int i = 0; i < items.length; i++) { viewer.refresh(items[i]); } } if (barPosition != 0) { bar.setSelection(Math.min(bar.getMaximum(), barPosition)); } } } catch (Throwable e) { //things may be disposed... Log.log(e); } finally { relinkAll(); } } /** * called when a single item changes */ public void updateItems(Object[] items) { try { unlinkAll(); if (isDisposed()) { return; } TreeViewer tree = getTreeViewer(); if (tree != null) { tree.update(items, null); } } finally { relinkAll(); } } /** * @return the preference store we should use */ /*package*/IPreferenceStore getStore() { return PydevPlugin.getDefault().getPreferenceStore(); } @Override public TreeViewer getTreeViewer() { return super.getTreeViewer(); } private void createActions() { linkWithEditor = new OutlineLinkWithEditorAction(this, imageCache); //---- Collapse all Action collapseAll = new Action("Collapse all", IAction.AS_PUSH_BUTTON) { public void run() { getTreeViewer().collapseAll(); } }; //---- Expand all Action expandAll = new Action("Expand all", IAction.AS_PUSH_BUTTON) { public void run() { getTreeViewer().expandAll(); } }; collapseAll.setImageDescriptor(imageCache.getDescriptor(UIConstants.COLLAPSE_ALL)); expandAll.setImageDescriptor(imageCache.getDescriptor(UIConstants.EXPAND_ALL)); // Add actions to the toolbar IActionBars actionBars = getSite().getActionBars(); IToolBarManager toolbarManager = actionBars.getToolBarManager(); toolbarManager.add(new OutlineSortByNameAction(this, imageCache)); toolbarManager.add(collapseAll); toolbarManager.add(expandAll); IMenuManager menuManager = actionBars.getMenuManager(); menuManager.add(linkWithEditor); menuManager.add(new OutlineHideCommentsAction(this, imageCache)); menuManager.add(new OutlineHideImportsAction(this, imageCache)); menuManager.add(new OutlineHideMagicObjectsAction(this, imageCache)); menuManager.add(new OutlineHideFieldsAction(this, imageCache)); menuManager.add(new OutlineHideNonPublicMembersAction(this, imageCache)); menuManager.add(new OutlineHideStaticMethodsAction(this, imageCache)); } /** * create the outline view widgets */ public void createControl(Composite parent) { super.createControl(parent); // this creates a tree viewer try { createParsedOutline(); // selecting an item in the outline scrolls the document selectionListener = new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { if (linkWithEditor == null) { return; } try { unlinkAll(); StructuredSelection sel = (StructuredSelection) event.getSelection(); boolean alreadySelected = false; if (sel.size() == 1) { // only sync the editing view if it is a single-selection ParsedItem firstElement = (ParsedItem) sel.getFirstElement(); ErrorDescription errorDesc = firstElement.getErrorDesc(); //select the error if (errorDesc != null && errorDesc.message != null) { int len = errorDesc.errorEnd - errorDesc.errorStart; editorView.setSelection(errorDesc.errorStart, len); alreadySelected = true; } } if (!alreadySelected) { SimpleNode[] node = model.getSelectionPosition(sel); editorView.revealModelNodes(node); } } finally { relinkAll(); } } }; addSelectionChangedListener(selectionListener); createActions(); //OK, instead of using the default selection engine, we recreate it only to handle mouse //and key events directly, because it seems that sometimes, SWT creates spurious select events //when those shouldn't be created, and there's also a risk of creating loops with the selection, //as when one selection arrives when we're linked, we have to perform a selection and doing that //selection could in turn trigger a new selection, so, we remove that treatment and only start //selections from interactions the user did. //see: Cursor jumps to method definition when an error is detected //https://sourceforge.net/tracker2/?func=detail&aid=2057092&group_id=85796&atid=577329 TreeViewer treeViewer = getTreeViewer(); treeViewer.removeSelectionChangedListener(this); Tree tree = treeViewer.getTree(); tree.addMouseListener(new MouseListener() { public void mouseDoubleClick(MouseEvent e) { tryToMakeSelection(); } public void mouseDown(MouseEvent e) { } public void mouseUp(MouseEvent e) { tryToMakeSelection(); } }); tree.addKeyListener(new KeyListener() { public void keyPressed(KeyEvent e) { } public void keyReleased(KeyEvent e) { if (e.keyCode == SWT.ARROW_UP || e.keyCode == SWT.ARROW_DOWN) { tryToMakeSelection(); } } }); onControlCreated.call(getTreeViewer()); createdCallbacksForControls = callRecursively(onControlCreated, filter, new ArrayList()); } catch (Throwable e) { Log.log(e); } } /** * Calls the callback with the composite c and all of its children (recursively). */ private List callRecursively(ICallbackWithListeners callback, Composite c, ArrayList controls) { try { for (Control child : c.getChildren()) { if (child instanceof Composite) { callRecursively(callback, (Composite) child, controls); } controls.add(child); callback.call(child); } } catch (Throwable e) { Log.log(e); } return controls; } public boolean show(ShowInContext context) { linkWithEditor.doLinkOutlinePosition(this.editorView, this, new PySelection(this.editorView)); return true; } public Object getAdapter(Class adapter) { if (adapter == IShowInTarget.class) { return this; } return null; } @Override public void selectionChanged(SelectionChangedEvent event) { super.selectionChanged(event); } /** * Used to hold a link level to know when it should be unlinked or relinked, as calls can be 'cascaded' */ private int linkLevel = 1; /** * Used for locking link/unlink access. */ private Object lock = new Object(); /** * Stops listening to changes (the linkLevel is used so that multiple unlinks can be called and later * multiple relinks should be used) */ void unlinkAll() { synchronized (lock) { linkLevel--; if (linkLevel == 0) { removeSelectionChangedListener(selectionListener); if (linkWithEditor != null) { linkWithEditor.unlink(); } } } } /** * Starts listening to changes again if the number of relinks matches the number of unlinks */ void relinkAll() { synchronized (lock) { linkLevel++; if (linkLevel == 1) { addSelectionChangedListener(selectionListener); if (linkWithEditor != null) { linkWithEditor.relink(); } } else if (linkLevel > 1) { throw new RuntimeException("Error: relinking without unlinking 1st"); } } } /** * Creates an event of a selection change if it's possible to do so (otherwise returns null) */ private SelectionChangedEvent createSelectionEvent() { SelectionChangedEvent event = null; ISelection selection = getSelection(); if (selection instanceof IStructuredSelection) { IStructuredSelection s = (IStructuredSelection) selection; if (s.iterator().hasNext()) { //only make the selection if there's some item selected event = new SelectionChangedEvent(getTreeViewer(), selection); } } return event; } /** * Tries to trigger a selection changed event (if a selection is available for doing so) */ private void tryToMakeSelection() { SelectionChangedEvent event = createSelectionEvent(); if (event != null) { selectionChanged(event); } } public ICallbackWithListeners getOnControlCreated() { return onControlCreated; } public ICallbackWithListeners getOnControlDisposed() { return onControlDisposed; } }