]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl.java
Fixed all line endings of the repository
[simantics/platform.git] / bundles / org.simantics.browsing.ui.swt / src / org / simantics / browsing / ui / swt / GraphExplorerImpl.java
index 3085e6f07e58890a7bce4368ff8c63fa939e9ff6..62b284858cd9893383719e5ec7b89632437335f9 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2012 Association for Decentralized Information Management\r
- * in Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
-package org.simantics.browsing.ui.swt;\r
-\r
-import java.util.ArrayList;\r
-import java.util.Arrays;\r
-import java.util.Collection;\r
-import java.util.Collections;\r
-import java.util.Deque;\r
-import java.util.HashMap;\r
-import java.util.HashSet;\r
-import java.util.Iterator;\r
-import java.util.LinkedList;\r
-import java.util.List;\r
-import java.util.Map;\r
-import java.util.Set;\r
-import java.util.WeakHashMap;\r
-import java.util.concurrent.CopyOnWriteArrayList;\r
-import java.util.concurrent.ExecutorService;\r
-import java.util.concurrent.Future;\r
-import java.util.concurrent.ScheduledExecutorService;\r
-import java.util.concurrent.TimeUnit;\r
-import java.util.concurrent.atomic.AtomicBoolean;\r
-import java.util.concurrent.atomic.AtomicReference;\r
-import java.util.function.Consumer;\r
-\r
-import org.eclipse.core.runtime.Assert;\r
-import org.eclipse.core.runtime.AssertionFailedException;\r
-import org.eclipse.core.runtime.IProgressMonitor;\r
-import org.eclipse.core.runtime.IStatus;\r
-import org.eclipse.core.runtime.MultiStatus;\r
-import org.eclipse.core.runtime.Platform;\r
-import org.eclipse.core.runtime.Status;\r
-import org.eclipse.core.runtime.jobs.Job;\r
-import org.eclipse.jface.action.IStatusLineManager;\r
-import org.eclipse.jface.resource.ColorDescriptor;\r
-import org.eclipse.jface.resource.DeviceResourceException;\r
-import org.eclipse.jface.resource.DeviceResourceManager;\r
-import org.eclipse.jface.resource.FontDescriptor;\r
-import org.eclipse.jface.resource.ImageDescriptor;\r
-import org.eclipse.jface.resource.JFaceResources;\r
-import org.eclipse.jface.resource.LocalResourceManager;\r
-import org.eclipse.jface.resource.ResourceManager;\r
-import org.eclipse.jface.viewers.IPostSelectionProvider;\r
-import org.eclipse.jface.viewers.ISelection;\r
-import org.eclipse.jface.viewers.ISelectionChangedListener;\r
-import org.eclipse.jface.viewers.ISelectionProvider;\r
-import org.eclipse.jface.viewers.IStructuredSelection;\r
-import org.eclipse.jface.viewers.SelectionChangedEvent;\r
-import org.eclipse.jface.viewers.StructuredSelection;\r
-import org.eclipse.jface.viewers.TreeSelection;\r
-import org.eclipse.swt.SWT;\r
-import org.eclipse.swt.SWTException;\r
-import org.eclipse.swt.custom.CCombo;\r
-import org.eclipse.swt.custom.TreeEditor;\r
-import org.eclipse.swt.events.FocusEvent;\r
-import org.eclipse.swt.events.FocusListener;\r
-import org.eclipse.swt.events.KeyEvent;\r
-import org.eclipse.swt.events.KeyListener;\r
-import org.eclipse.swt.events.MouseEvent;\r
-import org.eclipse.swt.events.MouseListener;\r
-import org.eclipse.swt.events.SelectionEvent;\r
-import org.eclipse.swt.events.SelectionListener;\r
-import org.eclipse.swt.graphics.Color;\r
-import org.eclipse.swt.graphics.Font;\r
-import org.eclipse.swt.graphics.GC;\r
-import org.eclipse.swt.graphics.Image;\r
-import org.eclipse.swt.graphics.Point;\r
-import org.eclipse.swt.graphics.RGB;\r
-import org.eclipse.swt.graphics.Rectangle;\r
-import org.eclipse.swt.widgets.Composite;\r
-import org.eclipse.swt.widgets.Control;\r
-import org.eclipse.swt.widgets.Display;\r
-import org.eclipse.swt.widgets.Event;\r
-import org.eclipse.swt.widgets.Listener;\r
-import org.eclipse.swt.widgets.ScrollBar;\r
-import org.eclipse.swt.widgets.Shell;\r
-import org.eclipse.swt.widgets.Text;\r
-import org.eclipse.swt.widgets.Tree;\r
-import org.eclipse.swt.widgets.TreeColumn;\r
-import org.eclipse.swt.widgets.TreeItem;\r
-import org.eclipse.ui.IWorkbenchPart;\r
-import org.eclipse.ui.IWorkbenchSite;\r
-import org.eclipse.ui.PlatformUI;\r
-import org.eclipse.ui.contexts.IContextActivation;\r
-import org.eclipse.ui.contexts.IContextService;\r
-import org.eclipse.ui.services.IServiceLocator;\r
-import org.eclipse.ui.swt.IFocusService;\r
-import org.simantics.browsing.ui.BuiltinKeys;\r
-import org.simantics.browsing.ui.CheckedState;\r
-import org.simantics.browsing.ui.Column;\r
-import org.simantics.browsing.ui.Column.Align;\r
-import org.simantics.browsing.ui.DataSource;\r
-import org.simantics.browsing.ui.ExplorerState;\r
-import org.simantics.browsing.ui.GraphExplorer;\r
-import org.simantics.browsing.ui.NodeContext;\r
-import org.simantics.browsing.ui.NodeContext.CacheKey;\r
-import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey;\r
-import org.simantics.browsing.ui.NodeContext.QueryKey;\r
-import org.simantics.browsing.ui.NodeContextPath;\r
-import org.simantics.browsing.ui.NodeQueryManager;\r
-import org.simantics.browsing.ui.NodeQueryProcessor;\r
-import org.simantics.browsing.ui.PrimitiveQueryProcessor;\r
-import org.simantics.browsing.ui.SelectionDataResolver;\r
-import org.simantics.browsing.ui.SelectionFilter;\r
-import org.simantics.browsing.ui.StatePersistor;\r
-import org.simantics.browsing.ui.common.ColumnKeys;\r
-import org.simantics.browsing.ui.common.ErrorLogger;\r
-import org.simantics.browsing.ui.common.NodeContextBuilder;\r
-import org.simantics.browsing.ui.common.NodeContextUtil;\r
-import org.simantics.browsing.ui.common.internal.GECache;\r
-import org.simantics.browsing.ui.common.internal.GENodeQueryManager;\r
-import org.simantics.browsing.ui.common.internal.IGECache;\r
-import org.simantics.browsing.ui.common.internal.IGraphExplorerContext;\r
-import org.simantics.browsing.ui.common.internal.UIElementReference;\r
-import org.simantics.browsing.ui.common.processors.DefaultCheckedStateProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultComparableChildrenProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultFinalChildrenProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultImageDecoratorProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultImagerFactoriesProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultImagerProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultLabelDecoratorProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultLabelerFactoriesProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultLabelerProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultPrunedChildrenProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultSelectedImageDecoratorFactoriesProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelDecoratorFactoriesProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelerProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointFactoryProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionsProcessor;\r
-import org.simantics.browsing.ui.common.processors.DefaultViewpointProcessor;\r
-import org.simantics.browsing.ui.common.processors.IsExpandedProcessor;\r
-import org.simantics.browsing.ui.common.processors.NoSelectionRequestProcessor;\r
-import org.simantics.browsing.ui.common.processors.ProcessorLifecycle;\r
-import org.simantics.browsing.ui.content.ImageDecorator;\r
-import org.simantics.browsing.ui.content.Imager;\r
-import org.simantics.browsing.ui.content.LabelDecorator;\r
-import org.simantics.browsing.ui.content.Labeler;\r
-import org.simantics.browsing.ui.content.Labeler.CustomModifier;\r
-import org.simantics.browsing.ui.content.Labeler.DeniedModifier;\r
-import org.simantics.browsing.ui.content.Labeler.DialogModifier;\r
-import org.simantics.browsing.ui.content.Labeler.EnumerationModifier;\r
-import org.simantics.browsing.ui.content.Labeler.FilteringModifier;\r
-import org.simantics.browsing.ui.content.Labeler.LabelerListener;\r
-import org.simantics.browsing.ui.content.Labeler.Modifier;\r
-import org.simantics.browsing.ui.content.PrunedChildrenResult;\r
-import org.simantics.browsing.ui.model.nodetypes.EntityNodeType;\r
-import org.simantics.browsing.ui.model.nodetypes.NodeType;\r
-import org.simantics.browsing.ui.swt.internal.Threads;\r
-import org.simantics.db.layer0.SelectionHints;\r
-import org.simantics.utils.ObjectUtils;\r
-import org.simantics.utils.datastructures.BijectionMap;\r
-import org.simantics.utils.datastructures.BinaryFunction;\r
-import org.simantics.utils.datastructures.disposable.AbstractDisposable;\r
-import org.simantics.utils.datastructures.hints.IHintContext;\r
-import org.simantics.utils.threads.IThreadWorkQueue;\r
-import org.simantics.utils.threads.SWTThread;\r
-import org.simantics.utils.threads.ThreadUtils;\r
-import org.simantics.utils.ui.ISelectionUtils;\r
-import org.simantics.utils.ui.jface.BasePostSelectionProvider;\r
-import org.simantics.utils.ui.widgets.VetoingEventHandler;\r
-import org.simantics.utils.ui.workbench.WorkbenchUtils;\r
-\r
-import gnu.trove.map.hash.THashMap;\r
-import gnu.trove.procedure.TObjectProcedure;\r
-import gnu.trove.set.hash.THashSet;\r
-\r
-/**\r
- * @see #getMaxChildren()\r
- * @see #setMaxChildren(int)\r
- * @see #getMaxChildren(NodeQueryManager, NodeContext)\r
- */\r
-class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {\r
-\r
-       private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {\r
-               \r
-               private GraphExplorerImpl ge;\r
-               \r
-               GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {\r
-                       this.ge = ge;\r
-               }\r
-               \r
-               void dispose() {\r
-                       ge = null;\r
-               }\r
-               \r
-           @Override\r
-           public void setSelection(final ISelection selection) {\r
-               if(ge == null) return;\r
-               ge.setSelection(selection, false);\r
-           }\r
-           \r
-\r
-           @Override\r
-           public void removeSelectionChangedListener(ISelectionChangedListener listener) {\r
-               if(ge == null) return;\r
-               if(ge.isDisposed()) {\r
-                   if (DEBUG_SELECTION_LISTENERS)\r
-                       System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);\r
-                   return;\r
-               }\r
-               //assertNotDisposed();\r
-               //System.out.println("Remove selection changed listener: " + listener);\r
-               ge.selectionProvider.removeSelectionChangedListener(listener);\r
-           }\r
-           \r
-           @Override\r
-           public void addPostSelectionChangedListener(ISelectionChangedListener listener) {\r
-               if(ge == null) return;\r
-               if (!ge.thread.currentThreadAccess())\r
-                   throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());\r
-               if(ge.isDisposed()) {\r
-                   System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);\r
-                   return;\r
-               }\r
-               //System.out.println("Add POST selection changed listener: " + listener);\r
-               ge.selectionProvider.addPostSelectionChangedListener(listener);\r
-           }\r
-\r
-           @Override\r
-           public void removePostSelectionChangedListener(ISelectionChangedListener listener) {\r
-               if(ge == null) return;\r
-               if(ge.isDisposed()) {\r
-                   if (DEBUG_SELECTION_LISTENERS)\r
-                       System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);\r
-                   return;\r
-               }\r
-//             assertNotDisposed();\r
-               //System.out.println("Remove POST selection changed listener: " + listener);\r
-               ge.selectionProvider.removePostSelectionChangedListener(listener);\r
-           }\r
-           \r
-\r
-           @Override\r
-           public void addSelectionChangedListener(ISelectionChangedListener listener) {\r
-               if(ge == null) return;\r
-               if (!ge.thread.currentThreadAccess())\r
-                   throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());\r
-               //System.out.println("Add selection changed listener: " + listener);\r
-               if (ge.tree.isDisposed() || ge.selectionProvider == null) {\r
-                   System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);\r
-                   return;\r
-               }\r
-\r
-               ge.selectionProvider.addSelectionChangedListener(listener);\r
-           }\r
-\r
-           \r
-           @Override\r
-           public ISelection getSelection() {\r
-               if(ge == null) return StructuredSelection.EMPTY;\r
-               if (!ge.thread.currentThreadAccess())\r
-                   throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());\r
-               if (ge.tree.isDisposed() || ge.selectionProvider == null)\r
-                   return StructuredSelection.EMPTY;\r
-               return ge.selectionProvider.getSelection();\r
-           }\r
-           \r
-       }\r
-       \r
-    /**\r
-     * If this explorer is running with an Eclipse workbench open, this\r
-     * Workbench UI context will be activated whenever inline editing is started\r
-     * through {@link #startEditing(TreeItem, int)} and deactivated when inline\r
-     * editing finishes.\r
-     * \r
-     * This context information can be used to for UI handler activity testing.\r
-     */\r
-    private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";\r
-\r
-    private static final String KEY_DRAG_COLUMN = "dragColumn";\r
-\r
-    private static final boolean                   DEBUG_SELECTION_LISTENERS = false;\r
-\r
-    private static final int                       DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;\r
-\r
-    public static final int                        DEFAULT_MAX_CHILDREN                    = 1000;\r
-\r
-    private static final long                      POST_SELECTION_DELAY                    = 300;\r
-\r
-    /**\r
-     * The time in milliseconds that must elapse between consecutive\r
-     * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)}\r
-     * invocations in order for this class to construct a new selection.\r
-     * \r
-     * <p>\r
-     * This is done because selection construction can be very expensive as the\r
-     * selected set grows larger when the user is pressing shift+arrow keys.\r
-     * GraphExplorerImpl will naturally listen to all changes in the tree\r
-     * selection, but as an optimization will not construct new\r
-     * StructuredSelection instances for every selection change event. A new\r
-     * selection will be constructed and set only if the selection hasn't\r
-     * changed for the amount of milliseconds specified by this constant.\r
-     */\r
-    private static final long                      SELECTION_CHANGE_QUIET_TIME             = 150;\r
-\r
-    private final IThreadWorkQueue                 thread;\r
-\r
-    /**\r
-     * Local method for checking from whether resources are loaded in\r
-     * JFaceResources.\r
-     */\r
-    private final LocalResourceManager             localResourceManager;\r
-\r
-    /**\r
-     * Local device resource manager that is safe to use in\r
-     * {@link ImageLoaderJob} for creating images in a non-UI thread.\r
-     */\r
-    private final ResourceManager                  resourceManager;\r
-\r
-    /*\r
-     * Package visibility.\r
-     * TODO: Get rid of these.\r
-     */\r
-    Tree                                           tree;\r
-\r
-    @SuppressWarnings({ "rawtypes" })\r
-    final HashMap<CacheKey<?>, NodeQueryProcessor> processors            = new HashMap<CacheKey<?>, NodeQueryProcessor>();\r
-    @SuppressWarnings({ "rawtypes" })\r
-    final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors   = new HashMap<Object, PrimitiveQueryProcessor>();\r
-    @SuppressWarnings({ "rawtypes" })\r
-    final HashMap<Class, DataSource>               dataSources           = new HashMap<Class, DataSource>();\r
-    \r
-    class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {\r
-        // This is for query debugging only.\r
-        int                  queryIndent   = 0;\r
-\r
-        GECache              cache         = new GECache();\r
-        AtomicBoolean        propagating   = new AtomicBoolean(false);\r
-        Object               propagateList = new Object();\r
-        Object               propagate     = new Object();\r
-        List<Runnable>       scheduleList  = new ArrayList<Runnable>();\r
-        final Deque<Integer> activity      = new LinkedList<Integer>();\r
-        int                  activityInt   = 0;\r
-\r
-        /**\r
-         * Stores the currently running query update runnable. If\r
-         * <code>null</code> there's nothing scheduled yet in which case\r
-         * scheduling can commence. Otherwise the update should be skipped.\r
-         */\r
-        AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<Runnable>();\r
-\r
-        /**\r
-         * Keeps track of nodes that have already been auto-expanded. After\r
-         * being inserted into this set, nodes will not be forced to stay in an\r
-         * expanded state after that. This makes it possible for the user to\r
-         * close auto-expanded nodes.\r
-         */\r
-        Map<NodeContext, Boolean>     autoExpanded  = new WeakHashMap<NodeContext, Boolean>();\r
-\r
-        \r
-        @Override\r
-        protected void doDispose() {\r
-               saveState();\r
-            autoExpanded.clear();\r
-        }\r
-\r
-        @Override\r
-        public IGECache getCache() {\r
-            return cache;\r
-        }\r
-\r
-        @Override\r
-        public int queryIndent() {\r
-            return queryIndent;\r
-        }\r
-\r
-        @Override\r
-        public int queryIndent(int offset) {\r
-            queryIndent += offset;\r
-            return queryIndent;\r
-        }\r
-\r
-        @Override\r
-        @SuppressWarnings("unchecked")\r
-        public <T> NodeQueryProcessor<T> getProcessor(Object o) {\r
-            return processors.get(o);\r
-        }\r
-\r
-        @Override\r
-        @SuppressWarnings("unchecked")\r
-        public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(Object o) {\r
-            return primitiveProcessors.get(o);\r
-        }\r
-\r
-        @SuppressWarnings("unchecked")\r
-        @Override\r
-        public <T> DataSource<T> getDataSource(Class<T> clazz) {\r
-            return dataSources.get(clazz);\r
-        }\r
-\r
-        @Override\r
-        public void update(UIElementReference ref) {\r
-            //System.out.println("GE.update " + ref);\r
-            TreeItemReference tiref = (TreeItemReference) ref;\r
-            TreeItem item = tiref.getItem();\r
-            // NOTE: must be called regardless of the the item value.\r
-            // A null item is currently used to indicate a tree root update.\r
-            GraphExplorerImpl.this.update(item);\r
-        }\r
-\r
-        @Override\r
-        public Object getPropagateLock() {\r
-            return propagate;\r
-        }\r
-\r
-        @Override\r
-        public Object getPropagateListLock() {\r
-            return propagateList;\r
-        }\r
-\r
-        @Override\r
-        public boolean isPropagating() {\r
-            return propagating.get();\r
-        }\r
-\r
-        @Override\r
-        public void setPropagating(boolean b) {\r
-            this.propagating.set(b);\r
-        }\r
-\r
-        @Override\r
-        public List<Runnable> getScheduleList() {\r
-            return scheduleList;\r
-        }\r
-\r
-        @Override\r
-        public void setScheduleList(List<Runnable> list) {\r
-            this.scheduleList = list;\r
-        }\r
-\r
-        @Override\r
-        public Deque<Integer> getActivity() {\r
-            return activity;\r
-        }\r
-\r
-        @Override\r
-        public void setActivityInt(int i) {\r
-            this.activityInt = i;\r
-        }\r
-\r
-        @Override\r
-        public int getActivityInt() {\r
-            return activityInt;\r
-        }\r
-\r
-        @Override\r
-        public void scheduleQueryUpdate(Runnable r) {\r
-            if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())\r
-                return;\r
-            //System.out.println("Scheduling query update for runnable " + r);\r
-            if (currentQueryUpdater.compareAndSet(null, r)) {\r
-                //System.out.println("Scheduling query update for runnable " + r);\r
-                queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);\r
-            }\r
-        }\r
-\r
-        Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {\r
-            @Override\r
-            public void run() {\r
-                Runnable r = currentQueryUpdater.getAndSet(null);\r
-                if (r != null) {\r
-                    //System.out.println("Running query update runnable " + r);\r
-                    r.run();\r
-                }\r
-            }\r
-        };\r
-    }\r
-\r
-    GraphExplorerContext                         explorerContext     = new GraphExplorerContext();\r
-\r
-    HashSet<TreeItem>                            pendingItems        = new HashSet<TreeItem>();\r
-    boolean                                      updating            = false;\r
-    boolean                                      pendingRoot         = false;\r
-\r
-    @SuppressWarnings("deprecation")\r
-    ModificationContext                          modificationContext = null;\r
-\r
-    NodeContext                                  rootContext;\r
-\r
-    StatePersistor                               persistor           = null;\r
-\r
-    boolean                                      editable            = true;\r
-\r
-    /**\r
-     * This is a reverse mapping from {@link NodeContext} tree objects back to\r
-     * their owner TreeItems.\r
-     * \r
-     * <p>\r
-     * Access this map only in the SWT thread to keep it thread-safe.\r
-     * </p>\r
-     */\r
-    BijectionMap<NodeContext, TreeItem>         contextToItem     = new BijectionMap<NodeContext, TreeItem>();\r
-\r
-    /**\r
-     * Columns of the UI viewer. Use {@link #setColumns(Column[])} to\r
-     * initialize.\r
-     */\r
-    Column[]                                     columns           = new Column[0];\r
-    Map<String, Integer>                         columnKeyToIndex  = new HashMap<String, Integer>();\r
-    boolean                                      refreshingColumnSizes = false;\r
-    boolean                                      columnsAreVisible = true;\r
-\r
-    /**\r
-     * An array reused for invoking {@link TreeItem#setImage(Image[])} instead\r
-     * of constantly allocating new arrays for setting each TreeItems images.\r
-     * This works because {@link TreeItem#setImage(Image[])} does not take hold\r
-     * of the array itself, only the contents of the array.\r
-     * \r
-     * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)\r
-     */\r
-    Image[]                                      columnImageArray = { null };\r
-\r
-    /**\r
-     * Used for collecting Image or ImageDescriptor instances for a single\r
-     * TreeItem when initially setting images for a TreeItem.\r
-     * \r
-     * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)\r
-     */\r
-    Object[]                                     columnDescOrImageArray = { null };\r
-\r
-    final ExecutorService                        queryUpdateScheduler = Threads.getExecutor();\r
-    final ScheduledExecutorService               uiUpdateScheduler    = ThreadUtils.getNonBlockingWorkExecutor();\r
-\r
-    /** Set to true when the Tree widget is disposed. */\r
-    private boolean                              disposed                 = false;\r
-    private final CopyOnWriteArrayList<FocusListener>  focusListeners           = new CopyOnWriteArrayList<FocusListener>();\r
-    private final CopyOnWriteArrayList<MouseListener>  mouseListeners           = new CopyOnWriteArrayList<MouseListener>();\r
-    private final CopyOnWriteArrayList<KeyListener>    keyListeners             = new CopyOnWriteArrayList<KeyListener>();\r
-\r
-    /** Selection provider */\r
-    private   GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);\r
-    protected BasePostSelectionProvider          selectionProvider        = new BasePostSelectionProvider();\r
-    protected SelectionDataResolver              selectionDataResolver;\r
-    protected SelectionFilter                    selectionFilter;\r
-    protected BinaryFunction<Object[], GraphExplorer, Object[]>  selectionTransformation = new BinaryFunction<Object[], GraphExplorer, Object[]>() {\r
-\r
-        @Override\r
-        public Object[] call(GraphExplorer explorer, Object[] objects) {\r
-            Object[] result = new Object[objects.length];\r
-            for (int i = 0; i < objects.length; i++) {\r
-                IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);\r
-                context.setHint(SelectionHints.KEY_MAIN, objects[i]);\r
-                result[i] = context;\r
-            }\r
-            return result;\r
-        }\r
-\r
-    };\r
-    protected FontDescriptor                     originalFont;\r
-    protected ColorDescriptor                    originalForeground;\r
-    protected ColorDescriptor                    originalBackground;\r
-\r
-    /**\r
-     * The set of currently selected TreeItem instances. This set is needed\r
-     * because we need to know in {@link #setData(Event)} whether the updated\r
-     * item was a part of the current selection in which case the selection must\r
-     * be updated.\r
-     */\r
-    private final Map<TreeItem, NodeContext>     selectedItems            = new HashMap<TreeItem, NodeContext>();\r
-\r
-    /**\r
-     * TODO: specify what this is for\r
-     */\r
-    private final Set<NodeContext>               selectionRefreshContexts = new HashSet<NodeContext>();\r
-\r
-    /**\r
-     * If this field is non-null, it means that if {@link #setData(Event)}\r
-     * encounters a NodeContext equal to this one, it must make the TreeItem\r
-     * assigned to that NodeContext the topmost item of the tree using\r
-     * {@link Tree#setTopItem(TreeItem)}. After this the field value is\r
-     * nullified.\r
-     * \r
-     * <p>\r
-     * This is related to {@link #initializeState()}, i.e. explorer state\r
-     * restoration.\r
-     */\r
-//    private NodeContext[] topNodePath = NodeContext.NONE;\r
-//    private int[] topNodePath = {};\r
-//    private int currentTopNodePathIndex = -1;\r
-\r
-    /**\r
-     * See {@link #setAutoExpandLevel(int)}\r
-     */\r
-    private int autoExpandLevel = 0;\r
-\r
-    /**\r
-     * <code>null</code> if not explicitly set through\r
-     * {@link #setServiceLocator(IServiceLocator)}.\r
-     */\r
-    private IServiceLocator serviceLocator;\r
-\r
-    /**\r
-     * The global workbench context service, if the workbench is available.\r
-     * Retrieved in the constructor.\r
-     */\r
-    private IContextService contextService = null;\r
-\r
-    /**\r
-     * The global workbench IFocusService, if the workbench is available.\r
-     * Retrieved in the constructor.\r
-     */\r
-    private IFocusService focusService = null;\r
-\r
-    /**\r
-     * A Workbench UI context activation that is activated when starting inline\r
-     * editing through {@link #startEditing(TreeItem, int)}.\r
-     * \r
-     * @see #activateEditingContext()\r
-     * @see #deactivateEditingContext()\r
-     */\r
-    private IContextActivation editingContext = null;\r
-\r
-    static class ImageTask {\r
-        NodeContext node;\r
-        TreeItem item;\r
-        Object[] descsOrImages;\r
-        public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {\r
-            this.node = node;\r
-            this.item = item;\r
-            this.descsOrImages = descsOrImages;\r
-        }\r
-    }\r
-\r
-    /**\r
-     * The job that is used for off-loading image loading tasks (see\r
-     * {@link ImageTask} to a worker thread from the main UI thread.\r
-     * \r
-     * @see #setPendingImages(IProgressMonitor)\r
-     */\r
-    ImageLoaderJob           imageLoaderJob;\r
-\r
-    /**\r
-     * The set of currently gathered up image loading tasks for\r
-     * {@link #imageLoaderJob} to execute.\r
-     * \r
-     * @see #setPendingImages(IProgressMonitor)\r
-     */\r
-    Map<TreeItem, ImageTask> imageTasks     = new THashMap<TreeItem, ImageTask>();\r
-\r
-    /**\r
-     * A state flag indicating whether the vertical scroll bar was visible for\r
-     * {@link #tree} the last time it was checked. Since there is no listener\r
-     * that can provide this information, we check it in {@link #setData(Event)}\r
-     * every time any data for a TreeItem is updated. If the visibility changes,\r
-     * we will force re-layouting of the tree's parent composite.\r
-     * \r
-     * @see #setData(Event)\r
-     */\r
-    private boolean verticalBarVisible = false;\r
-\r
-    static class TransientStateImpl implements TransientExplorerState {\r
-\r
-       private Integer activeColumn = null;\r
-       \r
-               @Override\r
-               public synchronized Integer getActiveColumn() {\r
-                       return activeColumn;\r
-               }\r
-               \r
-               public synchronized void setActiveColumn(Integer column) {\r
-                       activeColumn = column;\r
-               }\r
-       \r
-    }\r
-    \r
-    private TransientStateImpl transientState = new TransientStateImpl();\r
-    \r
-    boolean scheduleUpdater() {\r
-\r
-       if (tree.isDisposed())\r
-            return false;\r
-\r
-        if (pendingRoot == true || !pendingItems.isEmpty()) {\r
-            assert(!tree.isDisposed());\r
-\r
-            int activity = explorerContext.activityInt;\r
-            long delay = 30;\r
-            if (activity < 100) {\r
-//                System.out.println("Scheduling update immediately.");\r
-            } else if (activity < 1000) {\r
-//                System.out.println("Scheduling update after 500ms.");\r
-                delay = 500;\r
-            } else {\r
-//                System.out.println("Scheduling update after 3000ms.");\r
-                delay = 3000;\r
-            }\r
-\r
-            updateCounter = 0;\r
-            \r
-            //System.out.println("Scheduling UI update after " + delay + " ms.");\r
-            uiUpdateScheduler.schedule(new Runnable() {\r
-                @Override\r
-                public void run() {\r
-                       \r
-                    if (tree.isDisposed())\r
-                        return;\r
-                    \r
-                    if (updateCounter > 0) {\r
-                       updateCounter = 0;\r
-                       uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);\r
-                    } else {\r
-                       tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));\r
-                    }\r
-                    \r
-                }\r
-            }, delay, TimeUnit.MILLISECONDS);\r
-\r
-            updating = true;\r
-            return true;\r
-        }\r
-\r
-        return false;\r
-    }\r
-\r
-    int updateCounter = 0;\r
-    \r
-    void update(TreeItem item) {\r
-\r
-        synchronized(pendingItems) {\r
-               \r
-//             System.out.println("update " + item);\r
-               \r
-               updateCounter++;\r
-\r
-            if(item == null) pendingRoot = true;\r
-            else pendingItems.add(item);\r
-\r
-            if(updating == true) return;\r
-\r
-            scheduleUpdater();\r
-\r
-        }\r
-\r
-    }\r
-\r
-    private int maxChildren = DEFAULT_MAX_CHILDREN;\r
-\r
-    @Override\r
-    public int getMaxChildren() {\r
-        return maxChildren;\r
-    }\r
-\r
-    @Override\r
-    public int getMaxChildren(NodeQueryManager manager, NodeContext context) {\r
-        Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);\r
-        //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);\r
-        if (result != null) {\r
-            if (result < 0)\r
-                throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);\r
-            return result;\r
-        }\r
-        return maxChildren;\r
-    }\r
-\r
-    @Override\r
-    public void setMaxChildren(int maxChildren) {\r
-        this.maxChildren = maxChildren;\r
-    }\r
-\r
-    @Override\r
-    public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {\r
-        this.modificationContext = modificationContext;\r
-    }\r
-\r
-    /**\r
-     * @param parent the parent SWT composite\r
-     */\r
-    public GraphExplorerImpl(Composite parent) {\r
-        this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);\r
-    }\r
-\r
-    /**\r
-     * Stores the node context and the modifier that is currently being\r
-     * modified. These are used internally to prevent duplicate edits from being\r
-     * initiated which should always be a sensible thing to do.\r
-     */\r
-    private Set<NodeContext> currentlyModifiedNodes   = new THashSet<NodeContext>();\r
-\r
-    private final TreeEditor editor;\r
-    private Color            invalidModificationColor = null;\r
-\r
-    /**\r
-     * @param item the TreeItem to start editing\r
-     * @param columnIndex the index of the column to edit, starts counting from\r
-     *        0\r
-     * @return <code>true</code> if the editing was initiated successfully or\r
-     *         <code>false</code> if editing could not be started due to lack of\r
-     *         {@link Modifier} for the labeler in question.\r
-     */\r
-    private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {\r
-        if (!editable)\r
-            return "Rename not supported for selection";\r
-\r
-        GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));\r
-        final NodeContext context = (NodeContext) item.getData();\r
-        Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);\r
-        if (labeler == null)\r
-            return "Rename not supported for selection";\r
-\r
-        if(columnKey == null) columnKey = columns[columnIndex].getKey();\r
-\r
-        // columnKey might be prefixed with '#' to indicate\r
-        // textual editing is preferred. Try to get modifier\r
-        // for that first and only if it fails, try without\r
-        // the '#' prefix.\r
-        Modifier modifier = labeler.getModifier(modificationContext, columnKey);\r
-        if (modifier == null) {\r
-            if(columnKey.startsWith("#"))\r
-                modifier = labeler.getModifier(modificationContext, columnKey.substring(1));\r
-            if (modifier == null)\r
-                return "Rename not supported for selection";\r
-        }\r
-        if (modifier instanceof DeniedModifier) {\r
-               DeniedModifier dm = (DeniedModifier)modifier;\r
-               return dm.getMessage();\r
-        }\r
-\r
-        // Prevent editing of a single node context multiple times.\r
-        if (currentlyModifiedNodes.contains(context)) {\r
-            //System.out.println("discarding duplicate edit for context " + context);\r
-            return "Rename not supported for selection";\r
-        }\r
-\r
-        // Clean up any previous editor control\r
-        Control oldEditor = editor.getEditor();\r
-        if (oldEditor != null)\r
-            oldEditor.dispose();\r
-\r
-        if (modifier instanceof DialogModifier) {\r
-            performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);\r
-        } else if (modifier instanceof CustomModifier) {\r
-            startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);\r
-        } else if (modifier instanceof EnumerationModifier) {\r
-            startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);\r
-        } else {\r
-            startTextEditing(item, columnIndex, context, modifier);\r
-        }\r
-\r
-        return null;\r
-    }\r
-\r
-    /**\r
-     * @param item\r
-     * @param columnIndex\r
-     * @param context\r
-     * @param modifier\r
-     */\r
-    void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,\r
-            final DialogModifier modifier) {\r
-        final AtomicBoolean disposed = new AtomicBoolean(false);\r
-        Consumer<String> callback = result -> {\r
-            if (disposed.get())\r
-                return;\r
-            String error = modifier.isValid(result);\r
-            if (error == null) {\r
-                modifier.modify(result);\r
-                // Item may be disposed if the tree gets reset after a previous editing.\r
-                if (!item.isDisposed()) {\r
-                    item.setText(columnIndex, result);\r
-                    queueSelectionRefresh(context);\r
-                }\r
-            }\r
-        };\r
-\r
-        currentlyModifiedNodes.add(context);\r
-        try {\r
-            String status = modifier.query(tree, item, columnIndex, context, callback);\r
-            if (status != null)\r
-                ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );\r
-        } finally {\r
-            currentlyModifiedNodes.remove(context);\r
-            disposed.set(true);\r
-        }\r
-    }\r
-\r
-    private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {\r
-        Point size = control.computeSize(widthHint, heightHint);\r
-        editor.horizontalAlignment = SWT.LEFT;\r
-        Rectangle itemRect = item.getBounds(columnIndex),\r
-                  rect = tree.getClientArea();\r
-        editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;\r
-        int left = itemRect.x,\r
-            right = rect.x + rect.width;\r
-        editor.minimumWidth = Math.min(editor.minimumWidth, right - left);\r
-        editor.minimumHeight = size.y + insetY * 2;\r
-        editor.layout();\r
-    }\r
-\r
-    void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {\r
-        GC gc = new GC(control);\r
-        Point size = gc.textExtent(text);\r
-        gc.dispose();\r
-        reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);\r
-    }\r
-\r
-    /**\r
-     * @param item\r
-     * @param columnIndex\r
-     * @param context\r
-     * @param modifier\r
-     */\r
-    void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,\r
-            final CustomModifier modifier) {\r
-        final Object obj = modifier.createControl(tree, item, columnIndex, context);\r
-        if (!(obj instanceof Control))\r
-            throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");\r
-        final Control control = (Control) obj;\r
-\r
-//        final int insetX = 0;\r
-//        final int insetY = 0;\r
-//        control.addListener(SWT.Resize, new Listener() {\r
-//            @Override\r
-//            public void handleEvent(Event e) {\r
-//                Rectangle rect = control.getBounds();\r
-//                control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);\r
-//            }\r
-//        });\r
-        control.addListener(SWT.Dispose, new Listener() {\r
-            @Override\r
-            public void handleEvent(Event event) {\r
-                currentlyModifiedNodes.remove(context);\r
-                queueSelectionRefresh(context);\r
-                deactivateEditingContext();\r
-            }\r
-        });\r
-        \r
-        if (!(control instanceof Shell)) {\r
-            editor.setEditor(control, item, columnIndex);\r
-        }\r
-        \r
-\r
-        control.setFocus();\r
-\r
-        GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);\r
-\r
-        activateEditingContext(control);\r
-\r
-        // Removed in disposeListener above\r
-        currentlyModifiedNodes.add(context);\r
-        //System.out.println("START CUSTOM EDITING: " + item);\r
-    }\r
-\r
-    /**\r
-     * @param item\r
-     * @param columnIndex\r
-     * @param context\r
-     * @param modifier\r
-     */\r
-    void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {\r
-        String initialText = modifier.getValue();\r
-        if (initialText == null)\r
-            throw new AssertionError("Labeler.Modifier.getValue() returned null");\r
-\r
-        List<String> values = modifier.getValues();\r
-        String selectedValue = modifier.getValue();\r
-        int selectedIndex = values.indexOf(selectedValue);\r
-        if (selectedIndex == -1)\r
-            throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);\r
-\r
-        final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);\r
-        combo.setVisibleItemCount(10);\r
-        //combo.setEditable(false);\r
-\r
-        for (String value : values) {\r
-            combo.add(value);\r
-        }\r
-        combo.select(selectedIndex);\r
-\r
-        Listener comboListener = new Listener() {\r
-            boolean arrowTraverseUsed = false; \r
-            @Override\r
-            public void handleEvent(final Event e) {\r
-                //System.out.println("FOO: " + e);\r
-                switch (e.type) {\r
-                    case SWT.KeyDown:\r
-                        if (e.character == SWT.CR) {\r
-                            // Commit edit directly on ENTER press.\r
-                            String text = combo.getText();\r
-                            modifier.modify(text);\r
-                            // Item may be disposed if the tree gets reset after a previous editing.\r
-                            if (!item.isDisposed()) {\r
-                                item.setText(columnIndex, text);\r
-                                queueSelectionRefresh(context);\r
-                            }\r
-                            combo.dispose();\r
-                            e.doit = false;\r
-                        } else if (e.keyCode == SWT.ESC) {\r
-                            // Cancel editing immediately\r
-                            combo.dispose();\r
-                            e.doit = false;\r
-                        }\r
-                        break;\r
-                    case SWT.Selection:\r
-                    {\r
-                        if (arrowTraverseUsed) {\r
-                            arrowTraverseUsed = false;\r
-                            return;\r
-                        }\r
-\r
-                        String text = combo.getText();\r
-                        modifier.modify(text);\r
-\r
-                        // Item may be disposed if the tree gets reset after a previous editing.\r
-                        if (!item.isDisposed()) {\r
-                            item.setText(columnIndex, text);\r
-                            queueSelectionRefresh(context);\r
-                        }\r
-                        combo.dispose();\r
-                        break;\r
-                    }\r
-                    case SWT.FocusOut: {\r
-                        String text = combo.getText();\r
-                        modifier.modify(text);\r
-\r
-                        // Item may be disposed if the tree gets reset after a previous editing.\r
-                        if (!item.isDisposed()) {\r
-                            item.setText(columnIndex, text);\r
-                            queueSelectionRefresh(context);\r
-                        }\r
-                        combo.dispose();\r
-                        break;\r
-                    }\r
-                    case SWT.Traverse: {\r
-                        switch (e.detail) {\r
-                            case SWT.TRAVERSE_RETURN:\r
-                                String text = combo.getText();\r
-                                modifier.modify(text);\r
-                                if (!item.isDisposed()) {\r
-                                    item.setText(columnIndex, text);\r
-                                    queueSelectionRefresh(context);\r
-                                }\r
-                                arrowTraverseUsed = false;\r
-                                // FALL THROUGH\r
-                            case SWT.TRAVERSE_ESCAPE:\r
-                                combo.dispose();\r
-                                e.doit = false;\r
-                                break;\r
-                            case SWT.TRAVERSE_ARROW_NEXT:\r
-                            case SWT.TRAVERSE_ARROW_PREVIOUS:\r
-                                arrowTraverseUsed = true;\r
-                                break;\r
-                            default:\r
-                                //System.out.println("unhandled traversal: " + e.detail);\r
-                                break;\r
-                        }\r
-                        break;\r
-                    }\r
-                    case SWT.Dispose:\r
-                        currentlyModifiedNodes.remove(context);\r
-                        deactivateEditingContext();\r
-                        break;\r
-                }\r
-            }\r
-        };\r
-        combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);\r
-        combo.addListener(SWT.KeyDown, comboListener);\r
-        combo.addListener(SWT.FocusOut, comboListener);\r
-        combo.addListener(SWT.Traverse, comboListener);\r
-        combo.addListener(SWT.Selection, comboListener);\r
-        combo.addListener(SWT.Dispose, comboListener);\r
-\r
-        editor.setEditor(combo, item, columnIndex);\r
-\r
-        combo.setFocus();\r
-        combo.setListVisible(true);\r
-\r
-        GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);\r
-\r
-        activateEditingContext(combo);\r
-\r
-        // Removed in comboListener\r
-        currentlyModifiedNodes.add(context);\r
-\r
-        //System.out.println("START ENUMERATION EDITING: " + item);\r
-    }\r
-\r
-    /**\r
-     * @param item\r
-     * @param columnIndex\r
-     * @param context\r
-     * @param modifier\r
-     */\r
-    void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {\r
-        String initialText = modifier.getValue();\r
-        if (initialText == null)\r
-            throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);\r
-\r
-        final Composite composite = new Composite(tree, SWT.NONE);\r
-        //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));\r
-        final Text text = new Text(composite, SWT.BORDER);\r
-        final int insetX = 0;\r
-        final int insetY = 0;\r
-        composite.addListener(SWT.Resize, new Listener() {\r
-            @Override\r
-            public void handleEvent(Event e) {\r
-                Rectangle rect = composite.getClientArea();\r
-                text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height\r
-                        - insetY * 2);\r
-            }\r
-        });\r
-        final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;\r
-        Listener textListener = new Listener() {\r
-               \r
-               boolean modified = false;\r
-               \r
-            @Override\r
-            public void handleEvent(final Event e) {\r
-                String error;\r
-                String newText;\r
-                switch (e.type) {\r
-                    case SWT.FocusOut:\r
-                       if(modified) {\r
-                               //System.out.println("FOCUS OUT " + item);\r
-                               newText = text.getText();\r
-                               error = modifier.isValid(newText);\r
-                               if (error == null) {\r
-                                       modifier.modify(newText);\r
-\r
-                                       // Item may be disposed if the tree gets reset after a previous editing.\r
-                                       if (!item.isDisposed()) {\r
-                                               item.setText(columnIndex, newText);\r
-                                               queueSelectionRefresh(context);\r
-                                       }\r
-                               } else {\r
-                                       //                                System.out.println("validation error: " + error);\r
-                               }\r
-                       }\r
-                        composite.dispose();\r
-                        break;\r
-                    case SWT.Modify:\r
-                        newText = text.getText();\r
-                        error = modifier.isValid(newText);\r
-                        if (error != null) {\r
-                            text.setBackground(invalidModificationColor);\r
-                            errorStatus(error);\r
-                            //System.out.println("validation error: " + error);\r
-                        } else {\r
-                            text.setBackground(null);\r
-                            errorStatus(null);\r
-                        }\r
-                        modified = true;\r
-                        break;\r
-                    case SWT.Verify:\r
-                       \r
-                        // Safety check since it seems that this may happen with\r
-                        // virtual trees.\r
-                        if (item.isDisposed())\r
-                            return;\r
-\r
-                        // Filter input if necessary\r
-                        e.text = filter != null ? filter.filter(e.text) : e.text;\r
-\r
-                        newText = text.getText();\r
-                        String leftText = newText.substring(0, e.start);\r
-                        String rightText = newText.substring(e.end, newText.length());\r
-                        GraphExplorerImpl.this.reconfigureTreeEditorForText(\r
-                                item, columnIndex, text, leftText + e.text + rightText,\r
-                                SWT.DEFAULT, insetX, insetY);\r
-                        break;\r
-                    case SWT.Traverse:\r
-                        switch (e.detail) {\r
-                            case SWT.TRAVERSE_RETURN:\r
-                               if(modified) {\r
-                                       newText = text.getText();\r
-                                       error = modifier.isValid(newText);\r
-                                       if (error == null) {\r
-                                               modifier.modify(newText);\r
-                                               if (!item.isDisposed()) {\r
-                                                       item.setText(columnIndex, newText);\r
-                                                       queueSelectionRefresh(context);\r
-                                               }\r
-                                       }\r
-                               }\r
-                                // FALL THROUGH\r
-                            case SWT.TRAVERSE_ESCAPE:\r
-                                composite.dispose();\r
-                                e.doit = false;\r
-                                break;\r
-                            default:\r
-                                //System.out.println("unhandled traversal: " + e.detail);\r
-                                break;\r
-                        }\r
-                        break;\r
-\r
-                    case SWT.Dispose:\r
-                        currentlyModifiedNodes.remove(context);\r
-                        deactivateEditingContext();\r
-                        errorStatus(null);\r
-                        break;\r
-                }\r
-            }\r
-        };\r
-        // Set the initial text before registering a listener. We do not want immediate modification!\r
-        text.setText(initialText);\r
-        text.addListener(SWT.FocusOut, textListener);\r
-        text.addListener(SWT.Traverse, textListener);\r
-        text.addListener(SWT.Verify, textListener);\r
-        text.addListener(SWT.Modify, textListener);\r
-        text.addListener(SWT.Dispose, textListener);\r
-        editor.setEditor(composite, item, columnIndex);\r
-        text.selectAll();\r
-        text.setFocus();\r
-\r
-        // Initialize TreeEditor properly.\r
-        GraphExplorerImpl.this.reconfigureTreeEditorForText(\r
-                item, columnIndex, text, initialText,\r
-                SWT.DEFAULT, insetX, insetY);\r
-\r
-        // Removed in textListener\r
-        currentlyModifiedNodes.add(context);\r
-\r
-        activateEditingContext(text);\r
-\r
-        //System.out.println("START TEXT EDITING: " + item);\r
-    }\r
-\r
-    protected void errorStatus(String error) {\r
-        IStatusLineManager status = getStatusLineManager();\r
-        if (status != null) {\r
-            status.setErrorMessage(error);\r
-        }\r
-    }\r
-\r
-    protected IStatusLineManager getStatusLineManager() {\r
-        if (serviceLocator instanceof IWorkbenchPart) {\r
-            return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);\r
-        } else if (serviceLocator instanceof IWorkbenchSite) {\r
-            return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);\r
-        }\r
-        return null;\r
-    }\r
-\r
-    protected void activateEditingContext(Control control) {\r
-        if (contextService != null) {\r
-            editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);\r
-        }\r
-        if (control != null && focusService != null) {\r
-            focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);\r
-            // No need to remove the control, it will be\r
-            // removed automatically when it is disposed.\r
-        }\r
-    }\r
-\r
-    protected void deactivateEditingContext() {\r
-        IContextActivation a = editingContext;\r
-        if (a != null) {\r
-            editingContext = null;\r
-            contextService.deactivateContext(a);\r
-        }\r
-    }\r
-\r
-    /**\r
-     * @param forContext\r
-     */\r
-    void queueSelectionRefresh(NodeContext forContext) {\r
-        selectionRefreshContexts.add(forContext);\r
-    }\r
-\r
-    @Override\r
-    public String startEditing(NodeContext context, String columnKey_) {\r
-        assertNotDisposed();\r
-        if (!thread.currentThreadAccess())\r
-            throw new IllegalStateException("not in SWT display thread " + thread.getThread());\r
-\r
-        String columnKey = columnKey_;\r
-        if(columnKey.startsWith("#")) {\r
-               columnKey = columnKey.substring(1);\r
-        }\r
-        \r
-        Integer columnIndex = columnKeyToIndex.get(columnKey);\r
-        if (columnIndex == null)\r
-            return "Rename not supported for selection";\r
-\r
-        TreeItem item = contextToItem.getRight(context);\r
-        if (item == null)\r
-            return "Rename not supported for selection";\r
-\r
-        return startEditing(item, columnIndex, columnKey_);\r
-        \r
-    }\r
-\r
-    @Override\r
-    public String startEditing(String columnKey) {\r
-\r
-        ISelection selection = postSelectionProvider.getSelection();\r
-        if(selection == null) return "Rename not supported for selection";\r
-        NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);\r
-        if(context == null) return "Rename not supported for selection";\r
-\r
-        return startEditing(context, columnKey);\r
-\r
-    }\r
-\r
-    /**\r
-     * @param site <code>null</code> if the explorer is detached from the workbench\r
-     * @param parent the parent SWT composite\r
-     * @param style the tree style to use, check the see tags for the available flags\r
-     * \r
-     * @see SWT#SINGLE\r
-     * @see SWT#MULTI\r
-     * @see SWT#CHECK\r
-     * @see SWT#FULL_SELECTION\r
-     * @see SWT#NO_SCROLL\r
-     * @see SWT#H_SCROLL\r
-     * @see SWT#V_SCROLL\r
-     */\r
-    public GraphExplorerImpl(Composite parent, int style) {\r
-\r
-        setServiceLocator(null);\r
-\r
-        this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());\r
-        this.resourceManager = new DeviceResourceManager(parent.getDisplay());\r
-\r
-        this.imageLoaderJob = new ImageLoaderJob(this);\r
-        this.imageLoaderJob.setPriority(Job.DECORATE);\r
-\r
-        invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );\r
-\r
-        this.thread = SWTThread.getThreadAccess(parent);\r
-\r
-        for(int i=0;i<10;i++) explorerContext.activity.push(0);\r
-\r
-        tree = new Tree(parent, style);\r
-        tree.addListener(SWT.SetData, this);\r
-        tree.addListener(SWT.Expand, this);\r
-        tree.addListener(SWT.Dispose, this);\r
-        tree.addListener(SWT.Activate, this);\r
-\r
-        tree.setData(KEY_GRAPH_EXPLORER, this);\r
-\r
-        // These are both required for performing column resizing without flicker.\r
-        // See SWT.Resize event handling in #handleEvent() for more explanations.\r
-        parent.addListener(SWT.Resize, this);\r
-        tree.addListener(SWT.Resize, this);\r
-\r
-        originalFont = JFaceResources.getDefaultFontDescriptor();\r
-//        originalBackground = JFaceResources.getColorRegistry().get(symbolicName);\r
-//        originalForeground = tree.getForeground();\r
-\r
-        tree.setFont((Font) localResourceManager.get(originalFont));\r
-\r
-        columns = new Column[] { new Column(ColumnKeys.SINGLE) };\r
-        columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);\r
-\r
-        editor = new TreeEditor(tree);\r
-        editor.horizontalAlignment = SWT.LEFT;\r
-        editor.grabHorizontal = true;\r
-        editor.minimumWidth = 50;\r
-\r
-        setBasicListeners();\r
-        setDefaultProcessors();\r
-        \r
-        this.toolTip = new GraphExplorerToolTip(explorerContext, tree);\r
-    }\r
-\r
-    @Override\r
-    public IThreadWorkQueue getThread() {\r
-        return thread;\r
-    }\r
-\r
-    TreeItem previousSingleSelection = null;\r
-    long focusGainedAt = Long.MIN_VALUE;\r
-\r
-    protected GraphExplorerToolTip toolTip;\r
-\r
-    protected void setBasicListeners() {\r
-        // Keep track of the previous single selection to help\r
-        // decide whether to start editing a tree node on mouse\r
-        // downs or not.\r
-        tree.addListener(SWT.Selection, new Listener() {\r
-            @Override\r
-            public void handleEvent(Event event) {\r
-                TreeItem[] selection = tree.getSelection();\r
-                if (selection.length == 1) {\r
-                    //for (TreeItem item : selection)\r
-                    //    System.out.println("selection: " + item);\r
-                    previousSingleSelection = selection[0];\r
-                } else {\r
-                    previousSingleSelection = null;\r
-                }\r
-            }\r
-        });\r
-\r
-        // Try to start editing of tree column when clicked for the second time.\r
-        Listener mouseEditListener = new Listener() {\r
-\r
-            Future<?> startEdit = null;\r
-\r
-            @Override\r
-            public void handleEvent(Event event) {\r
-                if (event.type == SWT.DragDetect) {\r
-                    // Needed to prevent editing from being started when in fact\r
-                    // the user starts dragging an item.\r
-                    //System.out.println("DRAG DETECT: " + event);\r
-                    cancelEdit();\r
-                    return;\r
-                }\r
-                //System.out.println("mouse down: " + event);\r
-                if (event.button == 1) {\r
-                    // Always ignore the first mouse button press that focuses\r
-                    // the control. Do not let it start in-line editing since\r
-                    // that is very annoying to users and not how the UI's that\r
-                    // people are used to behave.\r
-                    long eventTime = ((long) event.time) & 0xFFFFFFFFL;\r
-                    if ((eventTime - focusGainedAt) < 250L) {\r
-                        //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));\r
-                        return;\r
-                    }\r
-                    //System.out.println("testing whether to start editing");\r
-\r
-                    final Point point = new Point(event.x, event.y);\r
-                    final TreeItem item = tree.getItem(point);\r
-                    if (item == null)\r
-                        return;\r
-                    //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);\r
-\r
-                    // Only start editing if the item was already selected.\r
-                    if (!item.equals(previousSingleSelection)) {\r
-                        cancelEdit();\r
-                        return;\r
-                    }\r
-\r
-                    if (tree.getColumnCount() > 1) {\r
-                        // TODO: reconsider this logic, might not be good in general.\r
-                        for (int i = 0; i < tree.getColumnCount(); i++) {\r
-                            if (item.getBounds(i).contains(point)) {\r
-                                tryScheduleEdit(event, item, point, 100, i);\r
-                                return;\r
-                            }\r
-                        }\r
-                    } else {\r
-                        //System.out.println("clicks: " + event.count);\r
-                        if (item.getBounds().contains(point)) {\r
-                            if (event.count == 1) {\r
-                                tryScheduleEdit(event, item, point, 500, 0);\r
-                            } else {\r
-                                cancelEdit();\r
-                            }\r
-                        }\r
-                    }\r
-                }\r
-            }\r
-\r
-            void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {\r
-                //System.out.println("\tCONTAINS: " + item);\r
-                if (!cancelEdit())\r
-                    return;\r
-\r
-                //System.out.println("\tScheduling edit: " + item);\r
-                startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {\r
-                    @Override\r
-                    public void run() {\r
-                        ThreadUtils.asyncExec(thread, new Runnable() {\r
-                            @Override\r
-                            public void run() {\r
-                                if (item.isDisposed())\r
-                                    return;\r
-                                startEditing(item, column, null);\r
-                            }\r
-                        });\r
-                    }\r
-                }, delayMs, TimeUnit.MILLISECONDS);\r
-            }\r
-\r
-            boolean cancelEdit() {\r
-                Future<?> f = startEdit;\r
-                if (f != null) {\r
-                    // Try to cancel the start edit task if it's not running yet.\r
-                    startEdit = null;\r
-                    if (!f.isDone()) {\r
-                        boolean ret = f.cancel(false);\r
-                        //System.out.println("\tCancelled edit: " + ret);\r
-                        return ret;\r
-                    }\r
-                }\r
-                //System.out.println("\tNo edit in progress to cancel");\r
-                return true;\r
-            }\r
-        };\r
-        tree.addListener(SWT.MouseDown, mouseEditListener);\r
-        tree.addListener(SWT.DragDetect, mouseEditListener);\r
-        tree.addListener(SWT.DragDetect, new Listener() {\r
-            @Override\r
-            public void handleEvent(Event event) {\r
-                Point test = new Point(event.x, event.y);\r
-                TreeItem item = tree.getItem(test);\r
-                if(item != null) {\r
-                    for(int i=0;i<tree.getColumnCount();i++) {\r
-                        Rectangle rect = item.getBounds(i);\r
-                        if(rect.contains(test)) {\r
-                            tree.setData(KEY_DRAG_COLUMN, i);\r
-                            return;\r
-                        }\r
-                    }\r
-                }\r
-                tree.setData(KEY_DRAG_COLUMN, -1);\r
-            }\r
-        });\r
-        tree.addListener(SWT.MouseMove, new Listener() {\r
-            @Override\r
-            public void handleEvent(Event event) {\r
-                Point test = new Point(event.x, event.y);\r
-                TreeItem item = tree.getItem(test);\r
-                if(item != null) {\r
-                    for(int i=0;i<tree.getColumnCount();i++) {\r
-                        Rectangle rect = item.getBounds(i);\r
-                        if(rect.contains(test)) {\r
-                               transientState.setActiveColumn(i);\r
-                            return;\r
-                        }\r
-                    }\r
-                }\r
-               transientState.setActiveColumn(null);\r
-            }\r
-        });\r
-\r
-        // Add focus/mouse/key listeners for supporting the respective\r
-        // add/remove listener methods in IGraphExplorer.\r
-        tree.addFocusListener(new FocusListener() {\r
-            @Override\r
-            public void focusGained(FocusEvent e) {\r
-                focusGainedAt = ((long) e.time) & 0xFFFFFFFFL;\r
-                for (FocusListener listener : focusListeners)\r
-                    listener.focusGained(e);\r
-            }\r
-            @Override\r
-            public void focusLost(FocusEvent e) {\r
-                for (FocusListener listener : focusListeners)\r
-                    listener.focusLost(e);\r
-            }\r
-        });\r
-        tree.addMouseListener(new MouseListener() {\r
-            @Override\r
-            public void mouseDoubleClick(MouseEvent e) {\r
-                for (MouseListener listener : mouseListeners) {\r
-                    listener.mouseDoubleClick(e);\r
-                }\r
-            }\r
-            @Override\r
-            public void mouseDown(MouseEvent e) {\r
-                for (MouseListener listener : mouseListeners) {\r
-                    listener.mouseDown(e);\r
-                }\r
-            }\r
-            @Override\r
-            public void mouseUp(MouseEvent e) {\r
-                for (MouseListener listener : mouseListeners) {\r
-                    listener.mouseUp(e);\r
-                }\r
-            }\r
-        });\r
-        tree.addKeyListener(new KeyListener() {\r
-            @Override\r
-            public void keyPressed(KeyEvent e) {\r
-                for (KeyListener listener : keyListeners) {\r
-                    listener.keyPressed(e);\r
-                }\r
-            }\r
-            @Override\r
-            public void keyReleased(KeyEvent e) {\r
-                for (KeyListener listener : keyListeners) {\r
-                    listener.keyReleased(e);\r
-                }\r
-            }\r
-        });\r
-\r
-               // Add a tree selection listener for keeping the selection of\r
-               // GraphExplorer's ISelectionProvider up-to-date.\r
-        tree.addSelectionListener(new SelectionListener() {\r
-            @Override\r
-            public void widgetDefaultSelected(SelectionEvent e) {\r
-                widgetSelected(e);\r
-            }\r
-            @Override\r
-            public void widgetSelected(SelectionEvent e) {\r
-               widgetSelectionChanged(false);\r
-            }\r
-        });\r
-\r
-        // This listener takes care of updating the set of currently selected\r
-        // TreeItem instances. This set is needed because we need to know in\r
-        // #setData(Event) whether the updated item was a part of the current\r
-        // selection in which case the selection must be updated.\r
-        selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {\r
-            @Override\r
-            public void selectionChanged(SelectionChangedEvent event) {\r
-                //System.out.println("selection changed: " + event.getSelection());\r
-                Set<NodeContext> set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);\r
-                selectedItems.clear();\r
-                for (NodeContext nc : set) {\r
-                    TreeItem item = contextToItem.getRight(nc);\r
-                    if (item != null)\r
-                        selectedItems.put(item, nc);\r
-                }\r
-                //System.out.println("newly selected items: " + selectedItems);\r
-            }\r
-        });\r
-    }\r
-\r
-    /**\r
-     * Mod count for delaying post selection changed events.\r
-     */\r
-    int postSelectionModCount = 0;\r
-\r
-    /**\r
-     * Last tree selection modification time for implementing a quiet\r
-     * time for selection changes.\r
-     */\r
-    long lastSelectionModTime = System.currentTimeMillis() - 10000;\r
-\r
-    /**\r
-     * Current target time for the selection to be set. Calculated\r
-     * according to the set quiet time and last selection modification\r
-     * time.\r
-     */\r
-    long selectionSetTargetTime = 0;\r
-\r
-    /**\r
-     * <code>true</code> if delayed selection runnable is current scheduled or\r
-     * running.\r
-     */\r
-    boolean delayedSelectionScheduled = false;\r
-\r
-    Runnable SELECTION_DELAY = new Runnable() {\r
-        @Override\r
-        public void run() {\r
-            if (tree.isDisposed())\r
-                return;\r
-            long now = System.currentTimeMillis();\r
-            long waitTimeLeft = selectionSetTargetTime - now;\r
-            if (waitTimeLeft > 0) {\r
-                // Not enough quiet time, reschedule.\r
-                delayedSelectionScheduled = true;\r
-                tree.getDisplay().timerExec((int) waitTimeLeft, this);\r
-            } else {\r
-                // Time to perform selection, stop rescheduling.\r
-                delayedSelectionScheduled = false;\r
-                resetSelection();\r
-            }\r
-        }\r
-    };\r
-\r
-    private void widgetSelectionChanged(boolean forceSelectionChange) {\r
-        long modTime = System.currentTimeMillis();\r
-        long delta = modTime - lastSelectionModTime;\r
-        lastSelectionModTime = modTime;\r
-        if (!forceSelectionChange && delta < SELECTION_CHANGE_QUIET_TIME) {\r
-            long msToWait = SELECTION_CHANGE_QUIET_TIME - delta;\r
-            selectionSetTargetTime = modTime + msToWait;\r
-            if (!delayedSelectionScheduled) {\r
-                delayedSelectionScheduled = true;\r
-                tree.getDisplay().timerExec((int) msToWait, SELECTION_DELAY);\r
-            }\r
-            // Make sure that post selection change events do not fire.\r
-            ++postSelectionModCount;\r
-            return;\r
-        }\r
-\r
-        // Immediate selection reconstruction.\r
-        resetSelection();\r
-    }\r
-\r
-    private void resetSelection() {\r
-        final ISelection selection = getWidgetSelection();\r
-\r
-        //System.out.println("resetSelection(" + postSelectionModCount + ")");\r
-        //System.out.println("    provider selection: " + selectionProvider.getSelection());\r
-        //System.out.println("    widget selection:   " + selection);\r
-\r
-        selectionProvider.setAndFireNonEqualSelection(selection);\r
-\r
-        // Implement deferred firing of post selection events\r
-        final int count = ++postSelectionModCount;\r
-        //System.out.println("[" + System.currentTimeMillis() + "] scheduling postSelectionChanged " + count + ": " + selection);\r
-        ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {\r
-            @Override\r
-            public void run() {\r
-                int newCount = postSelectionModCount;\r
-                // Don't publish selection yet, there's another change incoming.\r
-                //System.out.println("[" + System.currentTimeMillis() + "] checking post selection publish: " + count + " vs. " + newCount + ": " + selection);\r
-                if (newCount != count)\r
-                    return;\r
-                //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);\r
-\r
-                if (tree.isDisposed())\r
-                    return;\r
-\r
-                //System.out.println("scheduling fire post selection changed: " + selection);\r
-                tree.getDisplay().asyncExec(new Runnable() {\r
-                    @Override\r
-                    public void run() {\r
-                        if (tree.isDisposed() || selectionProvider == null)\r
-                            return;\r
-                        //System.out.println("firing post selection changed: " + selection);\r
-                        selectionProvider.firePostSelection(selection);\r
-                    }\r
-                });\r
-            }\r
-        }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);\r
-    }\r
-    \r
-    protected void setDefaultProcessors() {\r
-        // Add a simple IMAGER query processor that always returns null.\r
-        // With this processor no images will ever be shown.\r
-//        setPrimitiveProcessor(new StaticImagerProcessor(null));\r
-\r
-        setProcessor(new DefaultComparableChildrenProcessor());\r
-        setProcessor(new DefaultLabelDecoratorsProcessor());\r
-        setProcessor(new DefaultImageDecoratorsProcessor());\r
-        setProcessor(new DefaultSelectedLabelerProcessor());\r
-        setProcessor(new DefaultLabelerFactoriesProcessor());\r
-        setProcessor(new DefaultSelectedImagerProcessor());\r
-        setProcessor(new DefaultImagerFactoriesProcessor());\r
-        setPrimitiveProcessor(new DefaultLabelerProcessor());\r
-        setPrimitiveProcessor(new DefaultCheckedStateProcessor());\r
-        setPrimitiveProcessor(new DefaultImagerProcessor());\r
-        setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());\r
-        setPrimitiveProcessor(new DefaultImageDecoratorProcessor());\r
-        setPrimitiveProcessor(new NoSelectionRequestProcessor());\r
-\r
-        setProcessor(new DefaultFinalChildrenProcessor(this));\r
-\r
-        setProcessor(new DefaultPrunedChildrenProcessor());\r
-        setProcessor(new DefaultSelectedViewpointProcessor());\r
-        setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());\r
-        setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());\r
-        setProcessor(new DefaultViewpointContributionsProcessor());\r
-\r
-        setPrimitiveProcessor(new DefaultViewpointProcessor());\r
-        setPrimitiveProcessor(new DefaultViewpointContributionProcessor());\r
-        setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());\r
-        setPrimitiveProcessor(new DefaultIsExpandedProcessor());\r
-        setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());\r
-    }\r
-\r
-    @Override\r
-    public <T> void setProcessor(NodeQueryProcessor<T> processor) {\r
-        assertNotDisposed();\r
-        if (processor == null)\r
-            throw new IllegalArgumentException("null processor");\r
-\r
-        processors.put(processor.getIdentifier(), processor);\r
-    }\r
-\r
-    @Override\r
-    public <T> void setPrimitiveProcessor(PrimitiveQueryProcessor<T> processor) {\r
-        assertNotDisposed();\r
-        if (processor == null)\r
-            throw new IllegalArgumentException("null processor");\r
-\r
-        PrimitiveQueryProcessor<?> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);\r
-\r
-        if (oldProcessor instanceof ProcessorLifecycle)\r
-            ((ProcessorLifecycle) oldProcessor).detached(this);\r
-        if (processor instanceof ProcessorLifecycle)\r
-            ((ProcessorLifecycle) processor).attached(this);\r
-    }\r
-\r
-    @Override\r
-    public <T> void setDataSource(DataSource<T> provider) {\r
-        assertNotDisposed();\r
-        if (provider == null)\r
-            throw new IllegalArgumentException("null provider");\r
-        dataSources.put(provider.getProvidedClass(), provider);\r
-    }\r
-\r
-    @SuppressWarnings("unchecked")\r
-    @Override\r
-    public <T> DataSource<T> removeDataSource(Class<T> forProvidedClass) {\r
-        assertNotDisposed();\r
-        if (forProvidedClass == null)\r
-            throw new IllegalArgumentException("null class");\r
-        return dataSources.remove(forProvidedClass);\r
-    }\r
-\r
-    @Override\r
-    public void setPersistor(StatePersistor persistor) {\r
-       this.persistor = persistor;\r
-    }\r
-    \r
-    @Override\r
-    public SelectionDataResolver getSelectionDataResolver() {\r
-        return selectionDataResolver;\r
-    }\r
-\r
-    @Override\r
-    public void setSelectionDataResolver(SelectionDataResolver r) {\r
-        this.selectionDataResolver = r;\r
-    }\r
-\r
-    @Override\r
-    public SelectionFilter getSelectionFilter() {\r
-        return selectionFilter;\r
-    }\r
-\r
-    @Override\r
-    public void setSelectionFilter(SelectionFilter f) {\r
-        this.selectionFilter = f;\r
-        // TODO: re-filter current selection?\r
-    }\r
-\r
-    @Override\r
-    public void setSelectionTransformation(BinaryFunction<Object[], GraphExplorer, Object[]> f) {\r
-        this.selectionTransformation = f;\r
-    }\r
-\r
-    @Override\r
-    public <T> void addListener(T listener) {\r
-        if(listener instanceof FocusListener) {\r
-            focusListeners.add((FocusListener)listener);\r
-        } else if(listener instanceof MouseListener) {\r
-            mouseListeners.add((MouseListener)listener);\r
-        } else if(listener instanceof KeyListener) {\r
-            keyListeners.add((KeyListener)listener);\r
-        }\r
-    }\r
-\r
-    @Override\r
-    public <T> void removeListener(T listener) {\r
-        if(listener instanceof FocusListener) {\r
-            focusListeners.remove(listener);\r
-        } else if(listener instanceof MouseListener) {\r
-            mouseListeners.remove(listener);\r
-        } else if(listener instanceof KeyListener) {\r
-            keyListeners.remove(listener);\r
-        }\r
-    }\r
-\r
-    public void addSelectionListener(SelectionListener listener) {\r
-        tree.addSelectionListener(listener);\r
-    }\r
-\r
-    public void removeSelectionListener(SelectionListener listener) {\r
-        tree.removeSelectionListener(listener);\r
-    }\r
-\r
-    private Set<String> uiContexts;\r
-    \r
-    @Override\r
-    public void setUIContexts(Set<String> contexts) {\r
-       this.uiContexts = contexts;\r
-    }\r
-    \r
-    @Override\r
-    public void setRoot(final Object root) {\r
-       if(uiContexts != null && uiContexts.size() == 1)\r
-               setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));\r
-       else\r
-               setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));\r
-    }\r
-\r
-    @Override\r
-    public void setRootContext(final NodeContext context) {\r
-        setRootContext0(context);\r
-    }\r
-\r
-    private void setRootContext0(final NodeContext context) {\r
-        Assert.isNotNull(context, "root must not be null");\r
-        if (isDisposed() || tree.isDisposed())\r
-            return;\r
-        Display display = tree.getDisplay();\r
-        if (display.getThread() == Thread.currentThread()) {\r
-            doSetRoot(context);\r
-        } else {\r
-            display.asyncExec(new Runnable() {\r
-                @Override\r
-                public void run() {\r
-                    doSetRoot(context);\r
-                }\r
-            });\r
-        }\r
-    }\r
-    \r
-    private void initializeState() {\r
-        if (persistor == null)\r
-            return;\r
-\r
-        ExplorerState state = persistor.deserialize(\r
-                Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),\r
-                getRoot());\r
-\r
-        // topNodeToSet will be processed by #setData when it encounters a\r
-        // NodeContext that matches this one.\r
-//        topNodePath = state.topNodePath;\r
-//        topNodePathChildIndex = state.topNodePathChildIndex;\r
-//        currentTopNodePathIndex = 0;\r
-\r
-        Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);\r
-        if (processor instanceof DefaultIsExpandedProcessor) {\r
-            DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;\r
-            for(NodeContext expanded : state.expandedNodes) {\r
-                isExpandedProcessor.setExpanded(expanded, true);\r
-            }\r
-        }\r
-    }\r
-\r
-    private void saveState() {\r
-        if (persistor == null)\r
-            return;\r
-\r
-        NodeContext[] topNodePath = NodeContext.NONE;\r
-        int[] topNodePathChildIndex = {};\r
-        Collection<NodeContext> expandedNodes = Collections.emptyList();\r
-        Map<String, Integer> columnWidths = Collections.<String, Integer> emptyMap();\r
-\r
-        // Resolve top node path\r
-        TreeItem topItem = tree.getTopItem();\r
-        if (topItem != null) {\r
-            NodeContext topNode = (NodeContext) topItem.getData();\r
-            if (topNode != null) {\r
-                topNodePath = getNodeContextPathSegments(topNode);\r
-                topNodePathChildIndex = new int[topNodePath.length];\r
-                for (int i = 0; i < topNodePath.length; ++i) {\r
-                    // TODO: get child indexes\r
-                    topNodePathChildIndex[i] = 0;\r
-                }\r
-            }\r
-        }\r
-        \r
-        // Resolve expanded nodes\r
-        Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);\r
-        if (processor instanceof IsExpandedProcessor) {\r
-            IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;\r
-            expandedNodes = isExpandedProcessor.getExpanded();\r
-        }\r
-\r
-        // Column widths\r
-        TreeColumn[] columns = tree.getColumns();\r
-        if (columns.length > 1) {\r
-                   columnWidths = new HashMap<String, Integer>();\r
-                   for (int i = 0; i < columns.length; ++i) {\r
-                       columnWidths.put(columns[i].getText(), columns[i].getWidth());\r
-                   }\r
-        }\r
-                   \r
-        persistor.serialize(\r
-                Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),\r
-                getRoot(),\r
-                new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));\r
-    }\r
-\r
-    /**\r
-     * Invoke only from SWT thread to reset the root of the graph explorer tree.\r
-     * \r
-     * @param root\r
-     */\r
-    private void doSetRoot(NodeContext root) {\r
-        if (tree.isDisposed())\r
-            return;\r
-        if (root.getConstant(BuiltinKeys.INPUT) == null) {\r
-            ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));\r
-            return;\r
-        }\r
-\r
-        // Empty caches, release queries.\r
-        GraphExplorerContext oldContext = explorerContext;\r
-        GraphExplorerContext newContext = new GraphExplorerContext();\r
-        GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));\r
-        this.explorerContext = newContext;\r
-        oldContext.safeDispose();\r
-        toolTip.setGraphExplorerContext(explorerContext);\r
-\r
-        // Need to empty these or otherwise they won't be emptied until the\r
-        // explorer is disposed which would mean that many unwanted references\r
-        // will be held by this map.\r
-        clearPrimitiveProcessors();\r
-\r
-        this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root\r
-                : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);\r
-\r
-        explorerContext.getCache().incRef(this.rootContext);\r
-        \r
-       initializeState();\r
-        \r
-        NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);\r
-\r
-        tree.setItemCount(contexts.length);\r
-\r
-        select(rootContext);\r
-        refreshColumnSizes();\r
-    }\r
-\r
-    @Override\r
-    public NodeContext getRoot() {\r
-        return rootContext;\r
-    }\r
-\r
-    @Override\r
-    public NodeContext getParentContext(NodeContext context) {\r
-        if (disposed)\r
-            throw new IllegalStateException("disposed");\r
-        if (!thread.currentThreadAccess())\r
-            throw new IllegalStateException("not in SWT display thread " + thread.getThread());\r
-\r
-        TreeItem item = contextToItem.getRight(context);\r
-        if(item == null) return null;\r
-        TreeItem parentItem = item.getParentItem();\r
-        if(parentItem == null) return null;\r
-        return (NodeContext)parentItem.getData();\r
-    }\r
-\r
-    Point previousTreeSize;\r
-    Point previousTreeParentSize;\r
-    boolean activatedBefore = false;\r
-\r
-    @Override\r
-    public void handleEvent(Event event) {\r
-        //System.out.println("EVENT: " + event);\r
-        switch(event.type) {\r
-            case SWT.Expand:\r
-                //System.out.println("EXPAND: " + event.item);\r
-                if ((tree.getStyle() & SWT.VIRTUAL) != 0) {\r
-                    expandVirtual(event);\r
-                } else {\r
-                    System.out.println("TODO: non-virtual tree item expand");\r
-                }\r
-                break;\r
-            case SWT.SetData:\r
-                // Only invoked for SWT.VIRTUAL trees\r
-                if (disposed)\r
-                    // Happened for Hannu once during program startup.\r
-                    // java.lang.AssertionError\r
-                    //   at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)\r
-                    //   at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)\r
-                    //   at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)\r
-                    // I do not know whether SWT guarantees that SetData events\r
-                    // don't come after Dispose event has been issued, but I\r
-                    // think its better to have this check here just incase.\r
-                    return;\r
-                setData(event);\r
-                break;\r
-            case SWT.Activate:\r
-                // This ensures that column sizes are refreshed at\r
-                // least once when the GE is first shown.\r
-                if (!activatedBefore) {\r
-                    refreshColumnSizes();\r
-                    activatedBefore = true;\r
-                }\r
-                break;\r
-            case SWT.Dispose:\r
-                //new Exception().printStackTrace();\r
-                if (disposed)\r
-                    return;\r
-                disposed = true;\r
-                doDispose();\r
-                break;\r
-            case SWT.Resize:\r
-                if (event.widget == tree) {\r
-                    // This case is meant for listening to tree width increase.\r
-                    // The column resizing must be performed only after the tree\r
-                    // itself as been resized.\r
-                    Point size = tree.getSize();\r
-                    int dx = 0;\r
-                    if (previousTreeSize != null) {\r
-                        dx = size.x - previousTreeSize.x;\r
-                    }\r
-                    previousTreeSize = size;\r
-                    //System.out.println("RESIZE: " + dx + " - size=" + size);\r
-\r
-                    if (dx > 0) {\r
-                        tree.setRedraw(false);\r
-                        refreshColumnSizes(size);\r
-                        tree.setRedraw(true);\r
-                    }\r
-                } else if (event.widget == tree.getParent()) {\r
-                    // This case is meant for listening to tree width decrease.\r
-                    // The columns must be resized before the tree widget itself\r
-                    // is resized to prevent scroll bar flicker. This can be achieved\r
-                    // by listening to the resize events of the tree parent widget.\r
-                    Composite parent = tree.getParent();\r
-                    Point size = parent.getSize();\r
-\r
-                    // We must subtract the parent's border and possible\r
-                    // scroll bar width from the new target width of the columns.\r
-                    size.x -= tree.getParent().getBorderWidth() * 2;\r
-                    ScrollBar vBar = parent.getVerticalBar();\r
-                    if (vBar != null && vBar.isVisible())\r
-                        size.x -= vBar.getSize().x;\r
-\r
-                    int dx = 0;\r
-                    if (previousTreeParentSize != null) {\r
-                        dx = size.x - previousTreeParentSize.x;\r
-                    }\r
-                    previousTreeParentSize = size;\r
-                    //System.out.println("RESIZE: " + dx + " - size=" + size);\r
-\r
-                    if (dx < 0) {\r
-                        tree.setRedraw(false);\r
-                        refreshColumnSizes(size);\r
-                        tree.setRedraw(true);\r
-                    }\r
-                }\r
-                break;\r
-            default:\r
-                break;\r
-        }\r
-\r
-    }\r
-\r
-    protected void refreshColumnSizes() {\r
-//        Composite treeParent = tree.getParent();\r
-//        Point size = treeParent.getSize();\r
-//        size.x -= treeParent.getBorderWidth() * 2;\r
-        Point size = tree.getSize();\r
-        refreshColumnSizes(size);\r
-        tree.getParent().layout();\r
-    }\r
-\r
-    /**\r
-     * This has been disabled since the logic of handling column widths has been\r
-     * externalized to parties creating {@link GraphExplorerImpl} instances.\r
-     */\r
-    protected void refreshColumnSizes(Point toSize) {\r
-        /*\r
-        refreshingColumnSizes = true;\r
-        try {\r
-            int columnCount = tree.getColumnCount();\r
-            if (columnCount > 0) {\r
-                Point size = toSize;\r
-                int targetWidth = size.x - tree.getBorderWidth() * 2;\r
-                targetWidth -= 0;\r
-\r
-                // Take the vertical scroll bar existence into to account when\r
-                // calculating the overflow column width.\r
-                ScrollBar vBar = tree.getVerticalBar();\r
-                //if (vBar != null && vBar.isVisible())\r
-                if (vBar != null)\r
-                    targetWidth -= vBar.getSize().x;\r
-\r
-                List<TreeColumn> resizing = new ArrayList<TreeColumn>();\r
-                int usedWidth = 0;\r
-                int resizingWidth = 0;\r
-                int totalWeight = 0;\r
-                for (int i = 0; i < columnCount - 1; ++i) {\r
-                    TreeColumn col = tree.getColumn(i);\r
-                    //System.out.println("  " + col.getText() + ": " + col.getWidth());\r
-                    int width = col.getWidth();\r
-                    usedWidth += width;\r
-                    Column c = (Column) col.getData();\r
-                    if (c.hasGrab()) {\r
-                        resizing.add(col);\r
-                        resizingWidth += width;\r
-                        totalWeight += c.getWeight();\r
-                    }\r
-                }\r
-\r
-                int requiredWidthAdjustment = targetWidth - usedWidth;\r
-                if (requiredWidthAdjustment < 0)\r
-                    requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());\r
-                double diff = requiredWidthAdjustment;\r
-                //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);\r
-\r
-                // Decide how much to give space to / take space from each grabbing column\r
-                double wrel = 1.0 / resizing.size();\r
-\r
-                double[] weightedShares = new double[resizing.size()];\r
-                for (int i = 0; i < resizing.size(); ++i) {\r
-                    TreeColumn col = resizing.get(i);\r
-                    Column c = (Column) col.getData();\r
-                    if (totalWeight == 0) {\r
-                        weightedShares[i] = wrel;\r
-                    } else {\r
-                        weightedShares[i] = (double) c.getWeight() / (double) totalWeight;\r
-                    }\r
-                }\r
-                //System.out.println("grabbing columns:" + resizing);\r
-                //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));\r
-\r
-                // Always shrink the columns if necessary, but don't enlarge before\r
-                // there is sufficient space to at least give all resizable columns\r
-                // some more width.\r
-                if (diff < 0 || (diff > 0 && diff > resizing.size())) {\r
-                    // Need to either shrink or enlarge the resizable columns if possible.\r
-                    for (int i = 0; i < resizing.size(); ++i) {\r
-                        TreeColumn col = resizing.get(i);\r
-                        Column c = (Column) col.getData();\r
-                        int cw = col.getWidth();\r
-                        //double wrel = (double) cw / (double) resizingWidth;\r
-                        //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);\r
-                        double ddelta = weightedShares[i] * diff;\r
-                        int delta = 0;\r
-                        if (diff < 0) {\r
-                            delta = (int) Math.floor(ddelta);\r
-                        } else {\r
-                            delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);\r
-                        }\r
-                        //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);\r
-                        //System.out.println("argh(" + col.getText() + "): " + c.getWidth() +  " vs. " + col.getWidth() + " vs. " + (cw+delta));\r
-                        int newWidth = Math.max(c.getWidth(), cw + delta);\r
-                        requiredWidthAdjustment -= (newWidth - cw);\r
-                        col.setWidth(newWidth);\r
-                    }\r
-                }\r
-\r
-                //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);\r
-\r
-                TreeColumn last = tree.getColumn(columnCount - 1);\r
-                // HACK: see #setColumns for why this is here.\r
-                if (FILLER.equals(last.getText())) {\r
-                    last.setWidth(Math.max(0, requiredWidthAdjustment));\r
-                }\r
-            }\r
-        } finally {\r
-            refreshingColumnSizes = false;\r
-        }\r
-         */\r
-    }\r
-\r
-    private void doDispose() {\r
-        explorerContext.dispose();\r
-\r
-        // No longer necessary, the used executors are shared.\r
-        //scheduler.shutdown();\r
-        //scheduler2.shutdown();\r
-\r
-        processors.clear();\r
-        detachPrimitiveProcessors();\r
-        primitiveProcessors.clear();\r
-        dataSources.clear();\r
-\r
-        pendingItems.clear();\r
-\r
-        rootContext = null;\r
-\r
-        contextToItem.clear();\r
-\r
-        mouseListeners.clear();\r
-\r
-        selectionProvider.clearListeners();\r
-        selectionProvider = null;\r
-        selectionDataResolver = null;\r
-        selectionRefreshContexts.clear();\r
-        selectedItems.clear();\r
-        originalFont = null;\r
-\r
-        localResourceManager.dispose();\r
-\r
-        // Must shutdown image loader job before disposing its ResourceManager\r
-        imageLoaderJob.dispose();\r
-        imageLoaderJob.cancel();\r
-        try {\r
-            imageLoaderJob.join();\r
-        } catch (InterruptedException e) {\r
-            ErrorLogger.defaultLogError(e);\r
-        }\r
-        resourceManager.dispose();\r
-        \r
-        postSelectionProvider.dispose();\r
-\r
-    }\r
-\r
-    private void expandVirtual(final Event event) {\r
-        TreeItem item = (TreeItem) event.item;\r
-        assert (item != null);\r
-        NodeContext context = (NodeContext) item.getData();\r
-        assert (context != null);\r
-\r
-        GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));\r
-        NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);\r
-        int maxChildren = getMaxChildren(manager, context);\r
-        item.setItemCount(children.length < maxChildren ? children.length : maxChildren);\r
-    }\r
-\r
-    private NodeContext getNodeContext(TreeItem item) {\r
-        assert(item != null);\r
-\r
-        NodeContext context = (NodeContext)item.getData();\r
-        assert(context != null);\r
-\r
-        return context;\r
-    }\r
-\r
-    private NodeContext getParentContext(TreeItem item) {\r
-        TreeItem parentItem = item.getParentItem();\r
-        if(parentItem != null) {\r
-            return getNodeContext(parentItem);\r
-        } else {\r
-            return rootContext;\r
-        }\r
-    }\r
-\r
-    private static final String LISTENER_SET_INDICATOR = "LSI";\r
-    private static final String PENDING = "PENDING";\r
-    private int contextSelectionChangeModCount = 0;\r
-\r
-    /**\r
-     * Only invoked for SWT.VIRTUAL widgets.\r
-     * \r
-     * @param event\r
-     */\r
-    private void setData(final Event event) {\r
-        assert (event != null);\r
-        TreeItem item = (TreeItem) event.item;\r
-        assert (item != null);\r
-\r
-        // Based on experience it seems to be possible that\r
-        // SetData events are sent for disposed TreeItems.\r
-        if (item.isDisposed() || item.getData(PENDING) != null)\r
-            return;\r
-\r
-        //System.out.println("GE.SetData " + item);\r
-\r
-        GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));\r
-\r
-        NodeContext parentContext = getParentContext(item);\r
-        assert (parentContext != null);\r
-\r
-        NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);\r
-\r
-        // Some children have disappeared since counting\r
-        if (event.index < 0) {\r
-            ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());\r
-            return;\r
-        }\r
-        if (event.index >= parentChildren.length)\r
-            return;\r
-\r
-        NodeContext context = parentChildren[event.index];\r
-        assert (context != null);\r
-        item.setData(context);\r
-        \r
-        // Manage NodeContext -> TreeItem mappings\r
-        contextToItem.map(context, item);\r
-        if (item.getData(LISTENER_SET_INDICATOR) == null) {\r
-            // This "if" exists because setData will get called many\r
-            // times for the same (NodeContext, TreeItem) pairs.\r
-            // Each TreeItem only needs one listener, but this\r
-            // is needed to tell whether it already has a listener\r
-            // or not.\r
-            item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);\r
-            item.addListener(SWT.Dispose, itemDisposeListener);\r
-        }\r
-\r
-        boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);\r
-\r
-        PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);\r
-        int maxChildren = getMaxChildren(manager, context);\r
-        //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);\r
-\r
-     NodeContext[] pruned = children.getPrunedChildren(); \r
-     int count = Math.min(pruned.length, maxChildren);\r
-\r
-        if (isExpanded || item.getItemCount() > 1) {\r
-            item.setItemCount(count);\r
-            TreeItem[] childItems = item.getItems();\r
-         for(int i=0;i<count;i++)\r
-             contextToItem.map(pruned[i], childItems[i]);\r
-        } else {\r
-            if (children.getPrunedChildren().length == 0) {\r
-                item.setItemCount(0);\r
-            } else {\r
-//                item.setItemCount(1);\r
-                item.setItemCount(count);\r
-                TreeItem[] childItems = item.getItems();\r
-             for(int i=0;i<count;i++)\r
-                 contextToItem.map(pruned[i], childItems[i]);\r
-//                item.getItem(0).setData(PENDING, PENDING);\r
-//                item.getItem(0).setItemCount(o);\r
-            }\r
-        }\r
-\r
-        setTextAndImage(item, manager, context, event.index);\r
-\r
-        // Check if the node should be auto-expanded?\r
-        if ((autoExpandLevel == ALL_LEVELS || autoExpandLevel > 1) && !isExpanded) {\r
-            //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");\r
-            int level = getTreeItemLevel(item);\r
-            if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)\r
-                    && !explorerContext.autoExpanded.containsKey(context))\r
-            {\r
-                //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");\r
-                explorerContext.autoExpanded.put(context, Boolean.TRUE);\r
-                setExpanded(context, true);\r
-            }\r
-        }\r
-\r
-        item.setExpanded(isExpanded);\r
-\r
-        if ((tree.getStyle() & SWT.CHECK) != 0) {\r
-            CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);\r
-            item.setChecked(CheckedState.CHECKED_STATES.contains(checked));\r
-            item.setGrayed(CheckedState.GRAYED == checked);\r
-        }\r
-\r
-        //System.out.println("GE.SetData completed " + item);\r
-\r
-        // This test makes sure that selectionProvider holds the correct\r
-        // selection with respect to the actual selection stored by the virtual\r
-        // SWT Tree widget.\r
-        // The data items shown below the items occupied by the selected and now removed data\r
-        // will be squeezed to use the tree items previously used for the now\r
-        // removed data. When this happens, the NodeContext items stored by the\r
-        // tree items will be different from what the GraphExplorer's\r
-        // ISelectionProvider thinks the selection currently is. To compensate,\r
-        // 1. Recognize the situation\r
-        // 2. ASAP set the selection provider selection to what is actually\r
-        // offered by the tree widget.\r
-        NodeContext selectedContext = selectedItems.get(item);\r
-//        System.out.println("selectedContext(" + item + "): " + selectedContext);\r
-        if (selectedContext != null && !selectedContext.equals(context)) {\r
-               final int modCount = ++contextSelectionChangeModCount;\r
-//            System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);\r
-//            System.out.println("    old context: " + selectedContext);\r
-//            System.out.println("    new context: " + context);\r
-//            System.out.println("    provider selection: " + selectionProvider.getSelection());\r
-//            System.out.println("    widget   selection: " + getWidgetSelection());\r
-            ThreadUtils.asyncExec(thread, new Runnable() {\r
-                @Override\r
-                public void run() {\r
-                    if (isDisposed())\r
-                        return;\r
-                    int count = contextSelectionChangeModCount;\r
-//                    System.out.println("MODCOUNT: " + modCount + " vs. " + count);\r
-                    if (modCount != count)\r
-                        return;\r
-                    widgetSelectionChanged(true);\r
-                }\r
-            });\r
-        }\r
-\r
-        // This must be done to keep the visible tree selection properly\r
-        // in sync with the selectionProvider JFace proxy of this class in\r
-        // cases where an in-line editor was previously active for the node\r
-        // context.\r
-        if (selectionRefreshContexts.remove(context)) {\r
-            final ISelection currentSelection = selectionProvider.getSelection();\r
-            // asyncExec is here to prevent ui glitches that\r
-            // seem to occur if the selection setting is done\r
-            // directly here in between setData invocations.\r
-            ThreadUtils.asyncExec(thread, new Runnable() {\r
-                @Override\r
-                public void run() {\r
-                    if (isDisposed())\r
-                        return;\r
-//                    System.out.println("REFRESHING SELECTION: " + currentSelection);\r
-//                    System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));\r
-//                    System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());\r
-                    setSelection(currentSelection, true);\r
-//                    System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));\r
-//                    System.out.println("AFTER setSelection: " + selectionProvider.getSelection());\r
-                }\r
-            });\r
-        }\r
-\r
-        // TODO: doesn't work if any part of the node path that should be\r
-        // revealed is out of view.\r
-        // Disabled until a better solution is devised.\r
-        // Suggestion: include item indexes into the stored node context path\r
-        // to make it possible for this method to know whether the current\r
-        // node path segment is currently out of view based on event.index.\r
-        // If out of view, this code needs to scroll the view programmatically\r
-        // onwards.\r
-//        if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {\r
-//            NodeContext topNode = topNodePath[currentTopNodePathIndex];\r
-//            if (topNode.equals(context)) {\r
-//                final TreeItem topItem = item;\r
-//                ++currentTopNodePathIndex;\r
-//                if (currentTopNodePathIndex >= topNodePath.length) {\r
-//                    // Mission accomplished. End search for top node here.\r
-//                    topNodePath = NodeContext.NONE;\r
-//                    currentTopNodePathIndex = -1;\r
-//                }\r
-//                ThreadUtils.asyncExec(thread, new Runnable() {\r
-//                    @Override\r
-//                    public void run() {\r
-//                        if (isDisposed())\r
-//                            return;\r
-//                        tree.setTopItem(topItem);\r
-//                    }\r
-//                });\r
-//            }\r
-//        }\r
-\r
-        // Check if vertical scroll bar has become visible and refresh layout.\r
-        ScrollBar verticalBar = tree.getVerticalBar();\r
-        if(verticalBar != null) {\r
-               boolean currentlyVerticalBarVisible = verticalBar.isVisible();\r
-               if (verticalBarVisible != currentlyVerticalBarVisible) {\r
-                   verticalBarVisible = currentlyVerticalBarVisible;\r
-                   Composite parent = tree.getParent();\r
-                   if (parent != null)\r
-                       parent.layout();\r
-               }\r
-        }\r
-    }\r
-\r
-    /**\r
-     * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the\r
-     *         return value is calculated. Items without parents have level=2,\r
-     *         their children level=3, etc. Returns 0 for invalid items\r
-     */\r
-    private int getTreeItemLevel(TreeItem item) {\r
-        if (item == null)\r
-            return 0;\r
-        int level = 1;\r
-        for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);\r
-        //System.out.println("\tgetTreeItemLevel(" + parent + ")");\r
-        //System.out.println("level(" + item + "): " + level);\r
-        return level;\r
-    }\r
-\r
-    /**\r
-     * @param node\r
-     * @return\r
-     */\r
-    private NodeContext[] getNodeContextPathSegments(NodeContext node) {\r
-        TreeItem item = contextToItem.getRight(node);\r
-        if (item == null)\r
-            return NodeContext.NONE;\r
-        int level = getTreeItemLevel(item);\r
-        if (level == 0)\r
-            return NodeContext.NONE;\r
-        // Exclude root from the saved node path.\r
-        --level;\r
-        NodeContext[] segments = new NodeContext[level];\r
-        for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {\r
-            NodeContext ctx = (NodeContext) item.getData();\r
-            if (ctx == null)\r
-                return NodeContext.NONE;\r
-            segments[level-1] = ctx;\r
-        }\r
-        return segments;\r
-    }\r
-\r
-    /**\r
-     * @param node\r
-     * @return\r
-     */\r
-    @SuppressWarnings("unused")\r
-    private NodeContextPath getNodeContextPath(NodeContext node) {\r
-        NodeContext[] path = getNodeContextPathSegments(node);\r
-        return new NodeContextPath(path);\r
-    }\r
-\r
-    void setImage(NodeContext node, TreeItem item, Imager imager, Collection<ImageDecorator> decorators, int itemIndex) {\r
-        Image[] images = columnImageArray;\r
-        Arrays.fill(images, null);\r
-        if (imager == null) {\r
-            item.setImage(images);\r
-            return;\r
-        }\r
-\r
-        Object[] descOrImage = columnDescOrImageArray;\r
-        Arrays.fill(descOrImage, null);\r
-        boolean finishLoadingInJob = false;\r
-        int index = 0;\r
-        for (Column column : columns) {\r
-            String key = column.getKey();\r
-            ImageDescriptor desc = imager.getImage(key);\r
-            if (desc != null) {\r
-                // Attempt to decorate the label\r
-                if (!decorators.isEmpty()) {\r
-                    for (ImageDecorator id : decorators) {\r
-                        ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);\r
-                        if (ds != null)\r
-                            desc = ds;\r
-                    }\r
-                }\r
-\r
-                // Try resolving only cached images here and now\r
-                Object img = localResourceManager.find(desc);\r
-                if (img == null)\r
-                    img = resourceManager.find(desc);\r
-\r
-                images[index] = img != null ? (Image) img : null;\r
-                descOrImage[index] = img == null ? desc : img;\r
-                finishLoadingInJob |= img == null;\r
-            }\r
-            ++index;\r
-        }\r
-\r
-        // Finish loading the final image in the image loader job if necessary.\r
-        if (finishLoadingInJob) {\r
-            // Prevent UI from flashing unnecessarily by reusing the old image\r
-            // in the item if it exists.\r
-            for (int c = 0; c < columns.length; ++c) {\r
-                Image img = item.getImage(c);\r
-                if (img != null)\r
-                    images[c] = img;\r
-            }\r
-            item.setImage(images);\r
-\r
-            // Schedule loading to another thread to refrain from blocking\r
-            // the UI with database operations.\r
-            queueImageTask(item, new ImageTask(\r
-                    node,\r
-                    item,\r
-                    Arrays.copyOf(descOrImage, descOrImage.length)));\r
-        } else {\r
-            // Set any images that were resolved.\r
-            item.setImage(images);\r
-        }\r
-    }\r
-\r
-    private void queueImageTask(TreeItem item, ImageTask task) {\r
-        synchronized (imageTasks) {\r
-            imageTasks.put(item, task);\r
-        }\r
-        imageLoaderJob.scheduleIfNecessary(100);\r
-    }\r
-\r
-    /**\r
-     * Invoked in a job worker thread.\r
-     * \r
-     * @param monitor\r
-     * @see ImageLoaderJob\r
-     */\r
-    @Override\r
-       protected IStatus setPendingImages(IProgressMonitor monitor) {\r
-        ImageTask[] tasks = null;\r
-        synchronized (imageTasks) {\r
-            tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);\r
-            imageTasks.clear();\r
-        }\r
-        if (tasks.length == 0)\r
-            return Status.OK_STATUS;\r
-\r
-        MultiStatus status = null;\r
-\r
-        // Load missing images\r
-        for (ImageTask task : tasks) {\r
-            Object[] descs = task.descsOrImages;\r
-            for (int i = 0; i < descs.length; ++i) {\r
-                Object obj = descs[i];\r
-                if (obj instanceof ImageDescriptor) {\r
-                    ImageDescriptor desc = (ImageDescriptor) obj; \r
-                    try {\r
-                        descs[i] = resourceManager.get((ImageDescriptor) desc);\r
-                    } catch (DeviceResourceException e) {\r
-                        if (status == null)\r
-                            status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);\r
-                        status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));\r
-                    }\r
-                }\r
-            }\r
-        }\r
-\r
-        // Perform final UI updates in the UI thread.\r
-        final ImageTask[] _tasks = tasks;\r
-        thread.asyncExec(new Runnable() {\r
-            @Override\r
-            public void run() {\r
-                if (!tree.isDisposed()) {\r
-                    tree.setRedraw(false);\r
-                    setImages(_tasks);\r
-                    tree.setRedraw(true);\r
-                }\r
-            }\r
-        });\r
-\r
-        return status != null ? status : Status.OK_STATUS;\r
-    }\r
-\r
-    /**\r
-     * Invoked in the UI thread only.\r
-     * \r
-     * @param task\r
-     */\r
-    void setImages(ImageTask[] tasks) {\r
-        for (ImageTask task : tasks)\r
-            if (task != null)\r
-                setImage(task);\r
-    }\r
-\r
-    /**\r
-     * Invoked in the UI thread only.\r
-     * \r
-     * @param task\r
-     */\r
-    void setImage(ImageTask task) {\r
-        // Be sure not to process disposed items.\r
-        if (task.item.isDisposed())\r
-            return;\r
-        // Discard this task if the TreeItem has switched owning NodeContext.\r
-        if (!contextToItem.contains(task.node, task.item))\r
-            return;\r
-\r
-        Object[] descs = task.descsOrImages;\r
-        Image[] images = columnImageArray;\r
-        Arrays.fill(images, null);\r
-        for (int i = 0; i < descs.length; ++i) {\r
-            Object desc = descs[i];\r
-            if (desc instanceof Image) {\r
-                images[i] = (Image) desc;\r
-            }\r
-        }\r
-        task.item.setImage(images);\r
-    }\r
-\r
-    void setText(TreeItem item, Labeler labeler, Collection<LabelDecorator> decorators, int itemIndex) {\r
-        if (labeler != null) {\r
-            String[] texts = new String[columns.length];\r
-            int index = 0;\r
-            Map<String, String> labels = labeler.getLabels();\r
-            Map<String, String> runtimeLabels = labeler.getRuntimeLabels();\r
-            for (Column column : columns) {\r
-                String key = column.getKey();\r
-                String s = null;\r
-                if (runtimeLabels != null) s = runtimeLabels.get(key);\r
-                if (s == null) s = labels.get(key);\r
-                if (s != null) {\r
-                    FontDescriptor font = originalFont;\r
-                    ColorDescriptor bg = originalBackground;\r
-                    ColorDescriptor fg = originalForeground;\r
-\r
-                    // Attempt to decorate the label\r
-                    if (!decorators.isEmpty()) {\r
-                        for (LabelDecorator ld : decorators) {\r
-                            String ds = ld.decorateLabel(s, key, itemIndex);\r
-                            if (ds != null)\r
-                                s = ds;\r
-\r
-                            FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);\r
-                            if (dfont != null)\r
-                                font = dfont;\r
-\r
-                            ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);\r
-                            if (dbg != null)\r
-                                bg = dbg;\r
-\r
-                            ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);\r
-                            if (dfg != null)\r
-                                fg = dfg;\r
-                        }\r
-                    }\r
-\r
-                    if (font != originalFont) {\r
-                        //System.out.println("set font: " + index + ": " + font);\r
-                        item.setFont(index, (Font) localResourceManager.get(font));\r
-                    }\r
-                    if (bg != originalBackground)\r
-                        item.setBackground(index, (Color) localResourceManager.get(bg));\r
-                    if (fg != originalForeground)\r
-                        item.setForeground(index, (Color) localResourceManager.get(fg));\r
-\r
-                    texts[index] = s;\r
-                }\r
-                ++index;\r
-            }\r
-            item.setText(texts);\r
-        } else {\r
-            item.setText(Labeler.NO_LABEL);\r
-        }\r
-    }\r
-\r
-    void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {\r
-        Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);\r
-        if (labeler != null) {\r
-            labeler.setListener(labelListener);\r
-        }\r
-        Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);\r
-        Collection<LabelDecorator> labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);\r
-        Collection<ImageDecorator> imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);\r
-\r
-        setText(item, labeler, labelDecorators, itemIndex);\r
-        setImage(context, item, imager, imageDecorators, itemIndex);\r
-    }\r
-\r
-    @Override\r
-    public void setFocus() {\r
-        tree.setFocus();\r
-    }\r
-\r
-    @Override\r
-    public <T> T query(NodeContext context, CacheKey<T> key) {\r
-        return this.explorerContext.cache.get(context, key);\r
-    }\r
-\r
-    @Override\r
-    public boolean isDisposed() {\r
-        return disposed;\r
-    }\r
-\r
-    protected void assertNotDisposed() {\r
-        if (isDisposed())\r
-            throw new IllegalStateException("disposed");\r
-    }\r
-\r
-\r
-\r
-    /**\r
-     * @param selection\r
-     * @param forceControlUpdate\r
-     * @thread any\r
-     */\r
-    public void setSelection(final ISelection selection, boolean forceControlUpdate) {\r
-        assertNotDisposed();\r
-        boolean equalsOld = selectionProvider.selectionEquals(selection);\r
-        if (equalsOld && !forceControlUpdate) {\r
-            // Just set the selection object instance, fire no events nor update\r
-            // the viewer selection.\r
-            selectionProvider.setSelection(selection);\r
-        } else {\r
-            // Schedule viewer and selection update if necessary.\r
-            if (tree.isDisposed())\r
-                return;\r
-            Display d = tree.getDisplay();\r
-            if (d.getThread() == Thread.currentThread()) {\r
-                updateSelectionToControl(selection);\r
-            } else {\r
-                d.asyncExec(new Runnable() {\r
-                    @Override\r
-                    public void run() {\r
-                        if (tree.isDisposed())\r
-                            return;\r
-                        updateSelectionToControl(selection);\r
-                    }\r
-                });\r
-            }\r
-        }\r
-    }\r
-    \r
-\r
-    /* Contains the best currently found tree item and its priority\r
-     */\r
-    private static class SelectionResolutionStatus {\r
-        int bestPriority = Integer.MAX_VALUE;\r
-        TreeItem bestItem;\r
-    }\r
-\r
-    /**\r
-     * @param selection\r
-     * @thread SWT\r
-     */\r
-    private void updateSelectionToControl(ISelection selection) {\r
-        if (selectionDataResolver == null)\r
-            return;\r
-        if (!(selection instanceof IStructuredSelection))\r
-            return;\r
-        \r
-        // Initialize selection resolution status map \r
-        IStructuredSelection iss = (IStructuredSelection) selection;\r
-        final THashMap<Object,SelectionResolutionStatus> statusMap =\r
-                new THashMap<Object,SelectionResolutionStatus>(iss.size());\r
-        for(Iterator<?> it = iss.iterator(); it.hasNext();) {\r
-            Object selectionElement = it.next();\r
-            Object resolvedElement = selectionDataResolver.resolve(selectionElement);\r
-            statusMap.put(\r
-                    resolvedElement,\r
-                    new SelectionResolutionStatus());\r
-        }\r
-        \r
-        // Iterate all tree items and try to match them to the selection\r
-        iterateTreeItems(new TObjectProcedure<TreeItem>() {\r
-            @Override\r
-            public boolean execute(TreeItem treeItem) {\r
-                NodeContext nodeContext = (NodeContext)treeItem.getData();\r
-                if(nodeContext == null)\r
-                    return true;\r
-                SelectionResolutionStatus status = statusMap.get(nodeContext);\r
-                if(status != null) {\r
-                    status.bestPriority = 0; // best possible match\r
-                    status.bestItem = treeItem;\r
-                    return true;\r
-                }\r
-                \r
-                Object input = nodeContext.getConstant(BuiltinKeys.INPUT);\r
-                status = statusMap.get(input);\r
-                if(status != null) {\r
-                    NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);\r
-                    int curPriority = nodeType instanceof EntityNodeType \r
-                            ? 1 // Prefer EntityNodeType matches to other node types\r
-                            : 2;\r
-                    if(curPriority < status.bestPriority) {\r
-                        status.bestPriority = curPriority;\r
-                        status.bestItem = treeItem;\r
-                    }\r
-                }\r
-                return true;\r
-            }\r
-        });\r
-        \r
-        // Update selection\r
-        ArrayList<TreeItem> items = new ArrayList<TreeItem>(statusMap.size());\r
-        for(SelectionResolutionStatus status : statusMap.values())\r
-            if(status.bestItem != null)\r
-                items.add(status.bestItem);\r
-        select(items.toArray(new TreeItem[items.size()]));\r
-    }\r
-\r
-    /**\r
-     * @thread SWT\r
-     */\r
-    public ISelection getWidgetSelection() {\r
-        TreeItem[] items = tree.getSelection();\r
-        if (items.length == 0)\r
-            return StructuredSelection.EMPTY;\r
-\r
-        List<NodeContext> nodes = new ArrayList<NodeContext>(items.length);\r
-\r
-        // Caches for resolving node contexts the hard way if necessary.\r
-        GENodeQueryManager manager = null;\r
-        NodeContext lastParentContext = null;\r
-        NodeContext[] lastChildren = null;\r
-\r
-        for (int i = 0; i < items.length; i++) {\r
-            TreeItem item = items[i];\r
-            NodeContext ctx = (NodeContext) item.getData();\r
-            // It may happen due to the virtual nature of the tree control\r
-            // that it contains TreeItems which have not yet been ran through\r
-            // #setData(Event).\r
-            if (ctx != null) {\r
-                nodes.add(ctx);\r
-            } else {\r
-                TreeItem parentItem = item.getParentItem();\r
-                NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;\r
-                if (parentContext != null) {\r
-                    NodeContext[] children = lastChildren;\r
-                    if (parentContext != lastParentContext) {\r
-                        if (manager == null)\r
-                            manager = new GENodeQueryManager(this.explorerContext, null, null, null);\r
-                        lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);\r
-                        lastParentContext = parentContext;\r
-                    }\r
-                    int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);\r
-                    if (index >= 0 && index < children.length) {\r
-                        NodeContext child = children[index];\r
-                        if (child != null) {\r
-                            nodes.add(child);\r
-                            // Cache NodeContext in TreeItem for faster access\r
-                            item.setData(child);\r
-                        }\r
-                    }\r
-                }\r
-            }\r
-        }\r
-        //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");\r
-        ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));\r
-        return selection;\r
-    }\r
-    \r
-    @Override\r
-    public TransientExplorerState getTransientState() {\r
-        if (!thread.currentThreadAccess())\r
-            throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());\r
-        return transientState;\r
-    }\r
-\r
-    /**\r
-     * @param item\r
-     * @thread SWT\r
-     */\r
-    private void select(TreeItem item) {\r
-        tree.setSelection(item);\r
-        tree.showSelection();\r
-        selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));\r
-    }\r
-\r
-    /**\r
-     * @param items\r
-     * @thread SWT\r
-     */\r
-    private void select(TreeItem[] items) {\r
-        //System.out.println("Select: " + Arrays.toString(items));\r
-        tree.setSelection(items);\r
-        tree.showSelection();\r
-        NodeContext[] data = new NodeContext[items.length];\r
-        for (int i = 0; i < data.length; i++) {\r
-            data[i] = (NodeContext) items[i].getData();\r
-        }\r
-        selectionProvider.setAndFireNonEqualSelection(constructSelection(data));\r
-    }\r
-    \r
-    private void iterateTreeItems(TObjectProcedure<TreeItem> procedure) {\r
-        for(TreeItem item : tree.getItems())\r
-            if(!iterateTreeItems(item, procedure))\r
-                return;\r
-    }\r
-\r
-    private boolean iterateTreeItems(TreeItem item,\r
-            TObjectProcedure<TreeItem> procedure) {\r
-        if(!procedure.execute(item))\r
-            return false;\r
-        if(item.getExpanded())\r
-            for(TreeItem child : item.getItems())\r
-                if(!iterateTreeItems(child, procedure))\r
-                    return false;\r
-        return true;\r
-    }\r
-\r
-    /**\r
-     * @param item\r
-     * @param context\r
-     * @return\r
-     */\r
-    private boolean trySelect(TreeItem item, Object input) {\r
-        NodeContext itemCtx = (NodeContext) item.getData();\r
-        if (itemCtx != null) {\r
-            if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {\r
-                select(item);\r
-                return true;\r
-            }\r
-        }\r
-        if (item.getExpanded()) {\r
-            for (TreeItem child : item.getItems()) {\r
-                if (trySelect(child, input))\r
-                    return true;\r
-            }\r
-        }\r
-        return false;\r
-    }\r
-\r
-    private boolean equalsEnough(NodeContext c1, NodeContext c2) {\r
-       \r
-       Object input1 = c1.getConstant(BuiltinKeys.INPUT);\r
-       Object input2 = c2.getConstant(BuiltinKeys.INPUT);\r
-       if(!ObjectUtils.objectEquals(input1, input2)) \r
-               return false;\r
-\r
-       Object type1 = c1.getConstant(NodeType.TYPE);\r
-       Object type2 = c2.getConstant(NodeType.TYPE);\r
-       if(!ObjectUtils.objectEquals(type1, type2)) \r
-               return false;\r
-       \r
-       return true;\r
-       \r
-    }\r
-    \r
-    private NodeContext tryFind(NodeContext context) {\r
-        for (TreeItem item : tree.getItems()) {\r
-               NodeContext found = tryFind(item, context);\r
-               if(found != null) return found;\r
-        }\r
-        return null;\r
-    }\r
-    \r
-    private NodeContext tryFind(TreeItem item, NodeContext context) {\r
-        NodeContext itemCtx = (NodeContext) item.getData();\r
-        if (itemCtx != null) {\r
-            if (equalsEnough(context, itemCtx)) {\r
-                return itemCtx;\r
-            }\r
-        }\r
-        if (item.getExpanded()) {\r
-            for (TreeItem child : item.getItems()) {\r
-               NodeContext found = tryFind(child, context);\r
-               if(found != null) return found;\r
-            }\r
-        }\r
-        return null;\r
-    }\r
-    \r
-    @Override\r
-    public boolean select(NodeContext context) {\r
-\r
-        assertNotDisposed();\r
-\r
-        if (context == null || context.equals(rootContext)) {\r
-            tree.deselectAll();\r
-            selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);\r
-            return true;\r
-        }\r
-\r
-//        if (context.equals(rootContext)) {\r
-//            tree.deselectAll();\r
-//            selectionProvider.setAndFireNonEqualSelection(constructSelection(context));\r
-//            return;\r
-//        }\r
-\r
-        Object input = context.getConstant(BuiltinKeys.INPUT);\r
-\r
-        for (TreeItem item : tree.getItems()) {\r
-            if (trySelect(item, input))\r
-                return true;\r
-        }\r
-        \r
-        return false;\r
-        \r
-    }\r
-    \r
-    private NodeContext tryFind2(NodeContext context) {\r
-       Set<NodeContext> ctxs = contextToItem.getLeftSet();\r
-       for(NodeContext c : ctxs) \r
-               if(equalsEnough(c, context)) \r
-                       return c;\r
-       return null;\r
-    }\r
-\r
-    private boolean waitVisible(NodeContext parent, NodeContext context) {\r
-       long start = System.nanoTime();\r
-       \r
-       TreeItem parentItem = contextToItem.getRight(parent);\r
-       \r
-       if(parentItem == null) \r
-               return false; \r
-       \r
-       while(true) {\r
-               NodeContext target = tryFind2(context);\r
-               if(target != null) {\r
-                       TreeItem item = contextToItem.getRight(target);\r
-                       if (!(item.getParentItem().equals(parentItem)))\r
-                               return false;\r
-                       tree.setTopItem(item);\r
-                       return true;\r
-               }\r
-\r
-               Display.getCurrent().readAndDispatch();\r
-               long duration = System.nanoTime() - start;\r
-               if(duration > 10e9)\r
-                       return false;                   \r
-       }\r
-    }\r
-    \r
-    private boolean selectPathInternal(NodeContext[] contexts, int position) {\r
-       //System.out.println("NodeContext path : " + contexts);\r
-\r
-       NodeContext head = tryFind(contexts[position]);\r
-\r
-       if(position == contexts.length-1) {\r
-               return select(head);\r
-\r
-       }\r
-\r
-       //setExpanded(head, true);\r
-       \r
-       if(!waitVisible(head, contexts[position+1])) \r
-               return false;\r
-       \r
-       setExpanded(head, true);\r
-       \r
-       return selectPathInternal(contexts, position+1);\r
-       \r
-    }\r
-    \r
-    @Override\r
-    public boolean selectPath(Collection<NodeContext> contexts) {\r
-       \r
-       if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");\r
-       if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");\r
-       \r
-       return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);\r
-       \r
-    }\r
-    \r
-    @Override\r
-    public boolean isVisible(NodeContext context) {\r
-       \r
-        for (TreeItem item : tree.getItems()) {\r
-               NodeContext found = tryFind(item, context);\r
-               if(found != null) \r
-                       return true;\r
-        }\r
-        \r
-        return false;\r
-        \r
-    }\r
-\r
-    protected ISelection constructSelection(NodeContext... contexts) {\r
-        if (contexts ==  null)\r
-            throw new IllegalArgumentException("null contexts");\r
-        if (contexts.length == 0)\r
-            return StructuredSelection.EMPTY;\r
-        if (selectionFilter == null)\r
-            return new StructuredSelection(transformSelection(contexts));\r
-        return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );\r
-    }\r
-\r
-    protected Object[] transformSelection(Object[] objects) {\r
-        return selectionTransformation.call(this, objects);\r
-    }\r
-\r
-    protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {\r
-        int len = contexts.length;\r
-        Object[] objects = new Object[len];\r
-        for (int i = 0; i < len; ++i)\r
-            objects[i] = filter.filter(contexts[i]);\r
-        return objects;\r
-    }\r
-\r
-    @Override\r
-    public void setExpanded(final NodeContext context, final boolean expanded) {\r
-        assertNotDisposed();\r
-        ThreadUtils.asyncExec(thread, new Runnable() {\r
-            @Override\r
-            public void run() {\r
-                if (!isDisposed())\r
-                    doSetExpanded(context, expanded);\r
-            }\r
-        });\r
-    }\r
-\r
-    private void doSetExpanded(NodeContext context, boolean expanded) {\r
-        //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");\r
-        TreeItem item = contextToItem.getRight(context);\r
-        if (item != null) {\r
-            item.setExpanded(expanded);\r
-        }\r
-        PrimitiveQueryProcessor<?> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);\r
-        if (pqp instanceof IsExpandedProcessor) {\r
-            IsExpandedProcessor iep = (IsExpandedProcessor) pqp;\r
-            iep.replaceExpanded(context, expanded);\r
-        }\r
-    }\r
-\r
-    @Override\r
-    public void setColumnsVisible(boolean visible) {\r
-        columnsAreVisible = visible;\r
-        if(tree != null) tree.setHeaderVisible(columnsAreVisible);\r
-    }\r
-\r
-    @Override\r
-    public void setColumns(final Column[] columns) {\r
-        setColumns(columns, null);\r
-    }\r
-\r
-    @Override\r
-    public void setColumns(final Column[] columns, Consumer<Map<Column, Object>> callback) {\r
-        assertNotDisposed();\r
-        checkUniqueColumnKeys(columns);\r
-\r
-        Display d = tree.getDisplay();\r
-        if (d.getThread() == Thread.currentThread())\r
-            doSetColumns(columns, callback);\r
-        else\r
-            d.asyncExec(() -> {\r
-                if (tree.isDisposed())\r
-                    return;\r
-                doSetColumns(columns, callback);\r
-            });\r
-    }\r
-\r
-    private void checkUniqueColumnKeys(Column[] cols) {\r
-        Set<String> usedColumnKeys = new HashSet<String>();\r
-        List<Column> duplicateColumns = new ArrayList<Column>();\r
-        for (Column c : cols) {\r
-            if (!usedColumnKeys.add(c.getKey()))\r
-                duplicateColumns.add(c);\r
-        }\r
-        if (!duplicateColumns.isEmpty()) {\r
-            throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Only meant to be invoked from the SWT UI thread.\r
-     * \r
-     * @param cols\r
-     */\r
-    private void doSetColumns(Column[] cols, Consumer<Map<Column, Object>> callback) {\r
-        // Attempt to keep previous column widths.\r
-        Map<String, Integer> prevWidths = new HashMap<String, Integer>();\r
-        for (TreeColumn column : tree.getColumns()) {\r
-            prevWidths.put(column.getText(), column.getWidth());\r
-            column.dispose();\r
-        }\r
-\r
-        HashMap<String, Integer> keyToIndex = new HashMap<String, Integer>();\r
-        for (int i = 0; i < cols.length; ++i) {\r
-            keyToIndex.put(cols[i].getKey(), i);\r
-        }\r
-\r
-        this.columns = Arrays.copyOf(cols, cols.length);\r
-        //this.columns[cols.length] = FILLER_COLUMN;\r
-        this.columnKeyToIndex = keyToIndex;\r
-        this.columnImageArray = new Image[cols.length];\r
-        this.columnDescOrImageArray = new Object[cols.length];\r
-\r
-        Map<Column, Object> map = new HashMap<Column, Object>();\r
-\r
-        tree.setHeaderVisible(columnsAreVisible);\r
-        for (Column column : columns) {\r
-            TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));\r
-            map.put(column, c);\r
-            c.setData(column);\r
-            c.setText(column.getLabel());\r
-            c.setToolTipText(column.getTooltip());\r
-\r
-            int cw = column.getWidth();\r
-\r
-            // Try to keep previous widths\r
-            Integer w = prevWidths.get(column);\r
-            if (w != null)\r
-                c.setWidth(w);\r
-            else if (cw != Column.DEFAULT_CONTROL_WIDTH)\r
-                c.setWidth(cw);\r
-            else {\r
-                // Go for some kind of default settings then...\r
-                if (ColumnKeys.PROPERTY.equals(column.getKey()))\r
-                    c.setWidth(150);\r
-                else\r
-                    c.setWidth(50);\r
-            }\r
-\r
-//            if (!column.hasGrab() && !FILLER.equals(column.getKey())) {\r
-//                c.addListener(SWT.Resize, resizeListener);\r
-//                c.setResizable(true);\r
-//            } else {\r
-//                //c.setResizable(false);\r
-//            }\r
-\r
-        }\r
-\r
-        if(callback != null) callback.accept(map);\r
-\r
-        // Make sure the explorer fits the columns properly after initialization.\r
-        tree.getDisplay().asyncExec(new Runnable() {\r
-            @Override\r
-            public void run() {\r
-                if (tree.isDisposed())\r
-                    return;\r
-                refreshColumnSizes();\r
-            }\r
-        });\r
-    }\r
-\r
-    int toSWT(Align alignment) {\r
-        switch (alignment) {\r
-            case LEFT: return SWT.LEFT;\r
-            case CENTER: return SWT.CENTER;\r
-            case RIGHT: return SWT.RIGHT;\r
-            default: throw new Error("unhandled alignment: " + alignment);\r
-        }\r
-    }\r
-\r
-    @Override\r
-    public Column[] getColumns() {\r
-        return Arrays.copyOf(columns, columns.length);\r
-    }\r
-\r
-    private void detachPrimitiveProcessors() {\r
-        for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {\r
-            if (p instanceof ProcessorLifecycle) {\r
-                ((ProcessorLifecycle) p).detached(this);\r
-            }\r
-        }\r
-    }\r
-\r
-    private void clearPrimitiveProcessors() {\r
-        for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {\r
-            if (p instanceof ProcessorLifecycle) {\r
-                ((ProcessorLifecycle) p).clear();\r
-            }\r
-        }\r
-    }\r
-\r
-    Listener resizeListener = new Listener() {\r
-        @Override\r
-        public void handleEvent(Event event) {\r
-            // Prevent infinite recursion.\r
-            if (refreshingColumnSizes)\r
-                return;\r
-            //TreeColumn column = (TreeColumn) event.widget;\r
-            //Column c = (Column) column.getData();\r
-            refreshColumnSizes();\r
-        }\r
-    };\r
-\r
-    Listener itemDisposeListener = new Listener() {\r
-        @Override\r
-        public void handleEvent(Event event) {\r
-            if (event.type == SWT.Dispose) {\r
-                if (event.widget instanceof TreeItem) {\r
-                    TreeItem ti = (TreeItem) event.widget;\r
-                    //NodeContext ctx = (NodeContext) ti.getData();\r
-//                    System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));\r
-//                    System.out.println("  map size BEFORE: " + contextToItem.size());\r
-                    @SuppressWarnings("unused")\r
-                    NodeContext removed = contextToItem.removeWithRight(ti);\r
-//                    System.out.println("  REMOVED: " + removed);\r
-//                    System.out.println("  map size AFTER: " + contextToItem.size());\r
-                }\r
-            }\r
-        }\r
-    };\r
-\r
-    /**\r
-     * \r
-     */\r
-    LabelerListener labelListener = new LabelerListener() {\r
-        @Override\r
-        public boolean columnModified(final NodeContext context, final String key, final String newLabel) {\r
-            //System.out.println("column " + key + " modified for " + context + " to " + newLabel);\r
-            if (tree.isDisposed())\r
-                return false;\r
-\r
-            synchronized (labelRefreshRunnables) {\r
-                Runnable refresher = new Runnable() {\r
-                    @Override\r
-                    public void run() {\r
-                        // Tree is guaranteed to be non-disposed if this is invoked.\r
-\r
-                        // contextToItem should be accessed only in the SWT thread to keep things thread-safe.\r
-                        final TreeItem item = contextToItem.getRight(context);\r
-                        if (item == null || item.isDisposed())\r
-                            return;\r
-\r
-                        final Integer index = columnKeyToIndex.get(key);\r
-                        if (index == null)\r
-                            return;\r
-\r
-                        //System.out.println(" found index: " + index);\r
-                        //System.out.println("  found item: " + item);\r
-                        try {\r
-                            GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);\r
-\r
-                            // FIXME: indexOf is quadratic\r
-                            int itemIndex = 0;\r
-                            TreeItem parentItem = item.getParentItem();\r
-                            if (parentItem == null) {\r
-                                itemIndex = tree.indexOf(item);\r
-                                //tree.clear(parentIndex, false);\r
-                            } else {\r
-                                itemIndex = parentItem.indexOf(item);\r
-                                //item.clear(parentIndex, false);\r
-                            }\r
-                            setTextAndImage(item, manager, context, itemIndex);\r
-                        } catch (SWTException e) {\r
-                            ErrorLogger.defaultLogError(e);\r
-                        }\r
-                    }\r
-                };\r
-                //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);\r
-                labelRefreshRunnables.put(context, refresher);\r
-\r
-                if (!refreshIsQueued) {\r
-                    refreshIsQueued = true;\r
-                    long delay = 0;\r
-                    long now = System.currentTimeMillis();\r
-                    long elapsed = now - lastLabelRefreshScheduled;\r
-                    if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)\r
-                        delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;\r
-                    //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");\r
-                    if (delay > 0) {\r
-                        ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {\r
-                            @Override\r
-                            public void run() {\r
-                                scheduleImmediateLabelRefresh();\r
-                            }\r
-                        }, delay, TimeUnit.MILLISECONDS);\r
-                    } else {\r
-                        scheduleImmediateLabelRefresh();\r
-                    }\r
-                    lastLabelRefreshScheduled = now;\r
-                }\r
-            }\r
-            return true;\r
-        }\r
-\r
-        @Override\r
-        public boolean columnsModified(final NodeContext context, final Map<String, String> columns) {\r
-            System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");\r
-            return false;\r
-        }\r
-    };\r
-\r
-    private void scheduleImmediateLabelRefresh() {\r
-        Runnable[] runnables = null;\r
-        synchronized (labelRefreshRunnables) {\r
-            if (labelRefreshRunnables.isEmpty())\r
-                return;\r
-\r
-            runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);\r
-            labelRefreshRunnables.clear();\r
-            refreshIsQueued = false;\r
-        }\r
-        final Runnable[] rs = runnables;\r
-\r
-        if (tree.isDisposed())\r
-            return;\r
-        tree.getDisplay().asyncExec(new Runnable() {\r
-            @Override\r
-            public void run() {\r
-                if (tree.isDisposed())\r
-                    return;\r
-                //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");\r
-                tree.setRedraw(false);\r
-                for (Runnable r : rs) {\r
-                    r.run();\r
-                }\r
-                tree.setRedraw(true);\r
-            }\r
-        });\r
-    }\r
-\r
-    long                       lastLabelRefreshScheduled = 0;\r
-    boolean                    refreshIsQueued           = false;\r
-    Map<NodeContext, Runnable> labelRefreshRunnables     = new HashMap<NodeContext, Runnable>();\r
-\r
-    @SuppressWarnings("unchecked")\r
-    @Override\r
-    public <T> T getAdapter(Class<T> adapter) {\r
-        if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;\r
-        else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;\r
-        return null;\r
-    }\r
-\r
-    @SuppressWarnings("unchecked")\r
-    @Override\r
-    public <T> T getControl() {\r
-        return (T) tree;\r
-    }\r
-\r
-    /* (non-Javadoc)\r
-     * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)\r
-     */\r
-    @Override\r
-    public void setAutoExpandLevel(int level) {\r
-        this.autoExpandLevel = level;\r
-    }\r
-\r
-    @Override\r
-    public <T> NodeQueryProcessor<T> getProcessor(QueryKey<T> key) {\r
-        return explorerContext.getProcessor(key);\r
-    }\r
-\r
-    @Override\r
-    public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(PrimitiveQueryKey<T> key) {\r
-        return explorerContext.getPrimitiveProcessor(key);\r
-    }\r
-\r
-    @Override\r
-    public boolean isEditable() {\r
-        return editable;\r
-    }\r
-\r
-    @Override\r
-    public void setEditable(boolean editable) {\r
-        if (!thread.currentThreadAccess())\r
-            throw new IllegalStateException("not in SWT display thread " + thread.getThread());\r
-\r
-        this.editable = editable;\r
-        Display display = tree.getDisplay();\r
-        tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));\r
-    }\r
-\r
-    /**\r
-     * For setting a more local service locator for the explorer than the global\r
-     * workbench service locator. Sometimes required to give this implementation\r
-     * access to local workbench services like IFocusService.\r
-     * \r
-     * <p>\r
-     * Must be invoked during right after construction.\r
-     * \r
-     * @param serviceLocator\r
-     *            a specific service locator or <code>null</code> to use the\r
-     *            workbench global service locator\r
-     */\r
-    public void setServiceLocator(IServiceLocator serviceLocator) {\r
-        if (serviceLocator == null && PlatformUI.isWorkbenchRunning())\r
-            serviceLocator = PlatformUI.getWorkbench();\r
-        this.serviceLocator = serviceLocator;\r
-        if (serviceLocator != null) {\r
-            this.contextService = (IContextService) serviceLocator.getService(IContextService.class);\r
-            this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);\r
-        }\r
-    }\r
-    \r
-    @Override\r
-    public Object getClicked(Object event) {\r
-       MouseEvent e = (MouseEvent)event;\r
-       final Tree tree = (Tree) e.getSource();\r
-        Point point = new Point(e.x, e.y);\r
-        TreeItem item = tree.getItem(point);\r
-\r
-        // No selectable item at point?\r
-        if (item == null)\r
-            return null;\r
-\r
-        Object data = item.getData();\r
-        return data;\r
-    }\r
-\r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2012 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * 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:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.browsing.ui.swt;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.AssertionFailedException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.MultiStatus;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.resource.ColorDescriptor;
+import org.eclipse.jface.resource.DeviceResourceException;
+import org.eclipse.jface.resource.DeviceResourceManager;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.resource.ResourceManager;
+import org.eclipse.jface.viewers.IPostSelectionProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.TreeEditor;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+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.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+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.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchSite;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.contexts.IContextActivation;
+import org.eclipse.ui.contexts.IContextService;
+import org.eclipse.ui.services.IServiceLocator;
+import org.eclipse.ui.swt.IFocusService;
+import org.simantics.browsing.ui.BuiltinKeys;
+import org.simantics.browsing.ui.CheckedState;
+import org.simantics.browsing.ui.Column;
+import org.simantics.browsing.ui.Column.Align;
+import org.simantics.browsing.ui.DataSource;
+import org.simantics.browsing.ui.ExplorerState;
+import org.simantics.browsing.ui.GraphExplorer;
+import org.simantics.browsing.ui.NodeContext;
+import org.simantics.browsing.ui.NodeContext.CacheKey;
+import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey;
+import org.simantics.browsing.ui.NodeContext.QueryKey;
+import org.simantics.browsing.ui.NodeContextPath;
+import org.simantics.browsing.ui.NodeQueryManager;
+import org.simantics.browsing.ui.NodeQueryProcessor;
+import org.simantics.browsing.ui.PrimitiveQueryProcessor;
+import org.simantics.browsing.ui.SelectionDataResolver;
+import org.simantics.browsing.ui.SelectionFilter;
+import org.simantics.browsing.ui.StatePersistor;
+import org.simantics.browsing.ui.common.ColumnKeys;
+import org.simantics.browsing.ui.common.ErrorLogger;
+import org.simantics.browsing.ui.common.NodeContextBuilder;
+import org.simantics.browsing.ui.common.NodeContextUtil;
+import org.simantics.browsing.ui.common.internal.GECache;
+import org.simantics.browsing.ui.common.internal.GENodeQueryManager;
+import org.simantics.browsing.ui.common.internal.IGECache;
+import org.simantics.browsing.ui.common.internal.IGraphExplorerContext;
+import org.simantics.browsing.ui.common.internal.UIElementReference;
+import org.simantics.browsing.ui.common.processors.DefaultCheckedStateProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultComparableChildrenProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultFinalChildrenProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultImageDecoratorProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultImagerFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultImagerProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultLabelDecoratorProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultLabelerFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultLabelerProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultPrunedChildrenProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedImageDecoratorFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelDecoratorFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelerProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointFactoryProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionsProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultViewpointProcessor;
+import org.simantics.browsing.ui.common.processors.IsExpandedProcessor;
+import org.simantics.browsing.ui.common.processors.NoSelectionRequestProcessor;
+import org.simantics.browsing.ui.common.processors.ProcessorLifecycle;
+import org.simantics.browsing.ui.content.ImageDecorator;
+import org.simantics.browsing.ui.content.Imager;
+import org.simantics.browsing.ui.content.LabelDecorator;
+import org.simantics.browsing.ui.content.Labeler;
+import org.simantics.browsing.ui.content.Labeler.CustomModifier;
+import org.simantics.browsing.ui.content.Labeler.DeniedModifier;
+import org.simantics.browsing.ui.content.Labeler.DialogModifier;
+import org.simantics.browsing.ui.content.Labeler.EnumerationModifier;
+import org.simantics.browsing.ui.content.Labeler.FilteringModifier;
+import org.simantics.browsing.ui.content.Labeler.LabelerListener;
+import org.simantics.browsing.ui.content.Labeler.Modifier;
+import org.simantics.browsing.ui.content.PrunedChildrenResult;
+import org.simantics.browsing.ui.model.nodetypes.EntityNodeType;
+import org.simantics.browsing.ui.model.nodetypes.NodeType;
+import org.simantics.browsing.ui.swt.internal.Threads;
+import org.simantics.db.layer0.SelectionHints;
+import org.simantics.utils.ObjectUtils;
+import org.simantics.utils.datastructures.BijectionMap;
+import org.simantics.utils.datastructures.BinaryFunction;
+import org.simantics.utils.datastructures.disposable.AbstractDisposable;
+import org.simantics.utils.datastructures.hints.IHintContext;
+import org.simantics.utils.threads.IThreadWorkQueue;
+import org.simantics.utils.threads.SWTThread;
+import org.simantics.utils.threads.ThreadUtils;
+import org.simantics.utils.ui.ISelectionUtils;
+import org.simantics.utils.ui.jface.BasePostSelectionProvider;
+import org.simantics.utils.ui.widgets.VetoingEventHandler;
+import org.simantics.utils.ui.workbench.WorkbenchUtils;
+
+import gnu.trove.map.hash.THashMap;
+import gnu.trove.procedure.TObjectProcedure;
+import gnu.trove.set.hash.THashSet;
+
+/**
+ * @see #getMaxChildren()
+ * @see #setMaxChildren(int)
+ * @see #getMaxChildren(NodeQueryManager, NodeContext)
+ */
+class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
+
+       private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
+               
+               private GraphExplorerImpl ge;
+               
+               GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
+                       this.ge = ge;
+               }
+               
+               void dispose() {
+                       ge = null;
+               }
+               
+           @Override
+           public void setSelection(final ISelection selection) {
+               if(ge == null) return;
+               ge.setSelection(selection, false);
+           }
+           
+
+           @Override
+           public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+               if(ge == null) return;
+               if(ge.isDisposed()) {
+                   if (DEBUG_SELECTION_LISTENERS)
+                       System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);
+                   return;
+               }
+               //assertNotDisposed();
+               //System.out.println("Remove selection changed listener: " + listener);
+               ge.selectionProvider.removeSelectionChangedListener(listener);
+           }
+           
+           @Override
+           public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
+               if(ge == null) return;
+               if (!ge.thread.currentThreadAccess())
+                   throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
+               if(ge.isDisposed()) {
+                   System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);
+                   return;
+               }
+               //System.out.println("Add POST selection changed listener: " + listener);
+               ge.selectionProvider.addPostSelectionChangedListener(listener);
+           }
+
+           @Override
+           public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
+               if(ge == null) return;
+               if(ge.isDisposed()) {
+                   if (DEBUG_SELECTION_LISTENERS)
+                       System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);
+                   return;
+               }
+//             assertNotDisposed();
+               //System.out.println("Remove POST selection changed listener: " + listener);
+               ge.selectionProvider.removePostSelectionChangedListener(listener);
+           }
+           
+
+           @Override
+           public void addSelectionChangedListener(ISelectionChangedListener listener) {
+               if(ge == null) return;
+               if (!ge.thread.currentThreadAccess())
+                   throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
+               //System.out.println("Add selection changed listener: " + listener);
+               if (ge.tree.isDisposed() || ge.selectionProvider == null) {
+                   System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);
+                   return;
+               }
+
+               ge.selectionProvider.addSelectionChangedListener(listener);
+           }
+
+           
+           @Override
+           public ISelection getSelection() {
+               if(ge == null) return StructuredSelection.EMPTY;
+               if (!ge.thread.currentThreadAccess())
+                   throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());
+               if (ge.tree.isDisposed() || ge.selectionProvider == null)
+                   return StructuredSelection.EMPTY;
+               return ge.selectionProvider.getSelection();
+           }
+           
+       }
+       
+    /**
+     * If this explorer is running with an Eclipse workbench open, this
+     * Workbench UI context will be activated whenever inline editing is started
+     * through {@link #startEditing(TreeItem, int)} and deactivated when inline
+     * editing finishes.
+     * 
+     * This context information can be used to for UI handler activity testing.
+     */
+    private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
+
+    private static final String KEY_DRAG_COLUMN = "dragColumn";
+
+    private static final boolean                   DEBUG_SELECTION_LISTENERS = false;
+
+    private static final int                       DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
+
+    public static final int                        DEFAULT_MAX_CHILDREN                    = 1000;
+
+    private static final long                      POST_SELECTION_DELAY                    = 300;
+
+    /**
+     * The time in milliseconds that must elapse between consecutive
+     * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)}
+     * invocations in order for this class to construct a new selection.
+     * 
+     * <p>
+     * This is done because selection construction can be very expensive as the
+     * selected set grows larger when the user is pressing shift+arrow keys.
+     * GraphExplorerImpl will naturally listen to all changes in the tree
+     * selection, but as an optimization will not construct new
+     * StructuredSelection instances for every selection change event. A new
+     * selection will be constructed and set only if the selection hasn't
+     * changed for the amount of milliseconds specified by this constant.
+     */
+    private static final long                      SELECTION_CHANGE_QUIET_TIME             = 150;
+
+    private final IThreadWorkQueue                 thread;
+
+    /**
+     * Local method for checking from whether resources are loaded in
+     * JFaceResources.
+     */
+    private final LocalResourceManager             localResourceManager;
+
+    /**
+     * Local device resource manager that is safe to use in
+     * {@link ImageLoaderJob} for creating images in a non-UI thread.
+     */
+    private final ResourceManager                  resourceManager;
+
+    /*
+     * Package visibility.
+     * TODO: Get rid of these.
+     */
+    Tree                                           tree;
+
+    @SuppressWarnings({ "rawtypes" })
+    final HashMap<CacheKey<?>, NodeQueryProcessor> processors            = new HashMap<CacheKey<?>, NodeQueryProcessor>();
+    @SuppressWarnings({ "rawtypes" })
+    final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors   = new HashMap<Object, PrimitiveQueryProcessor>();
+    @SuppressWarnings({ "rawtypes" })
+    final HashMap<Class, DataSource>               dataSources           = new HashMap<Class, DataSource>();
+    
+    class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
+        // This is for query debugging only.
+        int                  queryIndent   = 0;
+
+        GECache              cache         = new GECache();
+        AtomicBoolean        propagating   = new AtomicBoolean(false);
+        Object               propagateList = new Object();
+        Object               propagate     = new Object();
+        List<Runnable>       scheduleList  = new ArrayList<Runnable>();
+        final Deque<Integer> activity      = new LinkedList<Integer>();
+        int                  activityInt   = 0;
+
+        /**
+         * Stores the currently running query update runnable. If
+         * <code>null</code> there's nothing scheduled yet in which case
+         * scheduling can commence. Otherwise the update should be skipped.
+         */
+        AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<Runnable>();
+
+        /**
+         * Keeps track of nodes that have already been auto-expanded. After
+         * being inserted into this set, nodes will not be forced to stay in an
+         * expanded state after that. This makes it possible for the user to
+         * close auto-expanded nodes.
+         */
+        Map<NodeContext, Boolean>     autoExpanded  = new WeakHashMap<NodeContext, Boolean>();
+
+        
+        @Override
+        protected void doDispose() {
+               saveState();
+            autoExpanded.clear();
+        }
+
+        @Override
+        public IGECache getCache() {
+            return cache;
+        }
+
+        @Override
+        public int queryIndent() {
+            return queryIndent;
+        }
+
+        @Override
+        public int queryIndent(int offset) {
+            queryIndent += offset;
+            return queryIndent;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> NodeQueryProcessor<T> getProcessor(Object o) {
+            return processors.get(o);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(Object o) {
+            return primitiveProcessors.get(o);
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public <T> DataSource<T> getDataSource(Class<T> clazz) {
+            return dataSources.get(clazz);
+        }
+
+        @Override
+        public void update(UIElementReference ref) {
+            //System.out.println("GE.update " + ref);
+            TreeItemReference tiref = (TreeItemReference) ref;
+            TreeItem item = tiref.getItem();
+            // NOTE: must be called regardless of the the item value.
+            // A null item is currently used to indicate a tree root update.
+            GraphExplorerImpl.this.update(item);
+        }
+
+        @Override
+        public Object getPropagateLock() {
+            return propagate;
+        }
+
+        @Override
+        public Object getPropagateListLock() {
+            return propagateList;
+        }
+
+        @Override
+        public boolean isPropagating() {
+            return propagating.get();
+        }
+
+        @Override
+        public void setPropagating(boolean b) {
+            this.propagating.set(b);
+        }
+
+        @Override
+        public List<Runnable> getScheduleList() {
+            return scheduleList;
+        }
+
+        @Override
+        public void setScheduleList(List<Runnable> list) {
+            this.scheduleList = list;
+        }
+
+        @Override
+        public Deque<Integer> getActivity() {
+            return activity;
+        }
+
+        @Override
+        public void setActivityInt(int i) {
+            this.activityInt = i;
+        }
+
+        @Override
+        public int getActivityInt() {
+            return activityInt;
+        }
+
+        @Override
+        public void scheduleQueryUpdate(Runnable r) {
+            if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
+                return;
+            //System.out.println("Scheduling query update for runnable " + r);
+            if (currentQueryUpdater.compareAndSet(null, r)) {
+                //System.out.println("Scheduling query update for runnable " + r);
+                queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);
+            }
+        }
+
+        Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
+            @Override
+            public void run() {
+                Runnable r = currentQueryUpdater.getAndSet(null);
+                if (r != null) {
+                    //System.out.println("Running query update runnable " + r);
+                    r.run();
+                }
+            }
+        };
+    }
+
+    GraphExplorerContext                         explorerContext     = new GraphExplorerContext();
+
+    HashSet<TreeItem>                            pendingItems        = new HashSet<TreeItem>();
+    boolean                                      updating            = false;
+    boolean                                      pendingRoot         = false;
+
+    @SuppressWarnings("deprecation")
+    ModificationContext                          modificationContext = null;
+
+    NodeContext                                  rootContext;
+
+    StatePersistor                               persistor           = null;
+
+    boolean                                      editable            = true;
+
+    /**
+     * This is a reverse mapping from {@link NodeContext} tree objects back to
+     * their owner TreeItems.
+     * 
+     * <p>
+     * Access this map only in the SWT thread to keep it thread-safe.
+     * </p>
+     */
+    BijectionMap<NodeContext, TreeItem>         contextToItem     = new BijectionMap<NodeContext, TreeItem>();
+
+    /**
+     * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
+     * initialize.
+     */
+    Column[]                                     columns           = new Column[0];
+    Map<String, Integer>                         columnKeyToIndex  = new HashMap<String, Integer>();
+    boolean                                      refreshingColumnSizes = false;
+    boolean                                      columnsAreVisible = true;
+
+    /**
+     * An array reused for invoking {@link TreeItem#setImage(Image[])} instead
+     * of constantly allocating new arrays for setting each TreeItems images.
+     * This works because {@link TreeItem#setImage(Image[])} does not take hold
+     * of the array itself, only the contents of the array.
+     * 
+     * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
+     */
+    Image[]                                      columnImageArray = { null };
+
+    /**
+     * Used for collecting Image or ImageDescriptor instances for a single
+     * TreeItem when initially setting images for a TreeItem.
+     * 
+     * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
+     */
+    Object[]                                     columnDescOrImageArray = { null };
+
+    final ExecutorService                        queryUpdateScheduler = Threads.getExecutor();
+    final ScheduledExecutorService               uiUpdateScheduler    = ThreadUtils.getNonBlockingWorkExecutor();
+
+    /** Set to true when the Tree widget is disposed. */
+    private boolean                              disposed                 = false;
+    private final CopyOnWriteArrayList<FocusListener>  focusListeners           = new CopyOnWriteArrayList<FocusListener>();
+    private final CopyOnWriteArrayList<MouseListener>  mouseListeners           = new CopyOnWriteArrayList<MouseListener>();
+    private final CopyOnWriteArrayList<KeyListener>    keyListeners             = new CopyOnWriteArrayList<KeyListener>();
+
+    /** Selection provider */
+    private   GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
+    protected BasePostSelectionProvider          selectionProvider        = new BasePostSelectionProvider();
+    protected SelectionDataResolver              selectionDataResolver;
+    protected SelectionFilter                    selectionFilter;
+    protected BinaryFunction<Object[], GraphExplorer, Object[]>  selectionTransformation = new BinaryFunction<Object[], GraphExplorer, Object[]>() {
+
+        @Override
+        public Object[] call(GraphExplorer explorer, Object[] objects) {
+            Object[] result = new Object[objects.length];
+            for (int i = 0; i < objects.length; i++) {
+                IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
+                context.setHint(SelectionHints.KEY_MAIN, objects[i]);
+                result[i] = context;
+            }
+            return result;
+        }
+
+    };
+    protected FontDescriptor                     originalFont;
+    protected ColorDescriptor                    originalForeground;
+    protected ColorDescriptor                    originalBackground;
+
+    /**
+     * The set of currently selected TreeItem instances. This set is needed
+     * because we need to know in {@link #setData(Event)} whether the updated
+     * item was a part of the current selection in which case the selection must
+     * be updated.
+     */
+    private final Map<TreeItem, NodeContext>     selectedItems            = new HashMap<TreeItem, NodeContext>();
+
+    /**
+     * TODO: specify what this is for
+     */
+    private final Set<NodeContext>               selectionRefreshContexts = new HashSet<NodeContext>();
+
+    /**
+     * If this field is non-null, it means that if {@link #setData(Event)}
+     * encounters a NodeContext equal to this one, it must make the TreeItem
+     * assigned to that NodeContext the topmost item of the tree using
+     * {@link Tree#setTopItem(TreeItem)}. After this the field value is
+     * nullified.
+     * 
+     * <p>
+     * This is related to {@link #initializeState()}, i.e. explorer state
+     * restoration.
+     */
+//    private NodeContext[] topNodePath = NodeContext.NONE;
+//    private int[] topNodePath = {};
+//    private int currentTopNodePathIndex = -1;
+
+    /**
+     * See {@link #setAutoExpandLevel(int)}
+     */
+    private int autoExpandLevel = 0;
+
+    /**
+     * <code>null</code> if not explicitly set through
+     * {@link #setServiceLocator(IServiceLocator)}.
+     */
+    private IServiceLocator serviceLocator;
+
+    /**
+     * The global workbench context service, if the workbench is available.
+     * Retrieved in the constructor.
+     */
+    private IContextService contextService = null;
+
+    /**
+     * The global workbench IFocusService, if the workbench is available.
+     * Retrieved in the constructor.
+     */
+    private IFocusService focusService = null;
+
+    /**
+     * A Workbench UI context activation that is activated when starting inline
+     * editing through {@link #startEditing(TreeItem, int)}.
+     * 
+     * @see #activateEditingContext()
+     * @see #deactivateEditingContext()
+     */
+    private IContextActivation editingContext = null;
+
+    static class ImageTask {
+        NodeContext node;
+        TreeItem item;
+        Object[] descsOrImages;
+        public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
+            this.node = node;
+            this.item = item;
+            this.descsOrImages = descsOrImages;
+        }
+    }
+
+    /**
+     * The job that is used for off-loading image loading tasks (see
+     * {@link ImageTask} to a worker thread from the main UI thread.
+     * 
+     * @see #setPendingImages(IProgressMonitor)
+     */
+    ImageLoaderJob           imageLoaderJob;
+
+    /**
+     * The set of currently gathered up image loading tasks for
+     * {@link #imageLoaderJob} to execute.
+     * 
+     * @see #setPendingImages(IProgressMonitor)
+     */
+    Map<TreeItem, ImageTask> imageTasks     = new THashMap<TreeItem, ImageTask>();
+
+    /**
+     * A state flag indicating whether the vertical scroll bar was visible for
+     * {@link #tree} the last time it was checked. Since there is no listener
+     * that can provide this information, we check it in {@link #setData(Event)}
+     * every time any data for a TreeItem is updated. If the visibility changes,
+     * we will force re-layouting of the tree's parent composite.
+     * 
+     * @see #setData(Event)
+     */
+    private boolean verticalBarVisible = false;
+
+    static class TransientStateImpl implements TransientExplorerState {
+
+       private Integer activeColumn = null;
+       
+               @Override
+               public synchronized Integer getActiveColumn() {
+                       return activeColumn;
+               }
+               
+               public synchronized void setActiveColumn(Integer column) {
+                       activeColumn = column;
+               }
+       
+    }
+    
+    private TransientStateImpl transientState = new TransientStateImpl();
+    
+    boolean scheduleUpdater() {
+
+       if (tree.isDisposed())
+            return false;
+
+        if (pendingRoot == true || !pendingItems.isEmpty()) {
+            assert(!tree.isDisposed());
+
+            int activity = explorerContext.activityInt;
+            long delay = 30;
+            if (activity < 100) {
+//                System.out.println("Scheduling update immediately.");
+            } else if (activity < 1000) {
+//                System.out.println("Scheduling update after 500ms.");
+                delay = 500;
+            } else {
+//                System.out.println("Scheduling update after 3000ms.");
+                delay = 3000;
+            }
+
+            updateCounter = 0;
+            
+            //System.out.println("Scheduling UI update after " + delay + " ms.");
+            uiUpdateScheduler.schedule(new Runnable() {
+                @Override
+                public void run() {
+                       
+                    if (tree.isDisposed())
+                        return;
+                    
+                    if (updateCounter > 0) {
+                       updateCounter = 0;
+                       uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
+                    } else {
+                       tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
+                    }
+                    
+                }
+            }, delay, TimeUnit.MILLISECONDS);
+
+            updating = true;
+            return true;
+        }
+
+        return false;
+    }
+
+    int updateCounter = 0;
+    
+    void update(TreeItem item) {
+
+        synchronized(pendingItems) {
+               
+//             System.out.println("update " + item);
+               
+               updateCounter++;
+
+            if(item == null) pendingRoot = true;
+            else pendingItems.add(item);
+
+            if(updating == true) return;
+
+            scheduleUpdater();
+
+        }
+
+    }
+
+    private int maxChildren = DEFAULT_MAX_CHILDREN;
+
+    @Override
+    public int getMaxChildren() {
+        return maxChildren;
+    }
+
+    @Override
+    public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
+        Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
+        //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
+        if (result != null) {
+            if (result < 0)
+                throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
+            return result;
+        }
+        return maxChildren;
+    }
+
+    @Override
+    public void setMaxChildren(int maxChildren) {
+        this.maxChildren = maxChildren;
+    }
+
+    @Override
+    public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
+        this.modificationContext = modificationContext;
+    }
+
+    /**
+     * @param parent the parent SWT composite
+     */
+    public GraphExplorerImpl(Composite parent) {
+        this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+    }
+
+    /**
+     * Stores the node context and the modifier that is currently being
+     * modified. These are used internally to prevent duplicate edits from being
+     * initiated which should always be a sensible thing to do.
+     */
+    private Set<NodeContext> currentlyModifiedNodes   = new THashSet<NodeContext>();
+
+    private final TreeEditor editor;
+    private Color            invalidModificationColor = null;
+
+    /**
+     * @param item the TreeItem to start editing
+     * @param columnIndex the index of the column to edit, starts counting from
+     *        0
+     * @return <code>true</code> if the editing was initiated successfully or
+     *         <code>false</code> if editing could not be started due to lack of
+     *         {@link Modifier} for the labeler in question.
+     */
+    private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
+        if (!editable)
+            return "Rename not supported for selection";
+
+        GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
+        final NodeContext context = (NodeContext) item.getData();
+        Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
+        if (labeler == null)
+            return "Rename not supported for selection";
+
+        if(columnKey == null) columnKey = columns[columnIndex].getKey();
+
+        // columnKey might be prefixed with '#' to indicate
+        // textual editing is preferred. Try to get modifier
+        // for that first and only if it fails, try without
+        // the '#' prefix.
+        Modifier modifier = labeler.getModifier(modificationContext, columnKey);
+        if (modifier == null) {
+            if(columnKey.startsWith("#"))
+                modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
+            if (modifier == null)
+                return "Rename not supported for selection";
+        }
+        if (modifier instanceof DeniedModifier) {
+               DeniedModifier dm = (DeniedModifier)modifier;
+               return dm.getMessage();
+        }
+
+        // Prevent editing of a single node context multiple times.
+        if (currentlyModifiedNodes.contains(context)) {
+            //System.out.println("discarding duplicate edit for context " + context);
+            return "Rename not supported for selection";
+        }
+
+        // Clean up any previous editor control
+        Control oldEditor = editor.getEditor();
+        if (oldEditor != null)
+            oldEditor.dispose();
+
+        if (modifier instanceof DialogModifier) {
+            performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
+        } else if (modifier instanceof CustomModifier) {
+            startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
+        } else if (modifier instanceof EnumerationModifier) {
+            startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
+        } else {
+            startTextEditing(item, columnIndex, context, modifier);
+        }
+
+        return null;
+    }
+
+    /**
+     * @param item
+     * @param columnIndex
+     * @param context
+     * @param modifier
+     */
+    void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
+            final DialogModifier modifier) {
+        final AtomicBoolean disposed = new AtomicBoolean(false);
+        Consumer<String> callback = result -> {
+            if (disposed.get())
+                return;
+            String error = modifier.isValid(result);
+            if (error == null) {
+                modifier.modify(result);
+                // Item may be disposed if the tree gets reset after a previous editing.
+                if (!item.isDisposed()) {
+                    item.setText(columnIndex, result);
+                    queueSelectionRefresh(context);
+                }
+            }
+        };
+
+        currentlyModifiedNodes.add(context);
+        try {
+            String status = modifier.query(tree, item, columnIndex, context, callback);
+            if (status != null)
+                ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
+        } finally {
+            currentlyModifiedNodes.remove(context);
+            disposed.set(true);
+        }
+    }
+
+    private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
+        Point size = control.computeSize(widthHint, heightHint);
+        editor.horizontalAlignment = SWT.LEFT;
+        Rectangle itemRect = item.getBounds(columnIndex),
+                  rect = tree.getClientArea();
+        editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
+        int left = itemRect.x,
+            right = rect.x + rect.width;
+        editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
+        editor.minimumHeight = size.y + insetY * 2;
+        editor.layout();
+    }
+
+    void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
+        GC gc = new GC(control);
+        Point size = gc.textExtent(text);
+        gc.dispose();
+        reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
+    }
+
+    /**
+     * @param item
+     * @param columnIndex
+     * @param context
+     * @param modifier
+     */
+    void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
+            final CustomModifier modifier) {
+        final Object obj = modifier.createControl(tree, item, columnIndex, context);
+        if (!(obj instanceof Control))
+            throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
+        final Control control = (Control) obj;
+
+//        final int insetX = 0;
+//        final int insetY = 0;
+//        control.addListener(SWT.Resize, new Listener() {
+//            @Override
+//            public void handleEvent(Event e) {
+//                Rectangle rect = control.getBounds();
+//                control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
+//            }
+//        });
+        control.addListener(SWT.Dispose, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                currentlyModifiedNodes.remove(context);
+                queueSelectionRefresh(context);
+                deactivateEditingContext();
+            }
+        });
+        
+        if (!(control instanceof Shell)) {
+            editor.setEditor(control, item, columnIndex);
+        }
+        
+
+        control.setFocus();
+
+        GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
+
+        activateEditingContext(control);
+
+        // Removed in disposeListener above
+        currentlyModifiedNodes.add(context);
+        //System.out.println("START CUSTOM EDITING: " + item);
+    }
+
+    /**
+     * @param item
+     * @param columnIndex
+     * @param context
+     * @param modifier
+     */
+    void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
+        String initialText = modifier.getValue();
+        if (initialText == null)
+            throw new AssertionError("Labeler.Modifier.getValue() returned null");
+
+        List<String> values = modifier.getValues();
+        String selectedValue = modifier.getValue();
+        int selectedIndex = values.indexOf(selectedValue);
+        if (selectedIndex == -1)
+            throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
+
+        final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
+        combo.setVisibleItemCount(10);
+        //combo.setEditable(false);
+
+        for (String value : values) {
+            combo.add(value);
+        }
+        combo.select(selectedIndex);
+
+        Listener comboListener = new Listener() {
+            boolean arrowTraverseUsed = false; 
+            @Override
+            public void handleEvent(final Event e) {
+                //System.out.println("FOO: " + e);
+                switch (e.type) {
+                    case SWT.KeyDown:
+                        if (e.character == SWT.CR) {
+                            // Commit edit directly on ENTER press.
+                            String text = combo.getText();
+                            modifier.modify(text);
+                            // Item may be disposed if the tree gets reset after a previous editing.
+                            if (!item.isDisposed()) {
+                                item.setText(columnIndex, text);
+                                queueSelectionRefresh(context);
+                            }
+                            combo.dispose();
+                            e.doit = false;
+                        } else if (e.keyCode == SWT.ESC) {
+                            // Cancel editing immediately
+                            combo.dispose();
+                            e.doit = false;
+                        }
+                        break;
+                    case SWT.Selection:
+                    {
+                        if (arrowTraverseUsed) {
+                            arrowTraverseUsed = false;
+                            return;
+                        }
+
+                        String text = combo.getText();
+                        modifier.modify(text);
+
+                        // Item may be disposed if the tree gets reset after a previous editing.
+                        if (!item.isDisposed()) {
+                            item.setText(columnIndex, text);
+                            queueSelectionRefresh(context);
+                        }
+                        combo.dispose();
+                        break;
+                    }
+                    case SWT.FocusOut: {
+                        String text = combo.getText();
+                        modifier.modify(text);
+
+                        // Item may be disposed if the tree gets reset after a previous editing.
+                        if (!item.isDisposed()) {
+                            item.setText(columnIndex, text);
+                            queueSelectionRefresh(context);
+                        }
+                        combo.dispose();
+                        break;
+                    }
+                    case SWT.Traverse: {
+                        switch (e.detail) {
+                            case SWT.TRAVERSE_RETURN:
+                                String text = combo.getText();
+                                modifier.modify(text);
+                                if (!item.isDisposed()) {
+                                    item.setText(columnIndex, text);
+                                    queueSelectionRefresh(context);
+                                }
+                                arrowTraverseUsed = false;
+                                // FALL THROUGH
+                            case SWT.TRAVERSE_ESCAPE:
+                                combo.dispose();
+                                e.doit = false;
+                                break;
+                            case SWT.TRAVERSE_ARROW_NEXT:
+                            case SWT.TRAVERSE_ARROW_PREVIOUS:
+                                arrowTraverseUsed = true;
+                                break;
+                            default:
+                                //System.out.println("unhandled traversal: " + e.detail);
+                                break;
+                        }
+                        break;
+                    }
+                    case SWT.Dispose:
+                        currentlyModifiedNodes.remove(context);
+                        deactivateEditingContext();
+                        break;
+                }
+            }
+        };
+        combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
+        combo.addListener(SWT.KeyDown, comboListener);
+        combo.addListener(SWT.FocusOut, comboListener);
+        combo.addListener(SWT.Traverse, comboListener);
+        combo.addListener(SWT.Selection, comboListener);
+        combo.addListener(SWT.Dispose, comboListener);
+
+        editor.setEditor(combo, item, columnIndex);
+
+        combo.setFocus();
+        combo.setListVisible(true);
+
+        GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
+
+        activateEditingContext(combo);
+
+        // Removed in comboListener
+        currentlyModifiedNodes.add(context);
+
+        //System.out.println("START ENUMERATION EDITING: " + item);
+    }
+
+    /**
+     * @param item
+     * @param columnIndex
+     * @param context
+     * @param modifier
+     */
+    void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {
+        String initialText = modifier.getValue();
+        if (initialText == null)
+            throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);
+
+        final Composite composite = new Composite(tree, SWT.NONE);
+        //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));
+        final Text text = new Text(composite, SWT.BORDER);
+        final int insetX = 0;
+        final int insetY = 0;
+        composite.addListener(SWT.Resize, new Listener() {
+            @Override
+            public void handleEvent(Event e) {
+                Rectangle rect = composite.getClientArea();
+                text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height
+                        - insetY * 2);
+            }
+        });
+        final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
+        Listener textListener = new Listener() {
+               
+               boolean modified = false;
+               
+            @Override
+            public void handleEvent(final Event e) {
+                String error;
+                String newText;
+                switch (e.type) {
+                    case SWT.FocusOut:
+                       if(modified) {
+                               //System.out.println("FOCUS OUT " + item);
+                               newText = text.getText();
+                               error = modifier.isValid(newText);
+                               if (error == null) {
+                                       modifier.modify(newText);
+
+                                       // Item may be disposed if the tree gets reset after a previous editing.
+                                       if (!item.isDisposed()) {
+                                               item.setText(columnIndex, newText);
+                                               queueSelectionRefresh(context);
+                                       }
+                               } else {
+                                       //                                System.out.println("validation error: " + error);
+                               }
+                       }
+                        composite.dispose();
+                        break;
+                    case SWT.Modify:
+                        newText = text.getText();
+                        error = modifier.isValid(newText);
+                        if (error != null) {
+                            text.setBackground(invalidModificationColor);
+                            errorStatus(error);
+                            //System.out.println("validation error: " + error);
+                        } else {
+                            text.setBackground(null);
+                            errorStatus(null);
+                        }
+                        modified = true;
+                        break;
+                    case SWT.Verify:
+                       
+                        // Safety check since it seems that this may happen with
+                        // virtual trees.
+                        if (item.isDisposed())
+                            return;
+
+                        // Filter input if necessary
+                        e.text = filter != null ? filter.filter(e.text) : e.text;
+
+                        newText = text.getText();
+                        String leftText = newText.substring(0, e.start);
+                        String rightText = newText.substring(e.end, newText.length());
+                        GraphExplorerImpl.this.reconfigureTreeEditorForText(
+                                item, columnIndex, text, leftText + e.text + rightText,
+                                SWT.DEFAULT, insetX, insetY);
+                        break;
+                    case SWT.Traverse:
+                        switch (e.detail) {
+                            case SWT.TRAVERSE_RETURN:
+                               if(modified) {
+                                       newText = text.getText();
+                                       error = modifier.isValid(newText);
+                                       if (error == null) {
+                                               modifier.modify(newText);
+                                               if (!item.isDisposed()) {
+                                                       item.setText(columnIndex, newText);
+                                                       queueSelectionRefresh(context);
+                                               }
+                                       }
+                               }
+                                // FALL THROUGH
+                            case SWT.TRAVERSE_ESCAPE:
+                                composite.dispose();
+                                e.doit = false;
+                                break;
+                            default:
+                                //System.out.println("unhandled traversal: " + e.detail);
+                                break;
+                        }
+                        break;
+
+                    case SWT.Dispose:
+                        currentlyModifiedNodes.remove(context);
+                        deactivateEditingContext();
+                        errorStatus(null);
+                        break;
+                }
+            }
+        };
+        // Set the initial text before registering a listener. We do not want immediate modification!
+        text.setText(initialText);
+        text.addListener(SWT.FocusOut, textListener);
+        text.addListener(SWT.Traverse, textListener);
+        text.addListener(SWT.Verify, textListener);
+        text.addListener(SWT.Modify, textListener);
+        text.addListener(SWT.Dispose, textListener);
+        editor.setEditor(composite, item, columnIndex);
+        text.selectAll();
+        text.setFocus();
+
+        // Initialize TreeEditor properly.
+        GraphExplorerImpl.this.reconfigureTreeEditorForText(
+                item, columnIndex, text, initialText,
+                SWT.DEFAULT, insetX, insetY);
+
+        // Removed in textListener
+        currentlyModifiedNodes.add(context);
+
+        activateEditingContext(text);
+
+        //System.out.println("START TEXT EDITING: " + item);
+    }
+
+    protected void errorStatus(String error) {
+        IStatusLineManager status = getStatusLineManager();
+        if (status != null) {
+            status.setErrorMessage(error);
+        }
+    }
+
+    protected IStatusLineManager getStatusLineManager() {
+        if (serviceLocator instanceof IWorkbenchPart) {
+            return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);
+        } else if (serviceLocator instanceof IWorkbenchSite) {
+            return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);
+        }
+        return null;
+    }
+
+    protected void activateEditingContext(Control control) {
+        if (contextService != null) {
+            editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
+        }
+        if (control != null && focusService != null) {
+            focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);
+            // No need to remove the control, it will be
+            // removed automatically when it is disposed.
+        }
+    }
+
+    protected void deactivateEditingContext() {
+        IContextActivation a = editingContext;
+        if (a != null) {
+            editingContext = null;
+            contextService.deactivateContext(a);
+        }
+    }
+
+    /**
+     * @param forContext
+     */
+    void queueSelectionRefresh(NodeContext forContext) {
+        selectionRefreshContexts.add(forContext);
+    }
+
+    @Override
+    public String startEditing(NodeContext context, String columnKey_) {
+        assertNotDisposed();
+        if (!thread.currentThreadAccess())
+            throw new IllegalStateException("not in SWT display thread " + thread.getThread());
+
+        String columnKey = columnKey_;
+        if(columnKey.startsWith("#")) {
+               columnKey = columnKey.substring(1);
+        }
+        
+        Integer columnIndex = columnKeyToIndex.get(columnKey);
+        if (columnIndex == null)
+            return "Rename not supported for selection";
+
+        TreeItem item = contextToItem.getRight(context);
+        if (item == null)
+            return "Rename not supported for selection";
+
+        return startEditing(item, columnIndex, columnKey_);
+        
+    }
+
+    @Override
+    public String startEditing(String columnKey) {
+
+        ISelection selection = postSelectionProvider.getSelection();
+        if(selection == null) return "Rename not supported for selection";
+        NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
+        if(context == null) return "Rename not supported for selection";
+
+        return startEditing(context, columnKey);
+
+    }
+
+    /**
+     * @param site <code>null</code> if the explorer is detached from the workbench
+     * @param parent the parent SWT composite
+     * @param style the tree style to use, check the see tags for the available flags
+     * 
+     * @see SWT#SINGLE
+     * @see SWT#MULTI
+     * @see SWT#CHECK
+     * @see SWT#FULL_SELECTION
+     * @see SWT#NO_SCROLL
+     * @see SWT#H_SCROLL
+     * @see SWT#V_SCROLL
+     */
+    public GraphExplorerImpl(Composite parent, int style) {
+
+        setServiceLocator(null);
+
+        this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
+        this.resourceManager = new DeviceResourceManager(parent.getDisplay());
+
+        this.imageLoaderJob = new ImageLoaderJob(this);
+        this.imageLoaderJob.setPriority(Job.DECORATE);
+
+        invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
+
+        this.thread = SWTThread.getThreadAccess(parent);
+
+        for(int i=0;i<10;i++) explorerContext.activity.push(0);
+
+        tree = new Tree(parent, style);
+        tree.addListener(SWT.SetData, this);
+        tree.addListener(SWT.Expand, this);
+        tree.addListener(SWT.Dispose, this);
+        tree.addListener(SWT.Activate, this);
+
+        tree.setData(KEY_GRAPH_EXPLORER, this);
+
+        // These are both required for performing column resizing without flicker.
+        // See SWT.Resize event handling in #handleEvent() for more explanations.
+        parent.addListener(SWT.Resize, this);
+        tree.addListener(SWT.Resize, this);
+
+        originalFont = JFaceResources.getDefaultFontDescriptor();
+//        originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
+//        originalForeground = tree.getForeground();
+
+        tree.setFont((Font) localResourceManager.get(originalFont));
+
+        columns = new Column[] { new Column(ColumnKeys.SINGLE) };
+        columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
+
+        editor = new TreeEditor(tree);
+        editor.horizontalAlignment = SWT.LEFT;
+        editor.grabHorizontal = true;
+        editor.minimumWidth = 50;
+
+        setBasicListeners();
+        setDefaultProcessors();
+        
+        this.toolTip = new GraphExplorerToolTip(explorerContext, tree);
+    }
+
+    @Override
+    public IThreadWorkQueue getThread() {
+        return thread;
+    }
+
+    TreeItem previousSingleSelection = null;
+    long focusGainedAt = Long.MIN_VALUE;
+
+    protected GraphExplorerToolTip toolTip;
+
+    protected void setBasicListeners() {
+        // Keep track of the previous single selection to help
+        // decide whether to start editing a tree node on mouse
+        // downs or not.
+        tree.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                TreeItem[] selection = tree.getSelection();
+                if (selection.length == 1) {
+                    //for (TreeItem item : selection)
+                    //    System.out.println("selection: " + item);
+                    previousSingleSelection = selection[0];
+                } else {
+                    previousSingleSelection = null;
+                }
+            }
+        });
+
+        // Try to start editing of tree column when clicked for the second time.
+        Listener mouseEditListener = new Listener() {
+
+            Future<?> startEdit = null;
+
+            @Override
+            public void handleEvent(Event event) {
+                if (event.type == SWT.DragDetect) {
+                    // Needed to prevent editing from being started when in fact
+                    // the user starts dragging an item.
+                    //System.out.println("DRAG DETECT: " + event);
+                    cancelEdit();
+                    return;
+                }
+                //System.out.println("mouse down: " + event);
+                if (event.button == 1) {
+                    // Always ignore the first mouse button press that focuses
+                    // the control. Do not let it start in-line editing since
+                    // that is very annoying to users and not how the UI's that
+                    // people are used to behave.
+                    long eventTime = ((long) event.time) & 0xFFFFFFFFL;
+                    if ((eventTime - focusGainedAt) < 250L) {
+                        //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));
+                        return;
+                    }
+                    //System.out.println("testing whether to start editing");
+
+                    final Point point = new Point(event.x, event.y);
+                    final TreeItem item = tree.getItem(point);
+                    if (item == null)
+                        return;
+                    //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
+
+                    // Only start editing if the item was already selected.
+                    if (!item.equals(previousSingleSelection)) {
+                        cancelEdit();
+                        return;
+                    }
+
+                    if (tree.getColumnCount() > 1) {
+                        // TODO: reconsider this logic, might not be good in general.
+                        for (int i = 0; i < tree.getColumnCount(); i++) {
+                            if (item.getBounds(i).contains(point)) {
+                                tryScheduleEdit(event, item, point, 100, i);
+                                return;
+                            }
+                        }
+                    } else {
+                        //System.out.println("clicks: " + event.count);
+                        if (item.getBounds().contains(point)) {
+                            if (event.count == 1) {
+                                tryScheduleEdit(event, item, point, 500, 0);
+                            } else {
+                                cancelEdit();
+                            }
+                        }
+                    }
+                }
+            }
+
+            void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {
+                //System.out.println("\tCONTAINS: " + item);
+                if (!cancelEdit())
+                    return;
+
+                //System.out.println("\tScheduling edit: " + item);
+                startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
+                    @Override
+                    public void run() {
+                        ThreadUtils.asyncExec(thread, new Runnable() {
+                            @Override
+                            public void run() {
+                                if (item.isDisposed())
+                                    return;
+                                startEditing(item, column, null);
+                            }
+                        });
+                    }
+                }, delayMs, TimeUnit.MILLISECONDS);
+            }
+
+            boolean cancelEdit() {
+                Future<?> f = startEdit;
+                if (f != null) {
+                    // Try to cancel the start edit task if it's not running yet.
+                    startEdit = null;
+                    if (!f.isDone()) {
+                        boolean ret = f.cancel(false);
+                        //System.out.println("\tCancelled edit: " + ret);
+                        return ret;
+                    }
+                }
+                //System.out.println("\tNo edit in progress to cancel");
+                return true;
+            }
+        };
+        tree.addListener(SWT.MouseDown, mouseEditListener);
+        tree.addListener(SWT.DragDetect, mouseEditListener);
+        tree.addListener(SWT.DragDetect, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                Point test = new Point(event.x, event.y);
+                TreeItem item = tree.getItem(test);
+                if(item != null) {
+                    for(int i=0;i<tree.getColumnCount();i++) {
+                        Rectangle rect = item.getBounds(i);
+                        if(rect.contains(test)) {
+                            tree.setData(KEY_DRAG_COLUMN, i);
+                            return;
+                        }
+                    }
+                }
+                tree.setData(KEY_DRAG_COLUMN, -1);
+            }
+        });
+        tree.addListener(SWT.MouseMove, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                Point test = new Point(event.x, event.y);
+                TreeItem item = tree.getItem(test);
+                if(item != null) {
+                    for(int i=0;i<tree.getColumnCount();i++) {
+                        Rectangle rect = item.getBounds(i);
+                        if(rect.contains(test)) {
+                               transientState.setActiveColumn(i);
+                            return;
+                        }
+                    }
+                }
+               transientState.setActiveColumn(null);
+            }
+        });
+
+        // Add focus/mouse/key listeners for supporting the respective
+        // add/remove listener methods in IGraphExplorer.
+        tree.addFocusListener(new FocusListener() {
+            @Override
+            public void focusGained(FocusEvent e) {
+                focusGainedAt = ((long) e.time) & 0xFFFFFFFFL;
+                for (FocusListener listener : focusListeners)
+                    listener.focusGained(e);
+            }
+            @Override
+            public void focusLost(FocusEvent e) {
+                for (FocusListener listener : focusListeners)
+                    listener.focusLost(e);
+            }
+        });
+        tree.addMouseListener(new MouseListener() {
+            @Override
+            public void mouseDoubleClick(MouseEvent e) {
+                for (MouseListener listener : mouseListeners) {
+                    listener.mouseDoubleClick(e);
+                }
+            }
+            @Override
+            public void mouseDown(MouseEvent e) {
+                for (MouseListener listener : mouseListeners) {
+                    listener.mouseDown(e);
+                }
+            }
+            @Override
+            public void mouseUp(MouseEvent e) {
+                for (MouseListener listener : mouseListeners) {
+                    listener.mouseUp(e);
+                }
+            }
+        });
+        tree.addKeyListener(new KeyListener() {
+            @Override
+            public void keyPressed(KeyEvent e) {
+                for (KeyListener listener : keyListeners) {
+                    listener.keyPressed(e);
+                }
+            }
+            @Override
+            public void keyReleased(KeyEvent e) {
+                for (KeyListener listener : keyListeners) {
+                    listener.keyReleased(e);
+                }
+            }
+        });
+
+               // Add a tree selection listener for keeping the selection of
+               // GraphExplorer's ISelectionProvider up-to-date.
+        tree.addSelectionListener(new SelectionListener() {
+            @Override
+            public void widgetDefaultSelected(SelectionEvent e) {
+                widgetSelected(e);
+            }
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+               widgetSelectionChanged(false);
+            }
+        });
+
+        // This listener takes care of updating the set of currently selected
+        // TreeItem instances. This set is needed because we need to know in
+        // #setData(Event) whether the updated item was a part of the current
+        // selection in which case the selection must be updated.
+        selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
+            @Override
+            public void selectionChanged(SelectionChangedEvent event) {
+                //System.out.println("selection changed: " + event.getSelection());
+                Set<NodeContext> set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);
+                selectedItems.clear();
+                for (NodeContext nc : set) {
+                    TreeItem item = contextToItem.getRight(nc);
+                    if (item != null)
+                        selectedItems.put(item, nc);
+                }
+                //System.out.println("newly selected items: " + selectedItems);
+            }
+        });
+    }
+
+    /**
+     * Mod count for delaying post selection changed events.
+     */
+    int postSelectionModCount = 0;
+
+    /**
+     * Last tree selection modification time for implementing a quiet
+     * time for selection changes.
+     */
+    long lastSelectionModTime = System.currentTimeMillis() - 10000;
+
+    /**
+     * Current target time for the selection to be set. Calculated
+     * according to the set quiet time and last selection modification
+     * time.
+     */
+    long selectionSetTargetTime = 0;
+
+    /**
+     * <code>true</code> if delayed selection runnable is current scheduled or
+     * running.
+     */
+    boolean delayedSelectionScheduled = false;
+
+    Runnable SELECTION_DELAY = new Runnable() {
+        @Override
+        public void run() {
+            if (tree.isDisposed())
+                return;
+            long now = System.currentTimeMillis();
+            long waitTimeLeft = selectionSetTargetTime - now;
+            if (waitTimeLeft > 0) {
+                // Not enough quiet time, reschedule.
+                delayedSelectionScheduled = true;
+                tree.getDisplay().timerExec((int) waitTimeLeft, this);
+            } else {
+                // Time to perform selection, stop rescheduling.
+                delayedSelectionScheduled = false;
+                resetSelection();
+            }
+        }
+    };
+
+    private void widgetSelectionChanged(boolean forceSelectionChange) {
+        long modTime = System.currentTimeMillis();
+        long delta = modTime - lastSelectionModTime;
+        lastSelectionModTime = modTime;
+        if (!forceSelectionChange && delta < SELECTION_CHANGE_QUIET_TIME) {
+            long msToWait = SELECTION_CHANGE_QUIET_TIME - delta;
+            selectionSetTargetTime = modTime + msToWait;
+            if (!delayedSelectionScheduled) {
+                delayedSelectionScheduled = true;
+                tree.getDisplay().timerExec((int) msToWait, SELECTION_DELAY);
+            }
+            // Make sure that post selection change events do not fire.
+            ++postSelectionModCount;
+            return;
+        }
+
+        // Immediate selection reconstruction.
+        resetSelection();
+    }
+
+    private void resetSelection() {
+        final ISelection selection = getWidgetSelection();
+
+        //System.out.println("resetSelection(" + postSelectionModCount + ")");
+        //System.out.println("    provider selection: " + selectionProvider.getSelection());
+        //System.out.println("    widget selection:   " + selection);
+
+        selectionProvider.setAndFireNonEqualSelection(selection);
+
+        // Implement deferred firing of post selection events
+        final int count = ++postSelectionModCount;
+        //System.out.println("[" + System.currentTimeMillis() + "] scheduling postSelectionChanged " + count + ": " + selection);
+        ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
+            @Override
+            public void run() {
+                int newCount = postSelectionModCount;
+                // Don't publish selection yet, there's another change incoming.
+                //System.out.println("[" + System.currentTimeMillis() + "] checking post selection publish: " + count + " vs. " + newCount + ": " + selection);
+                if (newCount != count)
+                    return;
+                //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);
+
+                if (tree.isDisposed())
+                    return;
+
+                //System.out.println("scheduling fire post selection changed: " + selection);
+                tree.getDisplay().asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (tree.isDisposed() || selectionProvider == null)
+                            return;
+                        //System.out.println("firing post selection changed: " + selection);
+                        selectionProvider.firePostSelection(selection);
+                    }
+                });
+            }
+        }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);
+    }
+    
+    protected void setDefaultProcessors() {
+        // Add a simple IMAGER query processor that always returns null.
+        // With this processor no images will ever be shown.
+//        setPrimitiveProcessor(new StaticImagerProcessor(null));
+
+        setProcessor(new DefaultComparableChildrenProcessor());
+        setProcessor(new DefaultLabelDecoratorsProcessor());
+        setProcessor(new DefaultImageDecoratorsProcessor());
+        setProcessor(new DefaultSelectedLabelerProcessor());
+        setProcessor(new DefaultLabelerFactoriesProcessor());
+        setProcessor(new DefaultSelectedImagerProcessor());
+        setProcessor(new DefaultImagerFactoriesProcessor());
+        setPrimitiveProcessor(new DefaultLabelerProcessor());
+        setPrimitiveProcessor(new DefaultCheckedStateProcessor());
+        setPrimitiveProcessor(new DefaultImagerProcessor());
+        setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());
+        setPrimitiveProcessor(new DefaultImageDecoratorProcessor());
+        setPrimitiveProcessor(new NoSelectionRequestProcessor());
+
+        setProcessor(new DefaultFinalChildrenProcessor(this));
+
+        setProcessor(new DefaultPrunedChildrenProcessor());
+        setProcessor(new DefaultSelectedViewpointProcessor());
+        setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());
+        setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());
+        setProcessor(new DefaultViewpointContributionsProcessor());
+
+        setPrimitiveProcessor(new DefaultViewpointProcessor());
+        setPrimitiveProcessor(new DefaultViewpointContributionProcessor());
+        setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());
+        setPrimitiveProcessor(new DefaultIsExpandedProcessor());
+        setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());
+    }
+
+    @Override
+    public <T> void setProcessor(NodeQueryProcessor<T> processor) {
+        assertNotDisposed();
+        if (processor == null)
+            throw new IllegalArgumentException("null processor");
+
+        processors.put(processor.getIdentifier(), processor);
+    }
+
+    @Override
+    public <T> void setPrimitiveProcessor(PrimitiveQueryProcessor<T> processor) {
+        assertNotDisposed();
+        if (processor == null)
+            throw new IllegalArgumentException("null processor");
+
+        PrimitiveQueryProcessor<?> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
+
+        if (oldProcessor instanceof ProcessorLifecycle)
+            ((ProcessorLifecycle) oldProcessor).detached(this);
+        if (processor instanceof ProcessorLifecycle)
+            ((ProcessorLifecycle) processor).attached(this);
+    }
+
+    @Override
+    public <T> void setDataSource(DataSource<T> provider) {
+        assertNotDisposed();
+        if (provider == null)
+            throw new IllegalArgumentException("null provider");
+        dataSources.put(provider.getProvidedClass(), provider);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> DataSource<T> removeDataSource(Class<T> forProvidedClass) {
+        assertNotDisposed();
+        if (forProvidedClass == null)
+            throw new IllegalArgumentException("null class");
+        return dataSources.remove(forProvidedClass);
+    }
+
+    @Override
+    public void setPersistor(StatePersistor persistor) {
+       this.persistor = persistor;
+    }
+    
+    @Override
+    public SelectionDataResolver getSelectionDataResolver() {
+        return selectionDataResolver;
+    }
+
+    @Override
+    public void setSelectionDataResolver(SelectionDataResolver r) {
+        this.selectionDataResolver = r;
+    }
+
+    @Override
+    public SelectionFilter getSelectionFilter() {
+        return selectionFilter;
+    }
+
+    @Override
+    public void setSelectionFilter(SelectionFilter f) {
+        this.selectionFilter = f;
+        // TODO: re-filter current selection?
+    }
+
+    @Override
+    public void setSelectionTransformation(BinaryFunction<Object[], GraphExplorer, Object[]> f) {
+        this.selectionTransformation = f;
+    }
+
+    @Override
+    public <T> void addListener(T listener) {
+        if(listener instanceof FocusListener) {
+            focusListeners.add((FocusListener)listener);
+        } else if(listener instanceof MouseListener) {
+            mouseListeners.add((MouseListener)listener);
+        } else if(listener instanceof KeyListener) {
+            keyListeners.add((KeyListener)listener);
+        }
+    }
+
+    @Override
+    public <T> void removeListener(T listener) {
+        if(listener instanceof FocusListener) {
+            focusListeners.remove(listener);
+        } else if(listener instanceof MouseListener) {
+            mouseListeners.remove(listener);
+        } else if(listener instanceof KeyListener) {
+            keyListeners.remove(listener);
+        }
+    }
+
+    public void addSelectionListener(SelectionListener listener) {
+        tree.addSelectionListener(listener);
+    }
+
+    public void removeSelectionListener(SelectionListener listener) {
+        tree.removeSelectionListener(listener);
+    }
+
+    private Set<String> uiContexts;
+    
+    @Override
+    public void setUIContexts(Set<String> contexts) {
+       this.uiContexts = contexts;
+    }
+    
+    @Override
+    public void setRoot(final Object root) {
+       if(uiContexts != null && uiContexts.size() == 1)
+               setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
+       else
+               setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
+    }
+
+    @Override
+    public void setRootContext(final NodeContext context) {
+        setRootContext0(context);
+    }
+
+    private void setRootContext0(final NodeContext context) {
+        Assert.isNotNull(context, "root must not be null");
+        if (isDisposed() || tree.isDisposed())
+            return;
+        Display display = tree.getDisplay();
+        if (display.getThread() == Thread.currentThread()) {
+            doSetRoot(context);
+        } else {
+            display.asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    doSetRoot(context);
+                }
+            });
+        }
+    }
+    
+    private void initializeState() {
+        if (persistor == null)
+            return;
+
+        ExplorerState state = persistor.deserialize(
+                Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
+                getRoot());
+
+        // topNodeToSet will be processed by #setData when it encounters a
+        // NodeContext that matches this one.
+//        topNodePath = state.topNodePath;
+//        topNodePathChildIndex = state.topNodePathChildIndex;
+//        currentTopNodePathIndex = 0;
+
+        Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
+        if (processor instanceof DefaultIsExpandedProcessor) {
+            DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
+            for(NodeContext expanded : state.expandedNodes) {
+                isExpandedProcessor.setExpanded(expanded, true);
+            }
+        }
+    }
+
+    private void saveState() {
+        if (persistor == null)
+            return;
+
+        NodeContext[] topNodePath = NodeContext.NONE;
+        int[] topNodePathChildIndex = {};
+        Collection<NodeContext> expandedNodes = Collections.emptyList();
+        Map<String, Integer> columnWidths = Collections.<String, Integer> emptyMap();
+
+        // Resolve top node path
+        TreeItem topItem = tree.getTopItem();
+        if (topItem != null) {
+            NodeContext topNode = (NodeContext) topItem.getData();
+            if (topNode != null) {
+                topNodePath = getNodeContextPathSegments(topNode);
+                topNodePathChildIndex = new int[topNodePath.length];
+                for (int i = 0; i < topNodePath.length; ++i) {
+                    // TODO: get child indexes
+                    topNodePathChildIndex[i] = 0;
+                }
+            }
+        }
+        
+        // Resolve expanded nodes
+        Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
+        if (processor instanceof IsExpandedProcessor) {
+            IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;
+            expandedNodes = isExpandedProcessor.getExpanded();
+        }
+
+        // Column widths
+        TreeColumn[] columns = tree.getColumns();
+        if (columns.length > 1) {
+                   columnWidths = new HashMap<String, Integer>();
+                   for (int i = 0; i < columns.length; ++i) {
+                       columnWidths.put(columns[i].getText(), columns[i].getWidth());
+                   }
+        }
+                   
+        persistor.serialize(
+                Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
+                getRoot(),
+                new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
+    }
+
+    /**
+     * Invoke only from SWT thread to reset the root of the graph explorer tree.
+     * 
+     * @param root
+     */
+    private void doSetRoot(NodeContext root) {
+        if (tree.isDisposed())
+            return;
+        if (root.getConstant(BuiltinKeys.INPUT) == null) {
+            ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));
+            return;
+        }
+
+        // Empty caches, release queries.
+        GraphExplorerContext oldContext = explorerContext;
+        GraphExplorerContext newContext = new GraphExplorerContext();
+        GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));
+        this.explorerContext = newContext;
+        oldContext.safeDispose();
+        toolTip.setGraphExplorerContext(explorerContext);
+
+        // Need to empty these or otherwise they won't be emptied until the
+        // explorer is disposed which would mean that many unwanted references
+        // will be held by this map.
+        clearPrimitiveProcessors();
+
+        this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
+                : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
+
+        explorerContext.getCache().incRef(this.rootContext);
+        
+       initializeState();
+        
+        NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
+
+        tree.setItemCount(contexts.length);
+
+        select(rootContext);
+        refreshColumnSizes();
+    }
+
+    @Override
+    public NodeContext getRoot() {
+        return rootContext;
+    }
+
+    @Override
+    public NodeContext getParentContext(NodeContext context) {
+        if (disposed)
+            throw new IllegalStateException("disposed");
+        if (!thread.currentThreadAccess())
+            throw new IllegalStateException("not in SWT display thread " + thread.getThread());
+
+        TreeItem item = contextToItem.getRight(context);
+        if(item == null) return null;
+        TreeItem parentItem = item.getParentItem();
+        if(parentItem == null) return null;
+        return (NodeContext)parentItem.getData();
+    }
+
+    Point previousTreeSize;
+    Point previousTreeParentSize;
+    boolean activatedBefore = false;
+
+    @Override
+    public void handleEvent(Event event) {
+        //System.out.println("EVENT: " + event);
+        switch(event.type) {
+            case SWT.Expand:
+                //System.out.println("EXPAND: " + event.item);
+                if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
+                    expandVirtual(event);
+                } else {
+                    System.out.println("TODO: non-virtual tree item expand");
+                }
+                break;
+            case SWT.SetData:
+                // Only invoked for SWT.VIRTUAL trees
+                if (disposed)
+                    // Happened for Hannu once during program startup.
+                    // java.lang.AssertionError
+                    //   at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)
+                    //   at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)
+                    //   at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)
+                    // I do not know whether SWT guarantees that SetData events
+                    // don't come after Dispose event has been issued, but I
+                    // think its better to have this check here just incase.
+                    return;
+                setData(event);
+                break;
+            case SWT.Activate:
+                // This ensures that column sizes are refreshed at
+                // least once when the GE is first shown.
+                if (!activatedBefore) {
+                    refreshColumnSizes();
+                    activatedBefore = true;
+                }
+                break;
+            case SWT.Dispose:
+                //new Exception().printStackTrace();
+                if (disposed)
+                    return;
+                disposed = true;
+                doDispose();
+                break;
+            case SWT.Resize:
+                if (event.widget == tree) {
+                    // This case is meant for listening to tree width increase.
+                    // The column resizing must be performed only after the tree
+                    // itself as been resized.
+                    Point size = tree.getSize();
+                    int dx = 0;
+                    if (previousTreeSize != null) {
+                        dx = size.x - previousTreeSize.x;
+                    }
+                    previousTreeSize = size;
+                    //System.out.println("RESIZE: " + dx + " - size=" + size);
+
+                    if (dx > 0) {
+                        tree.setRedraw(false);
+                        refreshColumnSizes(size);
+                        tree.setRedraw(true);
+                    }
+                } else if (event.widget == tree.getParent()) {
+                    // This case is meant for listening to tree width decrease.
+                    // The columns must be resized before the tree widget itself
+                    // is resized to prevent scroll bar flicker. This can be achieved
+                    // by listening to the resize events of the tree parent widget.
+                    Composite parent = tree.getParent();
+                    Point size = parent.getSize();
+
+                    // We must subtract the parent's border and possible
+                    // scroll bar width from the new target width of the columns.
+                    size.x -= tree.getParent().getBorderWidth() * 2;
+                    ScrollBar vBar = parent.getVerticalBar();
+                    if (vBar != null && vBar.isVisible())
+                        size.x -= vBar.getSize().x;
+
+                    int dx = 0;
+                    if (previousTreeParentSize != null) {
+                        dx = size.x - previousTreeParentSize.x;
+                    }
+                    previousTreeParentSize = size;
+                    //System.out.println("RESIZE: " + dx + " - size=" + size);
+
+                    if (dx < 0) {
+                        tree.setRedraw(false);
+                        refreshColumnSizes(size);
+                        tree.setRedraw(true);
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+
+    }
+
+    protected void refreshColumnSizes() {
+//        Composite treeParent = tree.getParent();
+//        Point size = treeParent.getSize();
+//        size.x -= treeParent.getBorderWidth() * 2;
+        Point size = tree.getSize();
+        refreshColumnSizes(size);
+        tree.getParent().layout();
+    }
+
+    /**
+     * This has been disabled since the logic of handling column widths has been
+     * externalized to parties creating {@link GraphExplorerImpl} instances.
+     */
+    protected void refreshColumnSizes(Point toSize) {
+        /*
+        refreshingColumnSizes = true;
+        try {
+            int columnCount = tree.getColumnCount();
+            if (columnCount > 0) {
+                Point size = toSize;
+                int targetWidth = size.x - tree.getBorderWidth() * 2;
+                targetWidth -= 0;
+
+                // Take the vertical scroll bar existence into to account when
+                // calculating the overflow column width.
+                ScrollBar vBar = tree.getVerticalBar();
+                //if (vBar != null && vBar.isVisible())
+                if (vBar != null)
+                    targetWidth -= vBar.getSize().x;
+
+                List<TreeColumn> resizing = new ArrayList<TreeColumn>();
+                int usedWidth = 0;
+                int resizingWidth = 0;
+                int totalWeight = 0;
+                for (int i = 0; i < columnCount - 1; ++i) {
+                    TreeColumn col = tree.getColumn(i);
+                    //System.out.println("  " + col.getText() + ": " + col.getWidth());
+                    int width = col.getWidth();
+                    usedWidth += width;
+                    Column c = (Column) col.getData();
+                    if (c.hasGrab()) {
+                        resizing.add(col);
+                        resizingWidth += width;
+                        totalWeight += c.getWeight();
+                    }
+                }
+
+                int requiredWidthAdjustment = targetWidth - usedWidth;
+                if (requiredWidthAdjustment < 0)
+                    requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());
+                double diff = requiredWidthAdjustment;
+                //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);
+
+                // Decide how much to give space to / take space from each grabbing column
+                double wrel = 1.0 / resizing.size();
+
+                double[] weightedShares = new double[resizing.size()];
+                for (int i = 0; i < resizing.size(); ++i) {
+                    TreeColumn col = resizing.get(i);
+                    Column c = (Column) col.getData();
+                    if (totalWeight == 0) {
+                        weightedShares[i] = wrel;
+                    } else {
+                        weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
+                    }
+                }
+                //System.out.println("grabbing columns:" + resizing);
+                //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
+
+                // Always shrink the columns if necessary, but don't enlarge before
+                // there is sufficient space to at least give all resizable columns
+                // some more width.
+                if (diff < 0 || (diff > 0 && diff > resizing.size())) {
+                    // Need to either shrink or enlarge the resizable columns if possible.
+                    for (int i = 0; i < resizing.size(); ++i) {
+                        TreeColumn col = resizing.get(i);
+                        Column c = (Column) col.getData();
+                        int cw = col.getWidth();
+                        //double wrel = (double) cw / (double) resizingWidth;
+                        //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);
+                        double ddelta = weightedShares[i] * diff;
+                        int delta = 0;
+                        if (diff < 0) {
+                            delta = (int) Math.floor(ddelta);
+                        } else {
+                            delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
+                        }
+                        //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);
+                        //System.out.println("argh(" + col.getText() + "): " + c.getWidth() +  " vs. " + col.getWidth() + " vs. " + (cw+delta));
+                        int newWidth = Math.max(c.getWidth(), cw + delta);
+                        requiredWidthAdjustment -= (newWidth - cw);
+                        col.setWidth(newWidth);
+                    }
+                }
+
+                //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
+
+                TreeColumn last = tree.getColumn(columnCount - 1);
+                // HACK: see #setColumns for why this is here.
+                if (FILLER.equals(last.getText())) {
+                    last.setWidth(Math.max(0, requiredWidthAdjustment));
+                }
+            }
+        } finally {
+            refreshingColumnSizes = false;
+        }
+         */
+    }
+
+    private void doDispose() {
+        explorerContext.dispose();
+
+        // No longer necessary, the used executors are shared.
+        //scheduler.shutdown();
+        //scheduler2.shutdown();
+
+        processors.clear();
+        detachPrimitiveProcessors();
+        primitiveProcessors.clear();
+        dataSources.clear();
+
+        pendingItems.clear();
+
+        rootContext = null;
+
+        contextToItem.clear();
+
+        mouseListeners.clear();
+
+        selectionProvider.clearListeners();
+        selectionProvider = null;
+        selectionDataResolver = null;
+        selectionRefreshContexts.clear();
+        selectedItems.clear();
+        originalFont = null;
+
+        localResourceManager.dispose();
+
+        // Must shutdown image loader job before disposing its ResourceManager
+        imageLoaderJob.dispose();
+        imageLoaderJob.cancel();
+        try {
+            imageLoaderJob.join();
+        } catch (InterruptedException e) {
+            ErrorLogger.defaultLogError(e);
+        }
+        resourceManager.dispose();
+        
+        postSelectionProvider.dispose();
+
+    }
+
+    private void expandVirtual(final Event event) {
+        TreeItem item = (TreeItem) event.item;
+        assert (item != null);
+        NodeContext context = (NodeContext) item.getData();
+        assert (context != null);
+
+        GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));
+        NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);
+        int maxChildren = getMaxChildren(manager, context);
+        item.setItemCount(children.length < maxChildren ? children.length : maxChildren);
+    }
+
+    private NodeContext getNodeContext(TreeItem item) {
+        assert(item != null);
+
+        NodeContext context = (NodeContext)item.getData();
+        assert(context != null);
+
+        return context;
+    }
+
+    private NodeContext getParentContext(TreeItem item) {
+        TreeItem parentItem = item.getParentItem();
+        if(parentItem != null) {
+            return getNodeContext(parentItem);
+        } else {
+            return rootContext;
+        }
+    }
+
+    private static final String LISTENER_SET_INDICATOR = "LSI";
+    private static final String PENDING = "PENDING";
+    private int contextSelectionChangeModCount = 0;
+
+    /**
+     * Only invoked for SWT.VIRTUAL widgets.
+     * 
+     * @param event
+     */
+    private void setData(final Event event) {
+        assert (event != null);
+        TreeItem item = (TreeItem) event.item;
+        assert (item != null);
+
+        // Based on experience it seems to be possible that
+        // SetData events are sent for disposed TreeItems.
+        if (item.isDisposed() || item.getData(PENDING) != null)
+            return;
+
+        //System.out.println("GE.SetData " + item);
+
+        GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
+
+        NodeContext parentContext = getParentContext(item);
+        assert (parentContext != null);
+
+        NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
+
+        // Some children have disappeared since counting
+        if (event.index < 0) {
+            ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());
+            return;
+        }
+        if (event.index >= parentChildren.length)
+            return;
+
+        NodeContext context = parentChildren[event.index];
+        assert (context != null);
+        item.setData(context);
+        
+        // Manage NodeContext -> TreeItem mappings
+        contextToItem.map(context, item);
+        if (item.getData(LISTENER_SET_INDICATOR) == null) {
+            // This "if" exists because setData will get called many
+            // times for the same (NodeContext, TreeItem) pairs.
+            // Each TreeItem only needs one listener, but this
+            // is needed to tell whether it already has a listener
+            // or not.
+            item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
+            item.addListener(SWT.Dispose, itemDisposeListener);
+        }
+
+        boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
+
+        PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);
+        int maxChildren = getMaxChildren(manager, context);
+        //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);
+
+     NodeContext[] pruned = children.getPrunedChildren(); 
+     int count = Math.min(pruned.length, maxChildren);
+
+        if (isExpanded || item.getItemCount() > 1) {
+            item.setItemCount(count);
+            TreeItem[] childItems = item.getItems();
+         for(int i=0;i<count;i++)
+             contextToItem.map(pruned[i], childItems[i]);
+        } else {
+            if (children.getPrunedChildren().length == 0) {
+                item.setItemCount(0);
+            } else {
+//                item.setItemCount(1);
+                item.setItemCount(count);
+                TreeItem[] childItems = item.getItems();
+             for(int i=0;i<count;i++)
+                 contextToItem.map(pruned[i], childItems[i]);
+//                item.getItem(0).setData(PENDING, PENDING);
+//                item.getItem(0).setItemCount(o);
+            }
+        }
+
+        setTextAndImage(item, manager, context, event.index);
+
+        // Check if the node should be auto-expanded?
+        if ((autoExpandLevel == ALL_LEVELS || autoExpandLevel > 1) && !isExpanded) {
+            //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");
+            int level = getTreeItemLevel(item);
+            if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)
+                    && !explorerContext.autoExpanded.containsKey(context))
+            {
+                //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
+                explorerContext.autoExpanded.put(context, Boolean.TRUE);
+                setExpanded(context, true);
+            }
+        }
+
+        item.setExpanded(isExpanded);
+
+        if ((tree.getStyle() & SWT.CHECK) != 0) {
+            CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);
+            item.setChecked(CheckedState.CHECKED_STATES.contains(checked));
+            item.setGrayed(CheckedState.GRAYED == checked);
+        }
+
+        //System.out.println("GE.SetData completed " + item);
+
+        // This test makes sure that selectionProvider holds the correct
+        // selection with respect to the actual selection stored by the virtual
+        // SWT Tree widget.
+        // The data items shown below the items occupied by the selected and now removed data
+        // will be squeezed to use the tree items previously used for the now
+        // removed data. When this happens, the NodeContext items stored by the
+        // tree items will be different from what the GraphExplorer's
+        // ISelectionProvider thinks the selection currently is. To compensate,
+        // 1. Recognize the situation
+        // 2. ASAP set the selection provider selection to what is actually
+        // offered by the tree widget.
+        NodeContext selectedContext = selectedItems.get(item);
+//        System.out.println("selectedContext(" + item + "): " + selectedContext);
+        if (selectedContext != null && !selectedContext.equals(context)) {
+               final int modCount = ++contextSelectionChangeModCount;
+//            System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);
+//            System.out.println("    old context: " + selectedContext);
+//            System.out.println("    new context: " + context);
+//            System.out.println("    provider selection: " + selectionProvider.getSelection());
+//            System.out.println("    widget   selection: " + getWidgetSelection());
+            ThreadUtils.asyncExec(thread, new Runnable() {
+                @Override
+                public void run() {
+                    if (isDisposed())
+                        return;
+                    int count = contextSelectionChangeModCount;
+//                    System.out.println("MODCOUNT: " + modCount + " vs. " + count);
+                    if (modCount != count)
+                        return;
+                    widgetSelectionChanged(true);
+                }
+            });
+        }
+
+        // This must be done to keep the visible tree selection properly
+        // in sync with the selectionProvider JFace proxy of this class in
+        // cases where an in-line editor was previously active for the node
+        // context.
+        if (selectionRefreshContexts.remove(context)) {
+            final ISelection currentSelection = selectionProvider.getSelection();
+            // asyncExec is here to prevent ui glitches that
+            // seem to occur if the selection setting is done
+            // directly here in between setData invocations.
+            ThreadUtils.asyncExec(thread, new Runnable() {
+                @Override
+                public void run() {
+                    if (isDisposed())
+                        return;
+//                    System.out.println("REFRESHING SELECTION: " + currentSelection);
+//                    System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));
+//                    System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());
+                    setSelection(currentSelection, true);
+//                    System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));
+//                    System.out.println("AFTER setSelection: " + selectionProvider.getSelection());
+                }
+            });
+        }
+
+        // TODO: doesn't work if any part of the node path that should be
+        // revealed is out of view.
+        // Disabled until a better solution is devised.
+        // Suggestion: include item indexes into the stored node context path
+        // to make it possible for this method to know whether the current
+        // node path segment is currently out of view based on event.index.
+        // If out of view, this code needs to scroll the view programmatically
+        // onwards.
+//        if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {
+//            NodeContext topNode = topNodePath[currentTopNodePathIndex];
+//            if (topNode.equals(context)) {
+//                final TreeItem topItem = item;
+//                ++currentTopNodePathIndex;
+//                if (currentTopNodePathIndex >= topNodePath.length) {
+//                    // Mission accomplished. End search for top node here.
+//                    topNodePath = NodeContext.NONE;
+//                    currentTopNodePathIndex = -1;
+//                }
+//                ThreadUtils.asyncExec(thread, new Runnable() {
+//                    @Override
+//                    public void run() {
+//                        if (isDisposed())
+//                            return;
+//                        tree.setTopItem(topItem);
+//                    }
+//                });
+//            }
+//        }
+
+        // Check if vertical scroll bar has become visible and refresh layout.
+        ScrollBar verticalBar = tree.getVerticalBar();
+        if(verticalBar != null) {
+               boolean currentlyVerticalBarVisible = verticalBar.isVisible();
+               if (verticalBarVisible != currentlyVerticalBarVisible) {
+                   verticalBarVisible = currentlyVerticalBarVisible;
+                   Composite parent = tree.getParent();
+                   if (parent != null)
+                       parent.layout();
+               }
+        }
+    }
+
+    /**
+     * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the
+     *         return value is calculated. Items without parents have level=2,
+     *         their children level=3, etc. Returns 0 for invalid items
+     */
+    private int getTreeItemLevel(TreeItem item) {
+        if (item == null)
+            return 0;
+        int level = 1;
+        for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);
+        //System.out.println("\tgetTreeItemLevel(" + parent + ")");
+        //System.out.println("level(" + item + "): " + level);
+        return level;
+    }
+
+    /**
+     * @param node
+     * @return
+     */
+    private NodeContext[] getNodeContextPathSegments(NodeContext node) {
+        TreeItem item = contextToItem.getRight(node);
+        if (item == null)
+            return NodeContext.NONE;
+        int level = getTreeItemLevel(item);
+        if (level == 0)
+            return NodeContext.NONE;
+        // Exclude root from the saved node path.
+        --level;
+        NodeContext[] segments = new NodeContext[level];
+        for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {
+            NodeContext ctx = (NodeContext) item.getData();
+            if (ctx == null)
+                return NodeContext.NONE;
+            segments[level-1] = ctx;
+        }
+        return segments;
+    }
+
+    /**
+     * @param node
+     * @return
+     */
+    @SuppressWarnings("unused")
+    private NodeContextPath getNodeContextPath(NodeContext node) {
+        NodeContext[] path = getNodeContextPathSegments(node);
+        return new NodeContextPath(path);
+    }
+
+    void setImage(NodeContext node, TreeItem item, Imager imager, Collection<ImageDecorator> decorators, int itemIndex) {
+        Image[] images = columnImageArray;
+        Arrays.fill(images, null);
+        if (imager == null) {
+            item.setImage(images);
+            return;
+        }
+
+        Object[] descOrImage = columnDescOrImageArray;
+        Arrays.fill(descOrImage, null);
+        boolean finishLoadingInJob = false;
+        int index = 0;
+        for (Column column : columns) {
+            String key = column.getKey();
+            ImageDescriptor desc = imager.getImage(key);
+            if (desc != null) {
+                // Attempt to decorate the label
+                if (!decorators.isEmpty()) {
+                    for (ImageDecorator id : decorators) {
+                        ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);
+                        if (ds != null)
+                            desc = ds;
+                    }
+                }
+
+                // Try resolving only cached images here and now
+                Object img = localResourceManager.find(desc);
+                if (img == null)
+                    img = resourceManager.find(desc);
+
+                images[index] = img != null ? (Image) img : null;
+                descOrImage[index] = img == null ? desc : img;
+                finishLoadingInJob |= img == null;
+            }
+            ++index;
+        }
+
+        // Finish loading the final image in the image loader job if necessary.
+        if (finishLoadingInJob) {
+            // Prevent UI from flashing unnecessarily by reusing the old image
+            // in the item if it exists.
+            for (int c = 0; c < columns.length; ++c) {
+                Image img = item.getImage(c);
+                if (img != null)
+                    images[c] = img;
+            }
+            item.setImage(images);
+
+            // Schedule loading to another thread to refrain from blocking
+            // the UI with database operations.
+            queueImageTask(item, new ImageTask(
+                    node,
+                    item,
+                    Arrays.copyOf(descOrImage, descOrImage.length)));
+        } else {
+            // Set any images that were resolved.
+            item.setImage(images);
+        }
+    }
+
+    private void queueImageTask(TreeItem item, ImageTask task) {
+        synchronized (imageTasks) {
+            imageTasks.put(item, task);
+        }
+        imageLoaderJob.scheduleIfNecessary(100);
+    }
+
+    /**
+     * Invoked in a job worker thread.
+     * 
+     * @param monitor
+     * @see ImageLoaderJob
+     */
+    @Override
+       protected IStatus setPendingImages(IProgressMonitor monitor) {
+        ImageTask[] tasks = null;
+        synchronized (imageTasks) {
+            tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);
+            imageTasks.clear();
+        }
+        if (tasks.length == 0)
+            return Status.OK_STATUS;
+
+        MultiStatus status = null;
+
+        // Load missing images
+        for (ImageTask task : tasks) {
+            Object[] descs = task.descsOrImages;
+            for (int i = 0; i < descs.length; ++i) {
+                Object obj = descs[i];
+                if (obj instanceof ImageDescriptor) {
+                    ImageDescriptor desc = (ImageDescriptor) obj; 
+                    try {
+                        descs[i] = resourceManager.get((ImageDescriptor) desc);
+                    } catch (DeviceResourceException e) {
+                        if (status == null)
+                            status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);
+                        status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));
+                    }
+                }
+            }
+        }
+
+        // Perform final UI updates in the UI thread.
+        final ImageTask[] _tasks = tasks;
+        thread.asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (!tree.isDisposed()) {
+                    tree.setRedraw(false);
+                    setImages(_tasks);
+                    tree.setRedraw(true);
+                }
+            }
+        });
+
+        return status != null ? status : Status.OK_STATUS;
+    }
+
+    /**
+     * Invoked in the UI thread only.
+     * 
+     * @param task
+     */
+    void setImages(ImageTask[] tasks) {
+        for (ImageTask task : tasks)
+            if (task != null)
+                setImage(task);
+    }
+
+    /**
+     * Invoked in the UI thread only.
+     * 
+     * @param task
+     */
+    void setImage(ImageTask task) {
+        // Be sure not to process disposed items.
+        if (task.item.isDisposed())
+            return;
+        // Discard this task if the TreeItem has switched owning NodeContext.
+        if (!contextToItem.contains(task.node, task.item))
+            return;
+
+        Object[] descs = task.descsOrImages;
+        Image[] images = columnImageArray;
+        Arrays.fill(images, null);
+        for (int i = 0; i < descs.length; ++i) {
+            Object desc = descs[i];
+            if (desc instanceof Image) {
+                images[i] = (Image) desc;
+            }
+        }
+        task.item.setImage(images);
+    }
+
+    void setText(TreeItem item, Labeler labeler, Collection<LabelDecorator> decorators, int itemIndex) {
+        if (labeler != null) {
+            String[] texts = new String[columns.length];
+            int index = 0;
+            Map<String, String> labels = labeler.getLabels();
+            Map<String, String> runtimeLabels = labeler.getRuntimeLabels();
+            for (Column column : columns) {
+                String key = column.getKey();
+                String s = null;
+                if (runtimeLabels != null) s = runtimeLabels.get(key);
+                if (s == null) s = labels.get(key);
+                if (s != null) {
+                    FontDescriptor font = originalFont;
+                    ColorDescriptor bg = originalBackground;
+                    ColorDescriptor fg = originalForeground;
+
+                    // Attempt to decorate the label
+                    if (!decorators.isEmpty()) {
+                        for (LabelDecorator ld : decorators) {
+                            String ds = ld.decorateLabel(s, key, itemIndex);
+                            if (ds != null)
+                                s = ds;
+
+                            FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
+                            if (dfont != null)
+                                font = dfont;
+
+                            ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
+                            if (dbg != null)
+                                bg = dbg;
+
+                            ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
+                            if (dfg != null)
+                                fg = dfg;
+                        }
+                    }
+
+                    if (font != originalFont) {
+                        //System.out.println("set font: " + index + ": " + font);
+                        item.setFont(index, (Font) localResourceManager.get(font));
+                    }
+                    if (bg != originalBackground)
+                        item.setBackground(index, (Color) localResourceManager.get(bg));
+                    if (fg != originalForeground)
+                        item.setForeground(index, (Color) localResourceManager.get(fg));
+
+                    texts[index] = s;
+                }
+                ++index;
+            }
+            item.setText(texts);
+        } else {
+            item.setText(Labeler.NO_LABEL);
+        }
+    }
+
+    void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {
+        Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
+        if (labeler != null) {
+            labeler.setListener(labelListener);
+        }
+        Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);
+        Collection<LabelDecorator> labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);
+        Collection<ImageDecorator> imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);
+
+        setText(item, labeler, labelDecorators, itemIndex);
+        setImage(context, item, imager, imageDecorators, itemIndex);
+    }
+
+    @Override
+    public void setFocus() {
+        tree.setFocus();
+    }
+
+    @Override
+    public <T> T query(NodeContext context, CacheKey<T> key) {
+        return this.explorerContext.cache.get(context, key);
+    }
+
+    @Override
+    public boolean isDisposed() {
+        return disposed;
+    }
+
+    protected void assertNotDisposed() {
+        if (isDisposed())
+            throw new IllegalStateException("disposed");
+    }
+
+
+
+    /**
+     * @param selection
+     * @param forceControlUpdate
+     * @thread any
+     */
+    public void setSelection(final ISelection selection, boolean forceControlUpdate) {
+        assertNotDisposed();
+        boolean equalsOld = selectionProvider.selectionEquals(selection);
+        if (equalsOld && !forceControlUpdate) {
+            // Just set the selection object instance, fire no events nor update
+            // the viewer selection.
+            selectionProvider.setSelection(selection);
+        } else {
+            // Schedule viewer and selection update if necessary.
+            if (tree.isDisposed())
+                return;
+            Display d = tree.getDisplay();
+            if (d.getThread() == Thread.currentThread()) {
+                updateSelectionToControl(selection);
+            } else {
+                d.asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (tree.isDisposed())
+                            return;
+                        updateSelectionToControl(selection);
+                    }
+                });
+            }
+        }
+    }
+    
+
+    /* Contains the best currently found tree item and its priority
+     */
+    private static class SelectionResolutionStatus {
+        int bestPriority = Integer.MAX_VALUE;
+        TreeItem bestItem;
+    }
+
+    /**
+     * @param selection
+     * @thread SWT
+     */
+    private void updateSelectionToControl(ISelection selection) {
+        if (selectionDataResolver == null)
+            return;
+        if (!(selection instanceof IStructuredSelection))
+            return;
+        
+        // Initialize selection resolution status map 
+        IStructuredSelection iss = (IStructuredSelection) selection;
+        final THashMap<Object,SelectionResolutionStatus> statusMap =
+                new THashMap<Object,SelectionResolutionStatus>(iss.size());
+        for(Iterator<?> it = iss.iterator(); it.hasNext();) {
+            Object selectionElement = it.next();
+            Object resolvedElement = selectionDataResolver.resolve(selectionElement);
+            statusMap.put(
+                    resolvedElement,
+                    new SelectionResolutionStatus());
+        }
+        
+        // Iterate all tree items and try to match them to the selection
+        iterateTreeItems(new TObjectProcedure<TreeItem>() {
+            @Override
+            public boolean execute(TreeItem treeItem) {
+                NodeContext nodeContext = (NodeContext)treeItem.getData();
+                if(nodeContext == null)
+                    return true;
+                SelectionResolutionStatus status = statusMap.get(nodeContext);
+                if(status != null) {
+                    status.bestPriority = 0; // best possible match
+                    status.bestItem = treeItem;
+                    return true;
+                }
+                
+                Object input = nodeContext.getConstant(BuiltinKeys.INPUT);
+                status = statusMap.get(input);
+                if(status != null) {
+                    NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);
+                    int curPriority = nodeType instanceof EntityNodeType 
+                            ? 1 // Prefer EntityNodeType matches to other node types
+                            : 2;
+                    if(curPriority < status.bestPriority) {
+                        status.bestPriority = curPriority;
+                        status.bestItem = treeItem;
+                    }
+                }
+                return true;
+            }
+        });
+        
+        // Update selection
+        ArrayList<TreeItem> items = new ArrayList<TreeItem>(statusMap.size());
+        for(SelectionResolutionStatus status : statusMap.values())
+            if(status.bestItem != null)
+                items.add(status.bestItem);
+        select(items.toArray(new TreeItem[items.size()]));
+    }
+
+    /**
+     * @thread SWT
+     */
+    public ISelection getWidgetSelection() {
+        TreeItem[] items = tree.getSelection();
+        if (items.length == 0)
+            return StructuredSelection.EMPTY;
+
+        List<NodeContext> nodes = new ArrayList<NodeContext>(items.length);
+
+        // Caches for resolving node contexts the hard way if necessary.
+        GENodeQueryManager manager = null;
+        NodeContext lastParentContext = null;
+        NodeContext[] lastChildren = null;
+
+        for (int i = 0; i < items.length; i++) {
+            TreeItem item = items[i];
+            NodeContext ctx = (NodeContext) item.getData();
+            // It may happen due to the virtual nature of the tree control
+            // that it contains TreeItems which have not yet been ran through
+            // #setData(Event).
+            if (ctx != null) {
+                nodes.add(ctx);
+            } else {
+                TreeItem parentItem = item.getParentItem();
+                NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;
+                if (parentContext != null) {
+                    NodeContext[] children = lastChildren;
+                    if (parentContext != lastParentContext) {
+                        if (manager == null)
+                            manager = new GENodeQueryManager(this.explorerContext, null, null, null);
+                        lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
+                        lastParentContext = parentContext;
+                    }
+                    int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);
+                    if (index >= 0 && index < children.length) {
+                        NodeContext child = children[index];
+                        if (child != null) {
+                            nodes.add(child);
+                            // Cache NodeContext in TreeItem for faster access
+                            item.setData(child);
+                        }
+                    }
+                }
+            }
+        }
+        //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
+        ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
+        return selection;
+    }
+    
+    @Override
+    public TransientExplorerState getTransientState() {
+        if (!thread.currentThreadAccess())
+            throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());
+        return transientState;
+    }
+
+    /**
+     * @param item
+     * @thread SWT
+     */
+    private void select(TreeItem item) {
+        tree.setSelection(item);
+        tree.showSelection();
+        selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
+    }
+
+    /**
+     * @param items
+     * @thread SWT
+     */
+    private void select(TreeItem[] items) {
+        //System.out.println("Select: " + Arrays.toString(items));
+        tree.setSelection(items);
+        tree.showSelection();
+        NodeContext[] data = new NodeContext[items.length];
+        for (int i = 0; i < data.length; i++) {
+            data[i] = (NodeContext) items[i].getData();
+        }
+        selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
+    }
+    
+    private void iterateTreeItems(TObjectProcedure<TreeItem> procedure) {
+        for(TreeItem item : tree.getItems())
+            if(!iterateTreeItems(item, procedure))
+                return;
+    }
+
+    private boolean iterateTreeItems(TreeItem item,
+            TObjectProcedure<TreeItem> procedure) {
+        if(!procedure.execute(item))
+            return false;
+        if(item.getExpanded())
+            for(TreeItem child : item.getItems())
+                if(!iterateTreeItems(child, procedure))
+                    return false;
+        return true;
+    }
+
+    /**
+     * @param item
+     * @param context
+     * @return
+     */
+    private boolean trySelect(TreeItem item, Object input) {
+        NodeContext itemCtx = (NodeContext) item.getData();
+        if (itemCtx != null) {
+            if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {
+                select(item);
+                return true;
+            }
+        }
+        if (item.getExpanded()) {
+            for (TreeItem child : item.getItems()) {
+                if (trySelect(child, input))
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean equalsEnough(NodeContext c1, NodeContext c2) {
+       
+       Object input1 = c1.getConstant(BuiltinKeys.INPUT);
+       Object input2 = c2.getConstant(BuiltinKeys.INPUT);
+       if(!ObjectUtils.objectEquals(input1, input2)) 
+               return false;
+
+       Object type1 = c1.getConstant(NodeType.TYPE);
+       Object type2 = c2.getConstant(NodeType.TYPE);
+       if(!ObjectUtils.objectEquals(type1, type2)) 
+               return false;
+       
+       return true;
+       
+    }
+    
+    private NodeContext tryFind(NodeContext context) {
+        for (TreeItem item : tree.getItems()) {
+               NodeContext found = tryFind(item, context);
+               if(found != null) return found;
+        }
+        return null;
+    }
+    
+    private NodeContext tryFind(TreeItem item, NodeContext context) {
+        NodeContext itemCtx = (NodeContext) item.getData();
+        if (itemCtx != null) {
+            if (equalsEnough(context, itemCtx)) {
+                return itemCtx;
+            }
+        }
+        if (item.getExpanded()) {
+            for (TreeItem child : item.getItems()) {
+               NodeContext found = tryFind(child, context);
+               if(found != null) return found;
+            }
+        }
+        return null;
+    }
+    
+    @Override
+    public boolean select(NodeContext context) {
+
+        assertNotDisposed();
+
+        if (context == null || context.equals(rootContext)) {
+            tree.deselectAll();
+            selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
+            return true;
+        }
+
+//        if (context.equals(rootContext)) {
+//            tree.deselectAll();
+//            selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
+//            return;
+//        }
+
+        Object input = context.getConstant(BuiltinKeys.INPUT);
+
+        for (TreeItem item : tree.getItems()) {
+            if (trySelect(item, input))
+                return true;
+        }
+        
+        return false;
+        
+    }
+    
+    private NodeContext tryFind2(NodeContext context) {
+       Set<NodeContext> ctxs = contextToItem.getLeftSet();
+       for(NodeContext c : ctxs) 
+               if(equalsEnough(c, context)) 
+                       return c;
+       return null;
+    }
+
+    private boolean waitVisible(NodeContext parent, NodeContext context) {
+       long start = System.nanoTime();
+       
+       TreeItem parentItem = contextToItem.getRight(parent);
+       
+       if(parentItem == null) 
+               return false; 
+       
+       while(true) {
+               NodeContext target = tryFind2(context);
+               if(target != null) {
+                       TreeItem item = contextToItem.getRight(target);
+                       if (!(item.getParentItem().equals(parentItem)))
+                               return false;
+                       tree.setTopItem(item);
+                       return true;
+               }
+
+               Display.getCurrent().readAndDispatch();
+               long duration = System.nanoTime() - start;
+               if(duration > 10e9)
+                       return false;                   
+       }
+    }
+    
+    private boolean selectPathInternal(NodeContext[] contexts, int position) {
+       //System.out.println("NodeContext path : " + contexts);
+
+       NodeContext head = tryFind(contexts[position]);
+
+       if(position == contexts.length-1) {
+               return select(head);
+
+       }
+
+       //setExpanded(head, true);
+       
+       if(!waitVisible(head, contexts[position+1])) 
+               return false;
+       
+       setExpanded(head, true);
+       
+       return selectPathInternal(contexts, position+1);
+       
+    }
+    
+    @Override
+    public boolean selectPath(Collection<NodeContext> contexts) {
+       
+       if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");
+       if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");
+       
+       return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
+       
+    }
+    
+    @Override
+    public boolean isVisible(NodeContext context) {
+       
+        for (TreeItem item : tree.getItems()) {
+               NodeContext found = tryFind(item, context);
+               if(found != null) 
+                       return true;
+        }
+        
+        return false;
+        
+    }
+
+    protected ISelection constructSelection(NodeContext... contexts) {
+        if (contexts ==  null)
+            throw new IllegalArgumentException("null contexts");
+        if (contexts.length == 0)
+            return StructuredSelection.EMPTY;
+        if (selectionFilter == null)
+            return new StructuredSelection(transformSelection(contexts));
+        return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );
+    }
+
+    protected Object[] transformSelection(Object[] objects) {
+        return selectionTransformation.call(this, objects);
+    }
+
+    protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
+        int len = contexts.length;
+        Object[] objects = new Object[len];
+        for (int i = 0; i < len; ++i)
+            objects[i] = filter.filter(contexts[i]);
+        return objects;
+    }
+
+    @Override
+    public void setExpanded(final NodeContext context, final boolean expanded) {
+        assertNotDisposed();
+        ThreadUtils.asyncExec(thread, new Runnable() {
+            @Override
+            public void run() {
+                if (!isDisposed())
+                    doSetExpanded(context, expanded);
+            }
+        });
+    }
+
+    private void doSetExpanded(NodeContext context, boolean expanded) {
+        //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
+        TreeItem item = contextToItem.getRight(context);
+        if (item != null) {
+            item.setExpanded(expanded);
+        }
+        PrimitiveQueryProcessor<?> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
+        if (pqp instanceof IsExpandedProcessor) {
+            IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
+            iep.replaceExpanded(context, expanded);
+        }
+    }
+
+    @Override
+    public void setColumnsVisible(boolean visible) {
+        columnsAreVisible = visible;
+        if(tree != null) tree.setHeaderVisible(columnsAreVisible);
+    }
+
+    @Override
+    public void setColumns(final Column[] columns) {
+        setColumns(columns, null);
+    }
+
+    @Override
+    public void setColumns(final Column[] columns, Consumer<Map<Column, Object>> callback) {
+        assertNotDisposed();
+        checkUniqueColumnKeys(columns);
+
+        Display d = tree.getDisplay();
+        if (d.getThread() == Thread.currentThread())
+            doSetColumns(columns, callback);
+        else
+            d.asyncExec(() -> {
+                if (tree.isDisposed())
+                    return;
+                doSetColumns(columns, callback);
+            });
+    }
+
+    private void checkUniqueColumnKeys(Column[] cols) {
+        Set<String> usedColumnKeys = new HashSet<String>();
+        List<Column> duplicateColumns = new ArrayList<Column>();
+        for (Column c : cols) {
+            if (!usedColumnKeys.add(c.getKey()))
+                duplicateColumns.add(c);
+        }
+        if (!duplicateColumns.isEmpty()) {
+            throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
+        }
+    }
+
+    /**
+     * Only meant to be invoked from the SWT UI thread.
+     * 
+     * @param cols
+     */
+    private void doSetColumns(Column[] cols, Consumer<Map<Column, Object>> callback) {
+        // Attempt to keep previous column widths.
+        Map<String, Integer> prevWidths = new HashMap<String, Integer>();
+        for (TreeColumn column : tree.getColumns()) {
+            prevWidths.put(column.getText(), column.getWidth());
+            column.dispose();
+        }
+
+        HashMap<String, Integer> keyToIndex = new HashMap<String, Integer>();
+        for (int i = 0; i < cols.length; ++i) {
+            keyToIndex.put(cols[i].getKey(), i);
+        }
+
+        this.columns = Arrays.copyOf(cols, cols.length);
+        //this.columns[cols.length] = FILLER_COLUMN;
+        this.columnKeyToIndex = keyToIndex;
+        this.columnImageArray = new Image[cols.length];
+        this.columnDescOrImageArray = new Object[cols.length];
+
+        Map<Column, Object> map = new HashMap<Column, Object>();
+
+        tree.setHeaderVisible(columnsAreVisible);
+        for (Column column : columns) {
+            TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));
+            map.put(column, c);
+            c.setData(column);
+            c.setText(column.getLabel());
+            c.setToolTipText(column.getTooltip());
+
+            int cw = column.getWidth();
+
+            // Try to keep previous widths
+            Integer w = prevWidths.get(column);
+            if (w != null)
+                c.setWidth(w);
+            else if (cw != Column.DEFAULT_CONTROL_WIDTH)
+                c.setWidth(cw);
+            else {
+                // Go for some kind of default settings then...
+                if (ColumnKeys.PROPERTY.equals(column.getKey()))
+                    c.setWidth(150);
+                else
+                    c.setWidth(50);
+            }
+
+//            if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
+//                c.addListener(SWT.Resize, resizeListener);
+//                c.setResizable(true);
+//            } else {
+//                //c.setResizable(false);
+//            }
+
+        }
+
+        if(callback != null) callback.accept(map);
+
+        // Make sure the explorer fits the columns properly after initialization.
+        tree.getDisplay().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (tree.isDisposed())
+                    return;
+                refreshColumnSizes();
+            }
+        });
+    }
+
+    int toSWT(Align alignment) {
+        switch (alignment) {
+            case LEFT: return SWT.LEFT;
+            case CENTER: return SWT.CENTER;
+            case RIGHT: return SWT.RIGHT;
+            default: throw new Error("unhandled alignment: " + alignment);
+        }
+    }
+
+    @Override
+    public Column[] getColumns() {
+        return Arrays.copyOf(columns, columns.length);
+    }
+
+    private void detachPrimitiveProcessors() {
+        for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
+            if (p instanceof ProcessorLifecycle) {
+                ((ProcessorLifecycle) p).detached(this);
+            }
+        }
+    }
+
+    private void clearPrimitiveProcessors() {
+        for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
+            if (p instanceof ProcessorLifecycle) {
+                ((ProcessorLifecycle) p).clear();
+            }
+        }
+    }
+
+    Listener resizeListener = new Listener() {
+        @Override
+        public void handleEvent(Event event) {
+            // Prevent infinite recursion.
+            if (refreshingColumnSizes)
+                return;
+            //TreeColumn column = (TreeColumn) event.widget;
+            //Column c = (Column) column.getData();
+            refreshColumnSizes();
+        }
+    };
+
+    Listener itemDisposeListener = new Listener() {
+        @Override
+        public void handleEvent(Event event) {
+            if (event.type == SWT.Dispose) {
+                if (event.widget instanceof TreeItem) {
+                    TreeItem ti = (TreeItem) event.widget;
+                    //NodeContext ctx = (NodeContext) ti.getData();
+//                    System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));
+//                    System.out.println("  map size BEFORE: " + contextToItem.size());
+                    @SuppressWarnings("unused")
+                    NodeContext removed = contextToItem.removeWithRight(ti);
+//                    System.out.println("  REMOVED: " + removed);
+//                    System.out.println("  map size AFTER: " + contextToItem.size());
+                }
+            }
+        }
+    };
+
+    /**
+     * 
+     */
+    LabelerListener labelListener = new LabelerListener() {
+        @Override
+        public boolean columnModified(final NodeContext context, final String key, final String newLabel) {
+            //System.out.println("column " + key + " modified for " + context + " to " + newLabel);
+            if (tree.isDisposed())
+                return false;
+
+            synchronized (labelRefreshRunnables) {
+                Runnable refresher = new Runnable() {
+                    @Override
+                    public void run() {
+                        // Tree is guaranteed to be non-disposed if this is invoked.
+
+                        // contextToItem should be accessed only in the SWT thread to keep things thread-safe.
+                        final TreeItem item = contextToItem.getRight(context);
+                        if (item == null || item.isDisposed())
+                            return;
+
+                        final Integer index = columnKeyToIndex.get(key);
+                        if (index == null)
+                            return;
+
+                        //System.out.println(" found index: " + index);
+                        //System.out.println("  found item: " + item);
+                        try {
+                            GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
+
+                            // FIXME: indexOf is quadratic
+                            int itemIndex = 0;
+                            TreeItem parentItem = item.getParentItem();
+                            if (parentItem == null) {
+                                itemIndex = tree.indexOf(item);
+                                //tree.clear(parentIndex, false);
+                            } else {
+                                itemIndex = parentItem.indexOf(item);
+                                //item.clear(parentIndex, false);
+                            }
+                            setTextAndImage(item, manager, context, itemIndex);
+                        } catch (SWTException e) {
+                            ErrorLogger.defaultLogError(e);
+                        }
+                    }
+                };
+                //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
+                labelRefreshRunnables.put(context, refresher);
+
+                if (!refreshIsQueued) {
+                    refreshIsQueued = true;
+                    long delay = 0;
+                    long now = System.currentTimeMillis();
+                    long elapsed = now - lastLabelRefreshScheduled;
+                    if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)
+                        delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;
+                    //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");
+                    if (delay > 0) {
+                        ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
+                            @Override
+                            public void run() {
+                                scheduleImmediateLabelRefresh();
+                            }
+                        }, delay, TimeUnit.MILLISECONDS);
+                    } else {
+                        scheduleImmediateLabelRefresh();
+                    }
+                    lastLabelRefreshScheduled = now;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public boolean columnsModified(final NodeContext context, final Map<String, String> columns) {
+            System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
+            return false;
+        }
+    };
+
+    private void scheduleImmediateLabelRefresh() {
+        Runnable[] runnables = null;
+        synchronized (labelRefreshRunnables) {
+            if (labelRefreshRunnables.isEmpty())
+                return;
+
+            runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
+            labelRefreshRunnables.clear();
+            refreshIsQueued = false;
+        }
+        final Runnable[] rs = runnables;
+
+        if (tree.isDisposed())
+            return;
+        tree.getDisplay().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (tree.isDisposed())
+                    return;
+                //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");
+                tree.setRedraw(false);
+                for (Runnable r : rs) {
+                    r.run();
+                }
+                tree.setRedraw(true);
+            }
+        });
+    }
+
+    long                       lastLabelRefreshScheduled = 0;
+    boolean                    refreshIsQueued           = false;
+    Map<NodeContext, Runnable> labelRefreshRunnables     = new HashMap<NodeContext, Runnable>();
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T getAdapter(Class<T> adapter) {
+        if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;
+        else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;
+        return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T getControl() {
+        return (T) tree;
+    }
+
+    /* (non-Javadoc)
+     * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
+     */
+    @Override
+    public void setAutoExpandLevel(int level) {
+        this.autoExpandLevel = level;
+    }
+
+    @Override
+    public <T> NodeQueryProcessor<T> getProcessor(QueryKey<T> key) {
+        return explorerContext.getProcessor(key);
+    }
+
+    @Override
+    public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(PrimitiveQueryKey<T> key) {
+        return explorerContext.getPrimitiveProcessor(key);
+    }
+
+    @Override
+    public boolean isEditable() {
+        return editable;
+    }
+
+    @Override
+    public void setEditable(boolean editable) {
+        if (!thread.currentThreadAccess())
+            throw new IllegalStateException("not in SWT display thread " + thread.getThread());
+
+        this.editable = editable;
+        Display display = tree.getDisplay();
+        tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
+    }
+
+    /**
+     * For setting a more local service locator for the explorer than the global
+     * workbench service locator. Sometimes required to give this implementation
+     * access to local workbench services like IFocusService.
+     * 
+     * <p>
+     * Must be invoked during right after construction.
+     * 
+     * @param serviceLocator
+     *            a specific service locator or <code>null</code> to use the
+     *            workbench global service locator
+     */
+    public void setServiceLocator(IServiceLocator serviceLocator) {
+        if (serviceLocator == null && PlatformUI.isWorkbenchRunning())
+            serviceLocator = PlatformUI.getWorkbench();
+        this.serviceLocator = serviceLocator;
+        if (serviceLocator != null) {
+            this.contextService = (IContextService) serviceLocator.getService(IContextService.class);
+            this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);
+        }
+    }
+    
+    @Override
+    public Object getClicked(Object event) {
+       MouseEvent e = (MouseEvent)event;
+       final Tree tree = (Tree) e.getSource();
+        Point point = new Point(e.x, e.y);
+        TreeItem item = tree.getItem(point);
+
+        // No selectable item at point?
+        if (item == null)
+            return null;
+
+        Object data = item.getData();
+        return data;
+    }
+
+}