blob: a3ef4bb476ffa163cce2fbf0bde42813cbe5f32b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005 IBM Corporation 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.debug.internal.ui.model.viewers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.internal.ui.viewers.TreePath;
import org.eclipse.debug.internal.ui.viewers.TreeSelection;
import org.eclipse.debug.internal.ui.viewers.provisional.IAsynchronousContentAdapter;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.Assert;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.events.TreeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* A tree viewer that retrieves children and labels asynchronously via adapters
* and supports duplicate elements in the tree with different parents.
* Retrieving children and labels asynchrnously allows for arbitrary latency
* without blocking the UI thread.
* <p>
* TODO: tree editor not implemented TODO: table tree - what implications does
* it have on IPresentationAdapter?
*
* TODO: Deprecate the public/abstract deferred workbench adapter in favor of
* the presentation adapter.
* </p>
* <p>
* Clients may instantiate and subclass this class.
* </p>
*
* @since 3.2
*/
public class AsynchronousTreeModelViewer extends AsynchronousModelViewer implements Listener {
private static final Rectangle NOT_VISIBLE = new Rectangle(0, 0, 0, 0);
/**
* The tree
*/
private Tree fTree;
/**
* Collection of tree paths to be expanded. A path is removed from the
* collection when it is expanded. The entire list is cleared when the input
* to the viewer changes.
*/
private List fPendingExpansion = new ArrayList();
/**
* Map of parent nodes for which chidlren were needed to "set data"
* in the virtual tree. A parent is added to this map when we try go
* get children but they aren't there yet. The children are retrieved
* asynchronously, and later put back into the tree widgetry.
* The value is an array of intsfor the indicies of the children that
* were requested.
*/
private Map fParentsPendingChildren = new HashMap();
/**
* Creates an asynchronous tree viewer on a newly-created tree control under
* the given parent. The tree control is created using the SWT style bits
* <code>MULTI, H_SCROLL, V_SCROLL,</code> and <code>BORDER</code>. The
* viewer has no input, no content provider, a default label provider, no
* sorter, and no filters.
*
* @param parent
* the parent control
*/
public AsynchronousTreeModelViewer(Composite parent) {
this(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.VIRTUAL);
}
/**
* Creates an asynchronous tree viewer on a newly-created tree control under
* the given parent. The tree control is created using the given SWT style
* bits. The viewer has no input.
*
* @param parent
* the parent control
* @param style
* the SWT style bits used to create the tree.
*/
public AsynchronousTreeModelViewer(Composite parent, int style) {
this(new Tree(parent, style));
}
/**
* Creates an asynchronous tree viewer on the given tree control. The viewer
* has no input, no content provider, a default label provider, no sorter,
* and no filters.
*
* @param tree
* the tree control
*/
public AsynchronousTreeModelViewer(Tree tree) {
super();
Assert.isTrue((tree.getStyle() & SWT.VIRTUAL) != 0);
fTree = tree;
hookControl(fTree);
setUseHashlookup(false);
tree.addTreeListener(new TreeListener() {
public void treeExpanded(TreeEvent e) {
((TreeItem) e.item).setExpanded(true);
internalRefresh(e.item.getData(), e.item);
}
public void treeCollapsed(TreeEvent e) {
}
});
tree.addMouseListener(new MouseListener() {
public void mouseUp(MouseEvent e) {
}
public void mouseDown(MouseEvent e) {
}
public void mouseDoubleClick(MouseEvent e) {
TreeItem item = ((Tree) e.widget).getItem(new Point(e.x, e.y));
if (item != null) {
if (item.getExpanded()) {
item.setExpanded(false);
} else {
item.setExpanded(true);
internalRefresh(item.getData(), item);
}
}
}
});
tree.addListener(SWT.SetData, this);
}
/**
* Returns the tree control for this viewer.
*
* @return the tree control for this viewer
*/
public Tree getTree() {
return fTree;
}
/**
* Updates whether the given element has children.
*
* @param element
* element to update
* @param widget
* widget associated with the element in this viewer's tree
*/
protected void updateHasChildren(Object element, Widget widget) {
ModelNode node = getModel().getNode(widget);
if (node != null) {
((AsynchronousTreeModel)getModel()).updateHasChildren(node);
}
}
/**
* Updates the children of the given element.
*
* @param parent
* element of which to update children
* @param widget
* widget associated with the element in this viewer's tree
*/
protected void updateChildren(Object parent, Widget widget) {
ModelNode node = getModel().getNode(widget);
if (node != null) {
((AsynchronousTreeModel)getModel()).updateChildren(node);
}
}
/**
* Returns the tree element adapter for the given element or
* <code>null</code> if none.
*
* @param element
* element to retrieve adapter for
* @return presentation adapter or <code>null</code>
*/
protected IAsynchronousContentAdapter getContentAdapter(Object element) {
IAsynchronousContentAdapter adapter = null;
if (element instanceof IAsynchronousContentAdapter) {
adapter = (IAsynchronousContentAdapter) element;
} else if (element instanceof IAdaptable) {
IAdaptable adaptable = (IAdaptable) element;
adapter = (IAsynchronousContentAdapter) adaptable.getAdapter(IAsynchronousContentAdapter.class);
}
return adapter;
}
/**
* Expands all elements in the given tree selection.
*
* @param selection
*/
public synchronized void expand(ISelection selection) {
if (selection instanceof TreeSelection) {
TreePath[] paths = ((TreeSelection) selection).getPaths();
for (int i = 0; i < paths.length; i++) {
fPendingExpansion.add(paths[i]);
}
if (getControl().getDisplay().getThread() == Thread.currentThread()) {
attemptExpansion();
} else {
WorkbenchJob job = new WorkbenchJob("attemptExpansion") { //$NON-NLS-1$
public IStatus runInUIThread(IProgressMonitor monitor) {
attemptExpansion();
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule();
}
}
}
/**
* Attempts to expand all pending expansions.
*/
synchronized void attemptExpansion() {
if (fPendingExpansion != null) {
for (Iterator i = fPendingExpansion.iterator(); i.hasNext();) {
TreePath path = (TreePath) i.next();
if (attemptExpansion(path)) {
i.remove();
}
}
}
}
/**
* Attempts to expand the given tree path and returns whether the expansion
* was completed.
*
* @param path path to expand
* @return whether the expansion was completed
*/
synchronized boolean attemptExpansion(TreePath path) {
boolean tryAgain = false;
int segmentCount = path.getSegmentCount();
for (int j = segmentCount - 1; j >= 0; j--) {
Object element = path.getSegment(j);
ModelNode[] nodes = getModel().getNodes(element);
if (nodes != null) {
for (int k = 0; k < nodes.length; k++) {
ModelNode node = nodes[k];
TreePath treePath = node.getTreePath();
if (path.startsWith(treePath)) {
if (!node.isDisposed()) {
Widget widget = node.getWidget();
if (widget == null) {
// force the widgets to be mapped
ModelNode parent = node.getParentNode();
ModelNode child = node;
widget = parent.getWidget();
while (widget == null && parent != null) {
child = parent;
parent = parent.getParentNode();
if (parent != null) {
widget = parent.getWidget();
treePath = parent.getTreePath();
}
}
int childIndex = parent.getChildIndex(child);
if (childIndex < 0) {
return false;
}
TreeItem[] items = getItems(widget);
if (childIndex < items.length) {
getModel().mapWidget(items[childIndex], child);
widget = child.getWidget();
treePath = child.getTreePath();
tryAgain = true;
} else {
return false;
}
}
if (widget instanceof TreeItem) {
TreeItem treeItem = (TreeItem) widget;
if (treeItem.getExpanded()) {
return path.getSegmentCount() == treePath.getSegmentCount();
}
if (treeItem.getItemCount() > 0) {
updateChildren(element, treeItem);
expand(treeItem);
if (path.getSegmentCount() == treePath.getSegmentCount()) {
return true;
}
if (tryAgain) {
return attemptExpansion(path);
}
return false;
}
}
}
}
}
}
}
if (tryAgain) {
return attemptExpansion(path);
}
return false;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.viewers.Viewer#getControl()
*/
public Control getControl() {
return fTree;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object,
* java.lang.Object)
*/
synchronized protected void inputChanged(Object input, Object oldInput) {
fPendingExpansion.clear();
super.inputChanged(input, oldInput);
}
/**
* Constructs and returns a tree path for the given item. Must be called
* from the UI thread.
*
* @param item
* item to constuct a path for
* @return tree path for the item
*/
protected synchronized TreePath getTreePath(TreeItem item) {
TreeItem parent = item;
List path = new ArrayList();
while (parent != null && !parent.isDisposed()) {
Object parentElement = parent.getData();
if (parentElement == null) {
return new TreePath(new Object[0]);
}
path.add(0, parentElement);
parent = parent.getParentItem();
}
path.add(0, fTree.getData());
return new TreePath(path.toArray());
}
/**
* Returns the tree paths to the given element in this viewer, possibly
* empty.
*
* @param element model element
* @return the paths to the element in this viewer
*/
public TreePath[] getTreePaths(Object element) {
ModelNode[] nodes = getModel().getNodes(element);
if (nodes == null) {
return new TreePath[]{};
}
TreePath[] paths = new TreePath[nodes.length];
for (int i = 0; i < nodes.length; i++) {
paths[i] = nodes[i].getTreePath();
}
return paths;
}
/**
* The children of a node have changed.
*
* @param parent
*/
protected void nodeChildrenChanged(ModelNode parentNode) {
Widget widget = parentNode.getWidget();
if (widget != null && !widget.isDisposed()) {
int childCount = parentNode.getChildCount();
setItemCount(widget, childCount);
attemptExpansion();
attemptSelection(false);
}
}
/**
* Container status of a node changed
*
* @param node
*/
protected void nodeContainerChanged(ModelNode node) {
Widget widget = node.getWidget();
if (widget != null && !widget.isDisposed()) {
if (isVisible(widget)) {
boolean expanded = true;
if (node.isContainer() && getItemCount(widget) == 0) {
setItemCount(widget, 1);
}
if (widget instanceof TreeItem) {
expanded = ((TreeItem)widget).getExpanded();
}
if (expanded) {
updateChildren(node.getElement(), widget);
}
}
}
attemptExpansion();
attemptSelection(false);
}
protected int getItemCount(Widget widget) {
if (widget instanceof TreeItem) {
return ((TreeItem) widget).getItemCount();
}
return ((Tree) widget).getItemCount();
}
protected void doUpdateItem(Widget item, Object element, boolean fullMap) {
super.doUpdateItem(item, element, fullMap);
updateHasChildren(element, item);
}
private void setItemCount(Widget widget, int itemCount) {
if (widget == fTree) {
fTree.setItemCount(itemCount);
} else {
((TreeItem) widget).setItemCount(itemCount);
}
}
private TreeItem[] getItems(Widget widget) {
if (widget instanceof TreeItem) {
return ((TreeItem) widget).getItems();
} else {
return fTree.getItems();
}
}
public void handleEvent(final Event event) {
// TODO: workaround for bug 125499: don't preserve selection when item is revealed
// preservingSelection(new Runnable() {
// public void run() {
TreeItem item = (TreeItem) event.item;
Widget parentItem = item.getParentItem();
int index = 0;
if (parentItem != null) {
index = ((TreeItem) parentItem).indexOf(item);
} else {
parentItem = fTree;
index = fTree.indexOf(item);
}
ModelNode parentNode = getModel().getModelNode(parentItem);
if (parentNode != null) {
ModelNode[] childrenNodes = parentNode.getChildrenNodes();
if (childrenNodes != null && index < childrenNodes.length) {
ModelNode child = childrenNodes[index];
getModel().mapWidget(item, child);
internalRefresh(child.getElement(), item);
attemptExpansion();
attemptSelection(false);
} else {
int[] indicies = (int[]) fParentsPendingChildren.get(parentNode);
if (indicies == null) {
indicies = new int[]{index};
} else {
int[] next = new int[indicies.length + 1];
System.arraycopy(indicies, 0, next, 0, indicies.length);
next[indicies.length] = index;
indicies = next;
}
fParentsPendingChildren.put(parentNode, indicies);
}
}
// }
// });
}
/**
* Expands the given tree item and all of its parents. Does *not* update
* elements or retrieve children.
*
* @param child
* item to expand
*/
private void expand(TreeItem child) {
if (!child.getExpanded()) {
child.setExpanded(true);
TreeItem parent = child.getParentItem();
if (parent != null) {
expand(parent);
}
}
}
private void clear(Widget widget) {
if (widget instanceof TreeItem) {
TreeItem item = (TreeItem) widget;
item.clearAll(true);
} else {
fTree.clearAll(true);
}
}
private boolean isVisible(Widget widget) {
if (widget instanceof Tree) {
return true;
} else {
TreeItem item = (TreeItem) widget;
Rectangle itemBounds = item.getBounds();
return !NOT_VISIBLE.equals(itemBounds);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object)
*/
protected Widget doFindInputItem(Object element) {
if (element.equals(getInput())) {
return fTree;
}
return null;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.viewers.StructuredViewer#doFindItem(java.lang.Object)
*/
protected Widget doFindItem(Object element) {
Widget[] widgets = getWidgets(element);
if (widgets != null && widgets.length > 0) {
return widgets[0];
}
return null;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.internal.ui.viewers.AsynchronousModelViewer#newSelectionFromWidget()
*/
protected ISelection newSelectionFromWidget() {
Control control = getControl();
if (control == null || control.isDisposed()) {
return StructuredSelection.EMPTY;
}
List list = getSelectionFromWidget();
return new TreeSelection((TreePath[]) list.toArray());
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.viewers.StructuredViewer#getSelectionFromWidget()
*/
protected synchronized List getSelectionFromWidget() {
TreeItem[] selection = fTree.getSelection();
TreePath[] paths = new TreePath[selection.length];
for (int i = 0; i < selection.length; i++) {
paths[i] = getTreePath(selection[i]);
}
return Arrays.asList(paths);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.ui.viewers.AsynchronousViewer#internalRefresh(java.lang.Object,
* org.eclipse.swt.widgets.Widget)
*/
protected synchronized void internalRefresh(Object element, Widget item) {
super.internalRefresh(element, item);
updateHasChildren(element, item);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.viewers.StructuredViewer#reveal(java.lang.Object)
*/
public void reveal(Object element) {
Widget[] widgets = getWidgets(element);
if (widgets != null && widgets.length > 0) {
// TODO: only reveals the first occurrence - should we reveal all?
TreeItem item = (TreeItem) widgets[0];
Tree tree = (Tree) getControl();
tree.showItem(item);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.ui.viewers.AsynchronousViewer#doAttemptSelectionToWidget(org.eclipse.jface.viewers.ISelection,
* boolean)
*/
protected synchronized ISelection doAttemptSelectionToWidget(ISelection selection, boolean reveal) {
List remaining = new ArrayList();
if (!selection.isEmpty()) {
List toSelect = new ArrayList();
List theElements = new ArrayList();
TreeSelection treeSelection = (TreeSelection) selection;
TreePath[] paths = treeSelection.getPaths();
for (int i = 0; i < paths.length; i++) {
TreePath path = paths[i];
if (path == null) {
continue;
}
ModelNode[] nodes = getModel().getNodes(path.getLastSegment());
boolean selected = false;
if (nodes != null) {
for (int j = 0; j < nodes.length; j++) {
ModelNode node = nodes[j];
if (node.correspondsTo(path)) {
Widget widget = node.getWidget();
if (widget != null && !widget.isDisposed()) {
toSelect.add(widget);
theElements.add(path.getLastSegment());
selected = true;
break;
}
}
}
}
if (!selected) {
remaining.add(path);
}
}
if (!toSelect.isEmpty()) {
final TreeItem[] items = (TreeItem[]) toSelect.toArray(new TreeItem[toSelect.size()]);
// TODO: hack to ensure selection contains 'selected' element
// instead of 'equivalent' element. Handles synch problems
// between set selection & refresh
for (int i = 0; i < items.length; i++) {
TreeItem item = items[i];
Object element = theElements.get(i);
if (!item.isDisposed() && item.getData() != element) {
ModelNode node = getModel().getNode(item);
node.remap(element);
item.setData(element);
}
}
fTree.setSelection(items);
if (reveal) {
fTree.showItem(items[0]);
}
}
} else {
fTree.setSelection(new TreeItem[0]);
}
return new TreeSelection((TreePath[]) remaining.toArray(new TreePath[remaining.size()]));
}
/**
* Collapses all items in the tree.
*/
public void collapseAll() {
TreeItem[] items = fTree.getItems();
for (int i = 0; i < items.length; i++) {
TreeItem item = items[i];
if (item.getExpanded())
collapse(item);
}
}
/**
* Collaspes the given item and all of its children items.
*
* @param item
* item to collapose recursively
*/
protected void collapse(TreeItem item) {
TreeItem[] items = item.getItems();
for (int i = 0; i < items.length; i++) {
TreeItem child = items[i];
if (child.getExpanded()) {
collapse(child);
}
}
item.setExpanded(false);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.ui.viewers.AsynchronousViewer#setColor(org.eclipse.swt.widgets.Widget,
* org.eclipse.swt.graphics.RGB, org.eclipse.swt.graphics.RGB)
*/
void setColors(Widget widget, RGB[] foregrounds, RGB[] backgrounds) {
if (widget instanceof TreeItem) {
TreeItem item = (TreeItem) widget;
Color[] fgs = getColor(foregrounds);
Color[] bgs = getColor(backgrounds);
for (int i = 0; i < bgs.length; i++) {
item.setForeground(i, fgs[i]);
item.setBackground(i, bgs[i]);
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.ui.viewers.AsynchronousViewer#setFont(org.eclipse.swt.widgets.Widget,
* org.eclipse.swt.graphics.FontData)
*/
void setFonts(Widget widget, FontData[] fontData) {
if (widget instanceof TreeItem) {
TreeItem item = (TreeItem) widget;
Font[] fonts = getFonts(fontData);
for (int i = 0; i < fonts.length; i++) {
item.setFont(i, fonts[i]);
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.ui.viewers.AsynchronousViewer#acceptsSelection(org.eclipse.jface.viewers.ISelection)
*/
protected boolean acceptsSelection(ISelection selection) {
return selection instanceof TreeSelection;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.ui.viewers.AsynchronousViewer#getEmptySelection()
*/
protected ISelection getEmptySelection() {
return new TreeSelection(new TreePath[0]);
}
/**
* Adds the item specified by the given tree path to this tree. Can be
* called in a non-UI thread.
*
* @param treePath
*/
public void add(TreePath treePath) {
((AsynchronousTreeModel)getModel()).add(treePath);
}
/**
* Removes the item specified in the given tree path from this tree. Can be
* called in a non-UI thread.
*
* @param treePath
*/
public void remove(TreePath treePath) {
synchronized (this) {
for (Iterator i = fPendingExpansion.iterator(); i.hasNext();) {
TreePath expansionPath = (TreePath) i.next();
if (expansionPath.startsWith(treePath)) {
i.remove();
}
}
}
((AsynchronousTreeModel)getModel()).remove(treePath);
}
void setLabels(Widget widget, String[] text, ImageDescriptor[] image) {
if (widget instanceof TreeItem) {
TreeItem item = (TreeItem) widget;
if (!item.isDisposed()) {
item.setText(text);
item.setImage(getImages(image));
}
}
}
/* (non-Javadoc)
* @see org.eclipse.debug.internal.ui.viewers.AsynchronousModelViewer#createModel(java.lang.Object)
*/
protected AsynchronousModel createModel(Object input) {
return new AsynchronousTreeModel(this, input, getControl());
}
/* (non-Javadoc)
* @see org.eclipse.debug.internal.ui.viewers.AsynchronousModelViewer#nodeChanged(org.eclipse.debug.internal.ui.viewers.ModelNode)
*/
protected void nodeChanged(ModelNode node) {
Widget widget = node.getWidget();
if (widget != null && !widget.isDisposed()) {
if (widget instanceof TreeItem) {
if (!isVisible(widget)) {
clear(widget);
return;
}
}
widget.setData(node.getElement());
getModel().mapWidget(widget, node);
internalRefresh(node.getElement(), widget);
attemptExpansion();
attemptSelection(false);
}
}
/**
* Called when nodes are set in the model. The children may not have been retrieved
* yet when the tree got the call to "set data".
*
* @param parent
* @param children
*/
protected void nodeChildrenSet(ModelNode parent, ModelNode[] children) {
int[] indicies = (int[]) fParentsPendingChildren.remove(parent);
if (indicies != null) {
Widget widget = parent.getWidget();
if (widget != null && !widget.isDisposed()) {
for (int i = 0; i < indicies.length; i++) {
int index = indicies[i];
TreeItem item = null;
if (widget instanceof TreeItem) {
TreeItem treeItem = (TreeItem)widget;
if (index < treeItem.getItemCount()) {
item = treeItem.getItem(index);
}
} else {
Tree tree = (Tree)widget;
if (index < tree.getItemCount()) {
item = ((Tree)widget).getItem(index);
}
}
if (item != null) {
if (index < children.length) {
getModel().mapWidget(item, children[index]);
internalRefresh(children[index].getElement(), item);
}
}
}
setItemCount(widget, children.length);
attemptExpansion();
attemptSelection(false);
}
}
}
/* (non-Javadoc)
* @see org.eclipse.debug.internal.ui.model.viewers.AsynchronousModelViewer#createUpdatePolicy()
*/
public IModelUpdatePolicy createUpdatePolicy() {
return new TreeUpdatePolicy();
}
}