/******************************************************************************
* Copyright (C) 2003-2013 Aleksandar Totic and others
*
* 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:
* Aleksandar Totic - initial API and implementation
* Fabio Zadrozny <fabiofz@gmail.com> - ongoing maintenance
* Jonah Graham <jonah@kichwacoders.com> - ongoing maintenance
******************************************************************************/
package org.python.pydev.shared_ui.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.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.shared_core.callbacks.CallbackWithListeners;
import org.python.pydev.shared_core.callbacks.ICallbackWithListeners;
import org.python.pydev.shared_core.log.Log;
import org.python.pydev.shared_core.model.ErrorDescription;
import org.python.pydev.shared_core.model.ISimpleNode;
import org.python.pydev.shared_ui.EditorUtils;
import org.python.pydev.shared_ui.ImageCache;
import org.python.pydev.shared_ui.UIConstants;
import org.python.pydev.shared_ui.editor.BaseEditor;
@SuppressWarnings({ "rawtypes", "unchecked" })
public abstract class BaseOutlinePage extends ContentOutlinePageWithFilter implements IShowInTarget, IAdaptable {
public abstract IPreferenceStore getStore();
protected IDocument document;
//Important: it must be final (i.e.: never change)
protected final IOutlineModel model;
protected final ImageCache imageCache;
// listeners to rawPartition
protected ISelectionChangedListener selectionListener;
protected BaseEditor editorView;
protected OutlineLinkWithEditorAction linkWithEditor;
public final ICallbackWithListeners onControlCreated = new CallbackWithListeners();
public final ICallbackWithListeners onControlDisposed = new CallbackWithListeners();
protected List createdCallbacksForControls;
protected final String pluginId;
public BaseOutlinePage(BaseEditor editorView, ImageCache imageCache, String pluginId) {
this.imageCache = imageCache;
this.editorView = editorView;
this.pluginId = pluginId;
this.model = (IOutlineModel) editorView.getAdapter(IOutlineModel.class);
this.model.setOutlinePage(this);
}
public IOutlineModel getOutlineModel() {
return model;
}
public BaseEditor getEditor() {
return editorView;
}
/**
* Parsed partition creates an outline that shows imports/classes/methods
*/
protected void createParsedOutline() {
final TreeViewer tree = getTreeViewer();
IDocumentProvider provider = editorView.getDocumentProvider();
document = provider.getDocument(editorView.getEditorInput());
tree.setAutoExpandLevel(2);
tree.setContentProvider(new ParsedContentProvider());
tree.setLabelProvider(new ParsedLabelProvider(imageCache));
tree.setInput(getOutlineModel().getRoot());
}
public boolean isDisconnectedFromTree() {
TreeViewer treeViewer2 = getTreeViewer();
if (treeViewer2 == null) {
return true;
}
Tree tree = treeViewer2.getTree();
if (tree == null) {
return true;
}
return tree.isDisposed();
}
@Override
public void dispose() {
onControlDisposed.call(getTreeViewer());
if (createdCallbacksForControls != null) {
for (Object o : createdCallbacksForControls) {
onControlDisposed.call(o);
}
createdCallbacksForControls = null;
}
//note: don't dispose on the model (we don't have ownership for it).
if (selectionListener != null) {
removeSelectionChangedListener(selectionListener);
}
//Note: not disposing of the image cache (the 'global' one is meant to be used).
// if (imageCache != null) {
// imageCache.dispose();
// }
if (linkWithEditor != null) {
linkWithEditor.dispose();
linkWithEditor = null;
}
super.dispose();
}
/**
* 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 (isDisconnectedFromTree()) {
return;
}
ScrollBar bar = treeWidget.getVerticalBar();
int barPosition = 0;
if (bar != null) {
barPosition = bar.getSelection();
}
if (items == null) {
if (isDisconnectedFromTree()) {
return;
}
viewer.refresh();
} else {
if (isDisconnectedFromTree()) {
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 (isDisconnectedFromTree()) {
return;
}
TreeViewer tree = getTreeViewer();
if (tree != null) {
tree.update(items, null);
}
} finally {
relinkAll();
}
}
/**
* 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 final Object linkLock = new Object();
/**
* Stops listening to changes (the linkLevel is used so that multiple unlinks can be called and later
* multiple relinks should be used)
*/
public void unlinkAll() {
synchronized (linkLock) {
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
*/
public void relinkAll() {
synchronized (linkLock) {
linkLevel++;
if (linkLevel == 1) {
addSelectionChangedListener(selectionListener);
if (linkWithEditor != null) {
linkWithEditor.relink();
}
} else if (linkLevel > 1) {
throw new RuntimeException("Error: relinking without unlinking 1st");
}
}
}
protected void createActions() {
linkWithEditor = new OutlineLinkWithEditorAction(this, imageCache, pluginId);
//---- Collapse all
Action collapseAll = new Action("Collapse all", IAction.AS_PUSH_BUTTON) {
@Override
public void run() {
TreeViewer treeViewer2 = getTreeViewer();
Tree tree = treeViewer2.getTree();
tree.setRedraw(false);
try {
getTreeViewer().collapseAll();
} finally {
tree.setRedraw(true);
}
}
};
//---- Expand all
Action expandAll = new Action("Expand all", IAction.AS_PUSH_BUTTON) {
@Override
public void run() {
TreeViewer treeViewer2 = getTreeViewer();
Tree tree = treeViewer2.getTree();
tree.setRedraw(false);
try {
treeViewer2.expandAll();
} finally {
tree.setRedraw(true);
}
}
};
collapseAll.setImageDescriptor(imageCache.getDescriptor(UIConstants.COLLAPSE_ALL));
collapseAll.setId("outline.page.collapse");
expandAll.setImageDescriptor(imageCache.getDescriptor(UIConstants.EXPAND_ALL));
expandAll.setId("outline.page.expand");
// Add actions to the toolbar
IActionBars actionBars = getSite().getActionBars();
IToolBarManager toolbarManager = actionBars.getToolBarManager();
OutlineSortByNameAction action = new OutlineSortByNameAction(this, imageCache, pluginId);
action.setId("outline.page.sort");
toolbarManager.add(action);
toolbarManager.add(collapseAll);
toolbarManager.add(expandAll);
IMenuManager menuManager = actionBars.getMenuManager();
menuManager.add(linkWithEditor);
}
/**
* create the outline view widgets
*/
@Override
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() {
@Override
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
IParsedItem firstElement = (IParsedItem) 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) {
ISimpleNode[] node = getOutlineModel().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() {
@Override
public void mouseDoubleClick(MouseEvent e) {
tryToMakeSelection();
}
@Override
public void mouseDown(MouseEvent e) {
}
@Override
public void mouseUp(MouseEvent e) {
tryToMakeSelection();
}
});
tree.addKeyListener(new KeyListener() {
@Override
public void keyPressed(KeyEvent e) {
}
@Override
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 {
controls.add(c);
callback.call(c);
for (Control child : c.getChildren()) {
if (child instanceof Composite) {
callRecursively(callback, (Composite) child, controls);
} else {
controls.add(child);
callback.call(child);
}
}
} catch (Throwable e) {
Log.log(e);
}
return controls;
}
@Override
public boolean show(ShowInContext context) {
linkWithEditor.doLinkOutlinePosition(this.editorView, this,
EditorUtils.createTextSelectionUtils(this.editorView));
return true;
}
@Override
public Object getAdapter(Class adapter) {
if (adapter == IShowInTarget.class) {
return this;
}
return null;
}
@Override
public void selectionChanged(SelectionChangedEvent event) {
super.selectionChanged(event);
}
/**
* 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;
}
}