]> gerrit.simantics Code Review - simantics/platform.git/blob - 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
1 /*******************************************************************************
2  * Copyright (c) 2007, 2012 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.browsing.ui.swt;
13
14 import java.util.ArrayList;
15 import java.util.Arrays;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.Deque;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.Iterator;
22 import java.util.LinkedList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.WeakHashMap;
27 import java.util.concurrent.CopyOnWriteArrayList;
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Future;
30 import java.util.concurrent.ScheduledExecutorService;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.atomic.AtomicBoolean;
33 import java.util.concurrent.atomic.AtomicReference;
34 import java.util.function.Consumer;
35
36 import org.eclipse.core.runtime.Assert;
37 import org.eclipse.core.runtime.AssertionFailedException;
38 import org.eclipse.core.runtime.IProgressMonitor;
39 import org.eclipse.core.runtime.IStatus;
40 import org.eclipse.core.runtime.MultiStatus;
41 import org.eclipse.core.runtime.Platform;
42 import org.eclipse.core.runtime.Status;
43 import org.eclipse.core.runtime.jobs.Job;
44 import org.eclipse.jface.action.IStatusLineManager;
45 import org.eclipse.jface.resource.ColorDescriptor;
46 import org.eclipse.jface.resource.DeviceResourceException;
47 import org.eclipse.jface.resource.DeviceResourceManager;
48 import org.eclipse.jface.resource.FontDescriptor;
49 import org.eclipse.jface.resource.ImageDescriptor;
50 import org.eclipse.jface.resource.JFaceResources;
51 import org.eclipse.jface.resource.LocalResourceManager;
52 import org.eclipse.jface.resource.ResourceManager;
53 import org.eclipse.jface.viewers.IPostSelectionProvider;
54 import org.eclipse.jface.viewers.ISelection;
55 import org.eclipse.jface.viewers.ISelectionChangedListener;
56 import org.eclipse.jface.viewers.ISelectionProvider;
57 import org.eclipse.jface.viewers.IStructuredSelection;
58 import org.eclipse.jface.viewers.SelectionChangedEvent;
59 import org.eclipse.jface.viewers.StructuredSelection;
60 import org.eclipse.jface.viewers.TreeSelection;
61 import org.eclipse.swt.SWT;
62 import org.eclipse.swt.SWTException;
63 import org.eclipse.swt.custom.CCombo;
64 import org.eclipse.swt.custom.TreeEditor;
65 import org.eclipse.swt.events.FocusEvent;
66 import org.eclipse.swt.events.FocusListener;
67 import org.eclipse.swt.events.KeyEvent;
68 import org.eclipse.swt.events.KeyListener;
69 import org.eclipse.swt.events.MouseEvent;
70 import org.eclipse.swt.events.MouseListener;
71 import org.eclipse.swt.events.SelectionEvent;
72 import org.eclipse.swt.events.SelectionListener;
73 import org.eclipse.swt.graphics.Color;
74 import org.eclipse.swt.graphics.Font;
75 import org.eclipse.swt.graphics.GC;
76 import org.eclipse.swt.graphics.Image;
77 import org.eclipse.swt.graphics.Point;
78 import org.eclipse.swt.graphics.RGB;
79 import org.eclipse.swt.graphics.Rectangle;
80 import org.eclipse.swt.widgets.Composite;
81 import org.eclipse.swt.widgets.Control;
82 import org.eclipse.swt.widgets.Display;
83 import org.eclipse.swt.widgets.Event;
84 import org.eclipse.swt.widgets.Listener;
85 import org.eclipse.swt.widgets.ScrollBar;
86 import org.eclipse.swt.widgets.Shell;
87 import org.eclipse.swt.widgets.Text;
88 import org.eclipse.swt.widgets.Tree;
89 import org.eclipse.swt.widgets.TreeColumn;
90 import org.eclipse.swt.widgets.TreeItem;
91 import org.eclipse.ui.IWorkbenchPart;
92 import org.eclipse.ui.IWorkbenchSite;
93 import org.eclipse.ui.PlatformUI;
94 import org.eclipse.ui.contexts.IContextActivation;
95 import org.eclipse.ui.contexts.IContextService;
96 import org.eclipse.ui.services.IServiceLocator;
97 import org.eclipse.ui.swt.IFocusService;
98 import org.simantics.browsing.ui.BuiltinKeys;
99 import org.simantics.browsing.ui.CheckedState;
100 import org.simantics.browsing.ui.Column;
101 import org.simantics.browsing.ui.Column.Align;
102 import org.simantics.browsing.ui.DataSource;
103 import org.simantics.browsing.ui.ExplorerState;
104 import org.simantics.browsing.ui.GraphExplorer;
105 import org.simantics.browsing.ui.NodeContext;
106 import org.simantics.browsing.ui.NodeContext.CacheKey;
107 import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey;
108 import org.simantics.browsing.ui.NodeContext.QueryKey;
109 import org.simantics.browsing.ui.NodeContextPath;
110 import org.simantics.browsing.ui.NodeQueryManager;
111 import org.simantics.browsing.ui.NodeQueryProcessor;
112 import org.simantics.browsing.ui.PrimitiveQueryProcessor;
113 import org.simantics.browsing.ui.SelectionDataResolver;
114 import org.simantics.browsing.ui.SelectionFilter;
115 import org.simantics.browsing.ui.StatePersistor;
116 import org.simantics.browsing.ui.common.ColumnKeys;
117 import org.simantics.browsing.ui.common.ErrorLogger;
118 import org.simantics.browsing.ui.common.NodeContextBuilder;
119 import org.simantics.browsing.ui.common.NodeContextUtil;
120 import org.simantics.browsing.ui.common.internal.GECache;
121 import org.simantics.browsing.ui.common.internal.GENodeQueryManager;
122 import org.simantics.browsing.ui.common.internal.IGECache;
123 import org.simantics.browsing.ui.common.internal.IGraphExplorerContext;
124 import org.simantics.browsing.ui.common.internal.UIElementReference;
125 import org.simantics.browsing.ui.common.processors.DefaultCheckedStateProcessor;
126 import org.simantics.browsing.ui.common.processors.DefaultComparableChildrenProcessor;
127 import org.simantics.browsing.ui.common.processors.DefaultFinalChildrenProcessor;
128 import org.simantics.browsing.ui.common.processors.DefaultImageDecoratorProcessor;
129 import org.simantics.browsing.ui.common.processors.DefaultImagerFactoriesProcessor;
130 import org.simantics.browsing.ui.common.processors.DefaultImagerProcessor;
131 import org.simantics.browsing.ui.common.processors.DefaultLabelDecoratorProcessor;
132 import org.simantics.browsing.ui.common.processors.DefaultLabelerFactoriesProcessor;
133 import org.simantics.browsing.ui.common.processors.DefaultLabelerProcessor;
134 import org.simantics.browsing.ui.common.processors.DefaultPrunedChildrenProcessor;
135 import org.simantics.browsing.ui.common.processors.DefaultSelectedImageDecoratorFactoriesProcessor;
136 import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelDecoratorFactoriesProcessor;
137 import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelerProcessor;
138 import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointFactoryProcessor;
139 import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointProcessor;
140 import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionProcessor;
141 import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionsProcessor;
142 import org.simantics.browsing.ui.common.processors.DefaultViewpointProcessor;
143 import org.simantics.browsing.ui.common.processors.IsExpandedProcessor;
144 import org.simantics.browsing.ui.common.processors.NoSelectionRequestProcessor;
145 import org.simantics.browsing.ui.common.processors.ProcessorLifecycle;
146 import org.simantics.browsing.ui.content.ImageDecorator;
147 import org.simantics.browsing.ui.content.Imager;
148 import org.simantics.browsing.ui.content.LabelDecorator;
149 import org.simantics.browsing.ui.content.Labeler;
150 import org.simantics.browsing.ui.content.Labeler.CustomModifier;
151 import org.simantics.browsing.ui.content.Labeler.DeniedModifier;
152 import org.simantics.browsing.ui.content.Labeler.DialogModifier;
153 import org.simantics.browsing.ui.content.Labeler.EnumerationModifier;
154 import org.simantics.browsing.ui.content.Labeler.FilteringModifier;
155 import org.simantics.browsing.ui.content.Labeler.LabelerListener;
156 import org.simantics.browsing.ui.content.Labeler.Modifier;
157 import org.simantics.browsing.ui.content.PrunedChildrenResult;
158 import org.simantics.browsing.ui.model.nodetypes.EntityNodeType;
159 import org.simantics.browsing.ui.model.nodetypes.NodeType;
160 import org.simantics.browsing.ui.swt.internal.Threads;
161 import org.simantics.db.layer0.SelectionHints;
162 import org.simantics.utils.ObjectUtils;
163 import org.simantics.utils.datastructures.BijectionMap;
164 import org.simantics.utils.datastructures.BinaryFunction;
165 import org.simantics.utils.datastructures.disposable.AbstractDisposable;
166 import org.simantics.utils.datastructures.hints.IHintContext;
167 import org.simantics.utils.threads.IThreadWorkQueue;
168 import org.simantics.utils.threads.SWTThread;
169 import org.simantics.utils.threads.ThreadUtils;
170 import org.simantics.utils.ui.ISelectionUtils;
171 import org.simantics.utils.ui.jface.BasePostSelectionProvider;
172 import org.simantics.utils.ui.widgets.VetoingEventHandler;
173 import org.simantics.utils.ui.workbench.WorkbenchUtils;
174
175 import gnu.trove.map.hash.THashMap;
176 import gnu.trove.procedure.TObjectProcedure;
177 import gnu.trove.set.hash.THashSet;
178
179 /**
180  * @see #getMaxChildren()
181  * @see #setMaxChildren(int)
182  * @see #getMaxChildren(NodeQueryManager, NodeContext)
183  */
184 class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
185
186         private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
187                 
188                 private GraphExplorerImpl ge;
189                 
190                 GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
191                         this.ge = ge;
192                 }
193                 
194                 void dispose() {
195                         ge = null;
196                 }
197                 
198             @Override
199             public void setSelection(final ISelection selection) {
200                 if(ge == null) return;
201                 ge.setSelection(selection, false);
202             }
203             
204
205             @Override
206             public void removeSelectionChangedListener(ISelectionChangedListener listener) {
207                 if(ge == null) return;
208                 if(ge.isDisposed()) {
209                     if (DEBUG_SELECTION_LISTENERS)
210                         System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);
211                     return;
212                 }
213                 //assertNotDisposed();
214                 //System.out.println("Remove selection changed listener: " + listener);
215                 ge.selectionProvider.removeSelectionChangedListener(listener);
216             }
217             
218             @Override
219             public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
220                 if(ge == null) return;
221                 if (!ge.thread.currentThreadAccess())
222                     throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
223                 if(ge.isDisposed()) {
224                     System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);
225                     return;
226                 }
227                 //System.out.println("Add POST selection changed listener: " + listener);
228                 ge.selectionProvider.addPostSelectionChangedListener(listener);
229             }
230
231             @Override
232             public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
233                 if(ge == null) return;
234                 if(ge.isDisposed()) {
235                     if (DEBUG_SELECTION_LISTENERS)
236                         System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);
237                     return;
238                 }
239 //              assertNotDisposed();
240                 //System.out.println("Remove POST selection changed listener: " + listener);
241                 ge.selectionProvider.removePostSelectionChangedListener(listener);
242             }
243             
244
245             @Override
246             public void addSelectionChangedListener(ISelectionChangedListener listener) {
247                 if(ge == null) return;
248                 if (!ge.thread.currentThreadAccess())
249                     throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
250                 //System.out.println("Add selection changed listener: " + listener);
251                 if (ge.tree.isDisposed() || ge.selectionProvider == null) {
252                     System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);
253                     return;
254                 }
255
256                 ge.selectionProvider.addSelectionChangedListener(listener);
257             }
258
259             
260             @Override
261             public ISelection getSelection() {
262                 if(ge == null) return StructuredSelection.EMPTY;
263                 if (!ge.thread.currentThreadAccess())
264                     throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());
265                 if (ge.tree.isDisposed() || ge.selectionProvider == null)
266                     return StructuredSelection.EMPTY;
267                 return ge.selectionProvider.getSelection();
268             }
269             
270         }
271         
272     /**
273      * If this explorer is running with an Eclipse workbench open, this
274      * Workbench UI context will be activated whenever inline editing is started
275      * through {@link #startEditing(TreeItem, int)} and deactivated when inline
276      * editing finishes.
277      * 
278      * This context information can be used to for UI handler activity testing.
279      */
280     private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
281
282     private static final String KEY_DRAG_COLUMN = "dragColumn";
283
284     private static final boolean                   DEBUG_SELECTION_LISTENERS = false;
285
286     private static final int                       DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
287
288     public static final int                        DEFAULT_MAX_CHILDREN                    = 1000;
289
290     private static final long                      POST_SELECTION_DELAY                    = 300;
291
292     /**
293      * The time in milliseconds that must elapse between consecutive
294      * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)}
295      * invocations in order for this class to construct a new selection.
296      * 
297      * <p>
298      * This is done because selection construction can be very expensive as the
299      * selected set grows larger when the user is pressing shift+arrow keys.
300      * GraphExplorerImpl will naturally listen to all changes in the tree
301      * selection, but as an optimization will not construct new
302      * StructuredSelection instances for every selection change event. A new
303      * selection will be constructed and set only if the selection hasn't
304      * changed for the amount of milliseconds specified by this constant.
305      */
306     private static final long                      SELECTION_CHANGE_QUIET_TIME             = 150;
307
308     private final IThreadWorkQueue                 thread;
309
310     /**
311      * Local method for checking from whether resources are loaded in
312      * JFaceResources.
313      */
314     private final LocalResourceManager             localResourceManager;
315
316     /**
317      * Local device resource manager that is safe to use in
318      * {@link ImageLoaderJob} for creating images in a non-UI thread.
319      */
320     private final ResourceManager                  resourceManager;
321
322     /*
323      * Package visibility.
324      * TODO: Get rid of these.
325      */
326     Tree                                           tree;
327
328     @SuppressWarnings({ "rawtypes" })
329     final HashMap<CacheKey<?>, NodeQueryProcessor> processors            = new HashMap<CacheKey<?>, NodeQueryProcessor>();
330     @SuppressWarnings({ "rawtypes" })
331     final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors   = new HashMap<Object, PrimitiveQueryProcessor>();
332     @SuppressWarnings({ "rawtypes" })
333     final HashMap<Class, DataSource>               dataSources           = new HashMap<Class, DataSource>();
334     
335     class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
336         // This is for query debugging only.
337         int                  queryIndent   = 0;
338
339         GECache              cache         = new GECache();
340         AtomicBoolean        propagating   = new AtomicBoolean(false);
341         Object               propagateList = new Object();
342         Object               propagate     = new Object();
343         List<Runnable>       scheduleList  = new ArrayList<Runnable>();
344         final Deque<Integer> activity      = new LinkedList<Integer>();
345         int                  activityInt   = 0;
346
347         /**
348          * Stores the currently running query update runnable. If
349          * <code>null</code> there's nothing scheduled yet in which case
350          * scheduling can commence. Otherwise the update should be skipped.
351          */
352         AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<Runnable>();
353
354         /**
355          * Keeps track of nodes that have already been auto-expanded. After
356          * being inserted into this set, nodes will not be forced to stay in an
357          * expanded state after that. This makes it possible for the user to
358          * close auto-expanded nodes.
359          */
360         Map<NodeContext, Boolean>     autoExpanded  = new WeakHashMap<NodeContext, Boolean>();
361
362         
363         @Override
364         protected void doDispose() {
365                 saveState();
366             autoExpanded.clear();
367         }
368
369         @Override
370         public IGECache getCache() {
371             return cache;
372         }
373
374         @Override
375         public int queryIndent() {
376             return queryIndent;
377         }
378
379         @Override
380         public int queryIndent(int offset) {
381             queryIndent += offset;
382             return queryIndent;
383         }
384
385         @Override
386         @SuppressWarnings("unchecked")
387         public <T> NodeQueryProcessor<T> getProcessor(Object o) {
388             return processors.get(o);
389         }
390
391         @Override
392         @SuppressWarnings("unchecked")
393         public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(Object o) {
394             return primitiveProcessors.get(o);
395         }
396
397         @SuppressWarnings("unchecked")
398         @Override
399         public <T> DataSource<T> getDataSource(Class<T> clazz) {
400             return dataSources.get(clazz);
401         }
402
403         @Override
404         public void update(UIElementReference ref) {
405             //System.out.println("GE.update " + ref);
406             TreeItemReference tiref = (TreeItemReference) ref;
407             TreeItem item = tiref.getItem();
408             // NOTE: must be called regardless of the the item value.
409             // A null item is currently used to indicate a tree root update.
410             GraphExplorerImpl.this.update(item);
411         }
412
413         @Override
414         public Object getPropagateLock() {
415             return propagate;
416         }
417
418         @Override
419         public Object getPropagateListLock() {
420             return propagateList;
421         }
422
423         @Override
424         public boolean isPropagating() {
425             return propagating.get();
426         }
427
428         @Override
429         public void setPropagating(boolean b) {
430             this.propagating.set(b);
431         }
432
433         @Override
434         public List<Runnable> getScheduleList() {
435             return scheduleList;
436         }
437
438         @Override
439         public void setScheduleList(List<Runnable> list) {
440             this.scheduleList = list;
441         }
442
443         @Override
444         public Deque<Integer> getActivity() {
445             return activity;
446         }
447
448         @Override
449         public void setActivityInt(int i) {
450             this.activityInt = i;
451         }
452
453         @Override
454         public int getActivityInt() {
455             return activityInt;
456         }
457
458         @Override
459         public void scheduleQueryUpdate(Runnable r) {
460             if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
461                 return;
462             //System.out.println("Scheduling query update for runnable " + r);
463             if (currentQueryUpdater.compareAndSet(null, r)) {
464                 //System.out.println("Scheduling query update for runnable " + r);
465                 queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);
466             }
467         }
468
469         Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
470             @Override
471             public void run() {
472                 Runnable r = currentQueryUpdater.getAndSet(null);
473                 if (r != null) {
474                     //System.out.println("Running query update runnable " + r);
475                     r.run();
476                 }
477             }
478         };
479     }
480
481     GraphExplorerContext                         explorerContext     = new GraphExplorerContext();
482
483     HashSet<TreeItem>                            pendingItems        = new HashSet<TreeItem>();
484     boolean                                      updating            = false;
485     boolean                                      pendingRoot         = false;
486
487     @SuppressWarnings("deprecation")
488     ModificationContext                          modificationContext = null;
489
490     NodeContext                                  rootContext;
491
492     StatePersistor                               persistor           = null;
493
494     boolean                                      editable            = true;
495
496     /**
497      * This is a reverse mapping from {@link NodeContext} tree objects back to
498      * their owner TreeItems.
499      * 
500      * <p>
501      * Access this map only in the SWT thread to keep it thread-safe.
502      * </p>
503      */
504     BijectionMap<NodeContext, TreeItem>         contextToItem     = new BijectionMap<NodeContext, TreeItem>();
505
506     /**
507      * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
508      * initialize.
509      */
510     Column[]                                     columns           = new Column[0];
511     Map<String, Integer>                         columnKeyToIndex  = new HashMap<String, Integer>();
512     boolean                                      refreshingColumnSizes = false;
513     boolean                                      columnsAreVisible = true;
514
515     /**
516      * An array reused for invoking {@link TreeItem#setImage(Image[])} instead
517      * of constantly allocating new arrays for setting each TreeItems images.
518      * This works because {@link TreeItem#setImage(Image[])} does not take hold
519      * of the array itself, only the contents of the array.
520      * 
521      * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
522      */
523     Image[]                                      columnImageArray = { null };
524
525     /**
526      * Used for collecting Image or ImageDescriptor instances for a single
527      * TreeItem when initially setting images for a TreeItem.
528      * 
529      * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
530      */
531     Object[]                                     columnDescOrImageArray = { null };
532
533     final ExecutorService                        queryUpdateScheduler = Threads.getExecutor();
534     final ScheduledExecutorService               uiUpdateScheduler    = ThreadUtils.getNonBlockingWorkExecutor();
535
536     /** Set to true when the Tree widget is disposed. */
537     private boolean                              disposed                 = false;
538     private final CopyOnWriteArrayList<FocusListener>  focusListeners           = new CopyOnWriteArrayList<FocusListener>();
539     private final CopyOnWriteArrayList<MouseListener>  mouseListeners           = new CopyOnWriteArrayList<MouseListener>();
540     private final CopyOnWriteArrayList<KeyListener>    keyListeners             = new CopyOnWriteArrayList<KeyListener>();
541
542     /** Selection provider */
543     private   GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
544     protected BasePostSelectionProvider          selectionProvider        = new BasePostSelectionProvider();
545     protected SelectionDataResolver              selectionDataResolver;
546     protected SelectionFilter                    selectionFilter;
547     protected BinaryFunction<Object[], GraphExplorer, Object[]>  selectionTransformation = new BinaryFunction<Object[], GraphExplorer, Object[]>() {
548
549         @Override
550         public Object[] call(GraphExplorer explorer, Object[] objects) {
551             Object[] result = new Object[objects.length];
552             for (int i = 0; i < objects.length; i++) {
553                 IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
554                 context.setHint(SelectionHints.KEY_MAIN, objects[i]);
555                 result[i] = context;
556             }
557             return result;
558         }
559
560     };
561     protected FontDescriptor                     originalFont;
562     protected ColorDescriptor                    originalForeground;
563     protected ColorDescriptor                    originalBackground;
564
565     /**
566      * The set of currently selected TreeItem instances. This set is needed
567      * because we need to know in {@link #setData(Event)} whether the updated
568      * item was a part of the current selection in which case the selection must
569      * be updated.
570      */
571     private final Map<TreeItem, NodeContext>     selectedItems            = new HashMap<TreeItem, NodeContext>();
572
573     /**
574      * TODO: specify what this is for
575      */
576     private final Set<NodeContext>               selectionRefreshContexts = new HashSet<NodeContext>();
577
578     /**
579      * If this field is non-null, it means that if {@link #setData(Event)}
580      * encounters a NodeContext equal to this one, it must make the TreeItem
581      * assigned to that NodeContext the topmost item of the tree using
582      * {@link Tree#setTopItem(TreeItem)}. After this the field value is
583      * nullified.
584      * 
585      * <p>
586      * This is related to {@link #initializeState()}, i.e. explorer state
587      * restoration.
588      */
589 //    private NodeContext[] topNodePath = NodeContext.NONE;
590 //    private int[] topNodePath = {};
591 //    private int currentTopNodePathIndex = -1;
592
593     /**
594      * See {@link #setAutoExpandLevel(int)}
595      */
596     private int autoExpandLevel = 0;
597
598     /**
599      * <code>null</code> if not explicitly set through
600      * {@link #setServiceLocator(IServiceLocator)}.
601      */
602     private IServiceLocator serviceLocator;
603
604     /**
605      * The global workbench context service, if the workbench is available.
606      * Retrieved in the constructor.
607      */
608     private IContextService contextService = null;
609
610     /**
611      * The global workbench IFocusService, if the workbench is available.
612      * Retrieved in the constructor.
613      */
614     private IFocusService focusService = null;
615
616     /**
617      * A Workbench UI context activation that is activated when starting inline
618      * editing through {@link #startEditing(TreeItem, int)}.
619      * 
620      * @see #activateEditingContext()
621      * @see #deactivateEditingContext()
622      */
623     private IContextActivation editingContext = null;
624
625     static class ImageTask {
626         NodeContext node;
627         TreeItem item;
628         Object[] descsOrImages;
629         public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
630             this.node = node;
631             this.item = item;
632             this.descsOrImages = descsOrImages;
633         }
634     }
635
636     /**
637      * The job that is used for off-loading image loading tasks (see
638      * {@link ImageTask} to a worker thread from the main UI thread.
639      * 
640      * @see #setPendingImages(IProgressMonitor)
641      */
642     ImageLoaderJob           imageLoaderJob;
643
644     /**
645      * The set of currently gathered up image loading tasks for
646      * {@link #imageLoaderJob} to execute.
647      * 
648      * @see #setPendingImages(IProgressMonitor)
649      */
650     Map<TreeItem, ImageTask> imageTasks     = new THashMap<TreeItem, ImageTask>();
651
652     /**
653      * A state flag indicating whether the vertical scroll bar was visible for
654      * {@link #tree} the last time it was checked. Since there is no listener
655      * that can provide this information, we check it in {@link #setData(Event)}
656      * every time any data for a TreeItem is updated. If the visibility changes,
657      * we will force re-layouting of the tree's parent composite.
658      * 
659      * @see #setData(Event)
660      */
661     private boolean verticalBarVisible = false;
662
663     static class TransientStateImpl implements TransientExplorerState {
664
665         private Integer activeColumn = null;
666         
667                 @Override
668                 public synchronized Integer getActiveColumn() {
669                         return activeColumn;
670                 }
671                 
672                 public synchronized void setActiveColumn(Integer column) {
673                         activeColumn = column;
674                 }
675         
676     }
677     
678     private TransientStateImpl transientState = new TransientStateImpl();
679     
680     boolean scheduleUpdater() {
681
682         if (tree.isDisposed())
683             return false;
684
685         if (pendingRoot == true || !pendingItems.isEmpty()) {
686             assert(!tree.isDisposed());
687
688             int activity = explorerContext.activityInt;
689             long delay = 30;
690             if (activity < 100) {
691 //                System.out.println("Scheduling update immediately.");
692             } else if (activity < 1000) {
693 //                System.out.println("Scheduling update after 500ms.");
694                 delay = 500;
695             } else {
696 //                System.out.println("Scheduling update after 3000ms.");
697                 delay = 3000;
698             }
699
700             updateCounter = 0;
701             
702             //System.out.println("Scheduling UI update after " + delay + " ms.");
703             uiUpdateScheduler.schedule(new Runnable() {
704                 @Override
705                 public void run() {
706                         
707                     if (tree.isDisposed())
708                         return;
709                     
710                     if (updateCounter > 0) {
711                         updateCounter = 0;
712                         uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
713                     } else {
714                         tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
715                     }
716                     
717                 }
718             }, delay, TimeUnit.MILLISECONDS);
719
720             updating = true;
721             return true;
722         }
723
724         return false;
725     }
726
727     int updateCounter = 0;
728     
729     void update(TreeItem item) {
730
731         synchronized(pendingItems) {
732                 
733 //              System.out.println("update " + item);
734                 
735                 updateCounter++;
736
737             if(item == null) pendingRoot = true;
738             else pendingItems.add(item);
739
740             if(updating == true) return;
741
742             scheduleUpdater();
743
744         }
745
746     }
747
748     private int maxChildren = DEFAULT_MAX_CHILDREN;
749
750     @Override
751     public int getMaxChildren() {
752         return maxChildren;
753     }
754
755     @Override
756     public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
757         Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
758         //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
759         if (result != null) {
760             if (result < 0)
761                 throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
762             return result;
763         }
764         return maxChildren;
765     }
766
767     @Override
768     public void setMaxChildren(int maxChildren) {
769         this.maxChildren = maxChildren;
770     }
771
772     @Override
773     public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
774         this.modificationContext = modificationContext;
775     }
776
777     /**
778      * @param parent the parent SWT composite
779      */
780     public GraphExplorerImpl(Composite parent) {
781         this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
782     }
783
784     /**
785      * Stores the node context and the modifier that is currently being
786      * modified. These are used internally to prevent duplicate edits from being
787      * initiated which should always be a sensible thing to do.
788      */
789     private Set<NodeContext> currentlyModifiedNodes   = new THashSet<NodeContext>();
790
791     private final TreeEditor editor;
792     private Color            invalidModificationColor = null;
793
794     /**
795      * @param item the TreeItem to start editing
796      * @param columnIndex the index of the column to edit, starts counting from
797      *        0
798      * @return <code>true</code> if the editing was initiated successfully or
799      *         <code>false</code> if editing could not be started due to lack of
800      *         {@link Modifier} for the labeler in question.
801      */
802     private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
803         if (!editable)
804             return "Rename not supported for selection";
805
806         GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
807         final NodeContext context = (NodeContext) item.getData();
808         Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
809         if (labeler == null)
810             return "Rename not supported for selection";
811
812         if(columnKey == null) columnKey = columns[columnIndex].getKey();
813
814         // columnKey might be prefixed with '#' to indicate
815         // textual editing is preferred. Try to get modifier
816         // for that first and only if it fails, try without
817         // the '#' prefix.
818         Modifier modifier = labeler.getModifier(modificationContext, columnKey);
819         if (modifier == null) {
820             if(columnKey.startsWith("#"))
821                 modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
822             if (modifier == null)
823                 return "Rename not supported for selection";
824         }
825         if (modifier instanceof DeniedModifier) {
826                 DeniedModifier dm = (DeniedModifier)modifier;
827                 return dm.getMessage();
828         }
829
830         // Prevent editing of a single node context multiple times.
831         if (currentlyModifiedNodes.contains(context)) {
832             //System.out.println("discarding duplicate edit for context " + context);
833             return "Rename not supported for selection";
834         }
835
836         // Clean up any previous editor control
837         Control oldEditor = editor.getEditor();
838         if (oldEditor != null)
839             oldEditor.dispose();
840
841         if (modifier instanceof DialogModifier) {
842             performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
843         } else if (modifier instanceof CustomModifier) {
844             startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
845         } else if (modifier instanceof EnumerationModifier) {
846             startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
847         } else {
848             startTextEditing(item, columnIndex, context, modifier);
849         }
850
851         return null;
852     }
853
854     /**
855      * @param item
856      * @param columnIndex
857      * @param context
858      * @param modifier
859      */
860     void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
861             final DialogModifier modifier) {
862         final AtomicBoolean disposed = new AtomicBoolean(false);
863         Consumer<String> callback = result -> {
864             if (disposed.get())
865                 return;
866             String error = modifier.isValid(result);
867             if (error == null) {
868                 modifier.modify(result);
869                 // Item may be disposed if the tree gets reset after a previous editing.
870                 if (!item.isDisposed()) {
871                     item.setText(columnIndex, result);
872                     queueSelectionRefresh(context);
873                 }
874             }
875         };
876
877         currentlyModifiedNodes.add(context);
878         try {
879             String status = modifier.query(tree, item, columnIndex, context, callback);
880             if (status != null)
881                 ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
882         } finally {
883             currentlyModifiedNodes.remove(context);
884             disposed.set(true);
885         }
886     }
887
888     private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
889         Point size = control.computeSize(widthHint, heightHint);
890         editor.horizontalAlignment = SWT.LEFT;
891         Rectangle itemRect = item.getBounds(columnIndex),
892                   rect = tree.getClientArea();
893         editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
894         int left = itemRect.x,
895             right = rect.x + rect.width;
896         editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
897         editor.minimumHeight = size.y + insetY * 2;
898         editor.layout();
899     }
900
901     void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
902         GC gc = new GC(control);
903         Point size = gc.textExtent(text);
904         gc.dispose();
905         reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
906     }
907
908     /**
909      * @param item
910      * @param columnIndex
911      * @param context
912      * @param modifier
913      */
914     void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
915             final CustomModifier modifier) {
916         final Object obj = modifier.createControl(tree, item, columnIndex, context);
917         if (!(obj instanceof Control))
918             throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
919         final Control control = (Control) obj;
920
921 //        final int insetX = 0;
922 //        final int insetY = 0;
923 //        control.addListener(SWT.Resize, new Listener() {
924 //            @Override
925 //            public void handleEvent(Event e) {
926 //                Rectangle rect = control.getBounds();
927 //                control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
928 //            }
929 //        });
930         control.addListener(SWT.Dispose, new Listener() {
931             @Override
932             public void handleEvent(Event event) {
933                 currentlyModifiedNodes.remove(context);
934                 queueSelectionRefresh(context);
935                 deactivateEditingContext();
936             }
937         });
938         
939         if (!(control instanceof Shell)) {
940             editor.setEditor(control, item, columnIndex);
941         }
942         
943
944         control.setFocus();
945
946         GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
947
948         activateEditingContext(control);
949
950         // Removed in disposeListener above
951         currentlyModifiedNodes.add(context);
952         //System.out.println("START CUSTOM EDITING: " + item);
953     }
954
955     /**
956      * @param item
957      * @param columnIndex
958      * @param context
959      * @param modifier
960      */
961     void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
962         String initialText = modifier.getValue();
963         if (initialText == null)
964             throw new AssertionError("Labeler.Modifier.getValue() returned null");
965
966         List<String> values = modifier.getValues();
967         String selectedValue = modifier.getValue();
968         int selectedIndex = values.indexOf(selectedValue);
969         if (selectedIndex == -1)
970             throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
971
972         final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
973         combo.setVisibleItemCount(10);
974         //combo.setEditable(false);
975
976         for (String value : values) {
977             combo.add(value);
978         }
979         combo.select(selectedIndex);
980
981         Listener comboListener = new Listener() {
982             boolean arrowTraverseUsed = false; 
983             @Override
984             public void handleEvent(final Event e) {
985                 //System.out.println("FOO: " + e);
986                 switch (e.type) {
987                     case SWT.KeyDown:
988                         if (e.character == SWT.CR) {
989                             // Commit edit directly on ENTER press.
990                             String text = combo.getText();
991                             modifier.modify(text);
992                             // Item may be disposed if the tree gets reset after a previous editing.
993                             if (!item.isDisposed()) {
994                                 item.setText(columnIndex, text);
995                                 queueSelectionRefresh(context);
996                             }
997                             combo.dispose();
998                             e.doit = false;
999                         } else if (e.keyCode == SWT.ESC) {
1000                             // Cancel editing immediately
1001                             combo.dispose();
1002                             e.doit = false;
1003                         }
1004                         break;
1005                     case SWT.Selection:
1006                     {
1007                         if (arrowTraverseUsed) {
1008                             arrowTraverseUsed = false;
1009                             return;
1010                         }
1011
1012                         String text = combo.getText();
1013                         modifier.modify(text);
1014
1015                         // Item may be disposed if the tree gets reset after a previous editing.
1016                         if (!item.isDisposed()) {
1017                             item.setText(columnIndex, text);
1018                             queueSelectionRefresh(context);
1019                         }
1020                         combo.dispose();
1021                         break;
1022                     }
1023                     case SWT.FocusOut: {
1024                         String text = combo.getText();
1025                         modifier.modify(text);
1026
1027                         // Item may be disposed if the tree gets reset after a previous editing.
1028                         if (!item.isDisposed()) {
1029                             item.setText(columnIndex, text);
1030                             queueSelectionRefresh(context);
1031                         }
1032                         combo.dispose();
1033                         break;
1034                     }
1035                     case SWT.Traverse: {
1036                         switch (e.detail) {
1037                             case SWT.TRAVERSE_RETURN:
1038                                 String text = combo.getText();
1039                                 modifier.modify(text);
1040                                 if (!item.isDisposed()) {
1041                                     item.setText(columnIndex, text);
1042                                     queueSelectionRefresh(context);
1043                                 }
1044                                 arrowTraverseUsed = false;
1045                                 // FALL THROUGH
1046                             case SWT.TRAVERSE_ESCAPE:
1047                                 combo.dispose();
1048                                 e.doit = false;
1049                                 break;
1050                             case SWT.TRAVERSE_ARROW_NEXT:
1051                             case SWT.TRAVERSE_ARROW_PREVIOUS:
1052                                 arrowTraverseUsed = true;
1053                                 break;
1054                             default:
1055                                 //System.out.println("unhandled traversal: " + e.detail);
1056                                 break;
1057                         }
1058                         break;
1059                     }
1060                     case SWT.Dispose:
1061                         currentlyModifiedNodes.remove(context);
1062                         deactivateEditingContext();
1063                         break;
1064                 }
1065             }
1066         };
1067         combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
1068         combo.addListener(SWT.KeyDown, comboListener);
1069         combo.addListener(SWT.FocusOut, comboListener);
1070         combo.addListener(SWT.Traverse, comboListener);
1071         combo.addListener(SWT.Selection, comboListener);
1072         combo.addListener(SWT.Dispose, comboListener);
1073
1074         editor.setEditor(combo, item, columnIndex);
1075
1076         combo.setFocus();
1077         combo.setListVisible(true);
1078
1079         GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
1080
1081         activateEditingContext(combo);
1082
1083         // Removed in comboListener
1084         currentlyModifiedNodes.add(context);
1085
1086         //System.out.println("START ENUMERATION EDITING: " + item);
1087     }
1088
1089     /**
1090      * @param item
1091      * @param columnIndex
1092      * @param context
1093      * @param modifier
1094      */
1095     void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {
1096         String initialText = modifier.getValue();
1097         if (initialText == null)
1098             throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);
1099
1100         final Composite composite = new Composite(tree, SWT.NONE);
1101         //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));
1102         final Text text = new Text(composite, SWT.BORDER);
1103         final int insetX = 0;
1104         final int insetY = 0;
1105         composite.addListener(SWT.Resize, new Listener() {
1106             @Override
1107             public void handleEvent(Event e) {
1108                 Rectangle rect = composite.getClientArea();
1109                 text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height
1110                         - insetY * 2);
1111             }
1112         });
1113         final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
1114         Listener textListener = new Listener() {
1115                 
1116                 boolean modified = false;
1117                 
1118             @Override
1119             public void handleEvent(final Event e) {
1120                 String error;
1121                 String newText;
1122                 switch (e.type) {
1123                     case SWT.FocusOut:
1124                         if(modified) {
1125                                 //System.out.println("FOCUS OUT " + item);
1126                                 newText = text.getText();
1127                                 error = modifier.isValid(newText);
1128                                 if (error == null) {
1129                                         modifier.modify(newText);
1130
1131                                         // Item may be disposed if the tree gets reset after a previous editing.
1132                                         if (!item.isDisposed()) {
1133                                                 item.setText(columnIndex, newText);
1134                                                 queueSelectionRefresh(context);
1135                                         }
1136                                 } else {
1137                                         //                                System.out.println("validation error: " + error);
1138                                 }
1139                         }
1140                         composite.dispose();
1141                         break;
1142                     case SWT.Modify:
1143                         newText = text.getText();
1144                         error = modifier.isValid(newText);
1145                         if (error != null) {
1146                             text.setBackground(invalidModificationColor);
1147                             errorStatus(error);
1148                             //System.out.println("validation error: " + error);
1149                         } else {
1150                             text.setBackground(null);
1151                             errorStatus(null);
1152                         }
1153                         modified = true;
1154                         break;
1155                     case SWT.Verify:
1156                         
1157                         // Safety check since it seems that this may happen with
1158                         // virtual trees.
1159                         if (item.isDisposed())
1160                             return;
1161
1162                         // Filter input if necessary
1163                         e.text = filter != null ? filter.filter(e.text) : e.text;
1164
1165                         newText = text.getText();
1166                         String leftText = newText.substring(0, e.start);
1167                         String rightText = newText.substring(e.end, newText.length());
1168                         GraphExplorerImpl.this.reconfigureTreeEditorForText(
1169                                 item, columnIndex, text, leftText + e.text + rightText,
1170                                 SWT.DEFAULT, insetX, insetY);
1171                         break;
1172                     case SWT.Traverse:
1173                         switch (e.detail) {
1174                             case SWT.TRAVERSE_RETURN:
1175                                 if(modified) {
1176                                         newText = text.getText();
1177                                         error = modifier.isValid(newText);
1178                                         if (error == null) {
1179                                                 modifier.modify(newText);
1180                                                 if (!item.isDisposed()) {
1181                                                         item.setText(columnIndex, newText);
1182                                                         queueSelectionRefresh(context);
1183                                                 }
1184                                         }
1185                                 }
1186                                 // FALL THROUGH
1187                             case SWT.TRAVERSE_ESCAPE:
1188                                 composite.dispose();
1189                                 e.doit = false;
1190                                 break;
1191                             default:
1192                                 //System.out.println("unhandled traversal: " + e.detail);
1193                                 break;
1194                         }
1195                         break;
1196
1197                     case SWT.Dispose:
1198                         currentlyModifiedNodes.remove(context);
1199                         deactivateEditingContext();
1200                         errorStatus(null);
1201                         break;
1202                 }
1203             }
1204         };
1205         // Set the initial text before registering a listener. We do not want immediate modification!
1206         text.setText(initialText);
1207         text.addListener(SWT.FocusOut, textListener);
1208         text.addListener(SWT.Traverse, textListener);
1209         text.addListener(SWT.Verify, textListener);
1210         text.addListener(SWT.Modify, textListener);
1211         text.addListener(SWT.Dispose, textListener);
1212         editor.setEditor(composite, item, columnIndex);
1213         text.selectAll();
1214         text.setFocus();
1215
1216         // Initialize TreeEditor properly.
1217         GraphExplorerImpl.this.reconfigureTreeEditorForText(
1218                 item, columnIndex, text, initialText,
1219                 SWT.DEFAULT, insetX, insetY);
1220
1221         // Removed in textListener
1222         currentlyModifiedNodes.add(context);
1223
1224         activateEditingContext(text);
1225
1226         //System.out.println("START TEXT EDITING: " + item);
1227     }
1228
1229     protected void errorStatus(String error) {
1230         IStatusLineManager status = getStatusLineManager();
1231         if (status != null) {
1232             status.setErrorMessage(error);
1233         }
1234     }
1235
1236     protected IStatusLineManager getStatusLineManager() {
1237         if (serviceLocator instanceof IWorkbenchPart) {
1238             return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);
1239         } else if (serviceLocator instanceof IWorkbenchSite) {
1240             return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);
1241         }
1242         return null;
1243     }
1244
1245     protected void activateEditingContext(Control control) {
1246         if (contextService != null) {
1247             editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
1248         }
1249         if (control != null && focusService != null) {
1250             focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);
1251             // No need to remove the control, it will be
1252             // removed automatically when it is disposed.
1253         }
1254     }
1255
1256     protected void deactivateEditingContext() {
1257         IContextActivation a = editingContext;
1258         if (a != null) {
1259             editingContext = null;
1260             contextService.deactivateContext(a);
1261         }
1262     }
1263
1264     /**
1265      * @param forContext
1266      */
1267     void queueSelectionRefresh(NodeContext forContext) {
1268         selectionRefreshContexts.add(forContext);
1269     }
1270
1271     @Override
1272     public String startEditing(NodeContext context, String columnKey_) {
1273         assertNotDisposed();
1274         if (!thread.currentThreadAccess())
1275             throw new IllegalStateException("not in SWT display thread " + thread.getThread());
1276
1277         String columnKey = columnKey_;
1278         if(columnKey.startsWith("#")) {
1279                 columnKey = columnKey.substring(1);
1280         }
1281         
1282         Integer columnIndex = columnKeyToIndex.get(columnKey);
1283         if (columnIndex == null)
1284             return "Rename not supported for selection";
1285
1286         TreeItem item = contextToItem.getRight(context);
1287         if (item == null)
1288             return "Rename not supported for selection";
1289
1290         return startEditing(item, columnIndex, columnKey_);
1291         
1292     }
1293
1294     @Override
1295     public String startEditing(String columnKey) {
1296
1297         ISelection selection = postSelectionProvider.getSelection();
1298         if(selection == null) return "Rename not supported for selection";
1299         NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
1300         if(context == null) return "Rename not supported for selection";
1301
1302         return startEditing(context, columnKey);
1303
1304     }
1305
1306     /**
1307      * @param site <code>null</code> if the explorer is detached from the workbench
1308      * @param parent the parent SWT composite
1309      * @param style the tree style to use, check the see tags for the available flags
1310      * 
1311      * @see SWT#SINGLE
1312      * @see SWT#MULTI
1313      * @see SWT#CHECK
1314      * @see SWT#FULL_SELECTION
1315      * @see SWT#NO_SCROLL
1316      * @see SWT#H_SCROLL
1317      * @see SWT#V_SCROLL
1318      */
1319     public GraphExplorerImpl(Composite parent, int style) {
1320
1321         setServiceLocator(null);
1322
1323         this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
1324         this.resourceManager = new DeviceResourceManager(parent.getDisplay());
1325
1326         this.imageLoaderJob = new ImageLoaderJob(this);
1327         this.imageLoaderJob.setPriority(Job.DECORATE);
1328
1329         invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
1330
1331         this.thread = SWTThread.getThreadAccess(parent);
1332
1333         for(int i=0;i<10;i++) explorerContext.activity.push(0);
1334
1335         tree = new Tree(parent, style);
1336         tree.addListener(SWT.SetData, this);
1337         tree.addListener(SWT.Expand, this);
1338         tree.addListener(SWT.Dispose, this);
1339         tree.addListener(SWT.Activate, this);
1340
1341         tree.setData(KEY_GRAPH_EXPLORER, this);
1342
1343         // These are both required for performing column resizing without flicker.
1344         // See SWT.Resize event handling in #handleEvent() for more explanations.
1345         parent.addListener(SWT.Resize, this);
1346         tree.addListener(SWT.Resize, this);
1347
1348         originalFont = JFaceResources.getDefaultFontDescriptor();
1349 //        originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
1350 //        originalForeground = tree.getForeground();
1351
1352         tree.setFont((Font) localResourceManager.get(originalFont));
1353
1354         columns = new Column[] { new Column(ColumnKeys.SINGLE) };
1355         columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
1356
1357         editor = new TreeEditor(tree);
1358         editor.horizontalAlignment = SWT.LEFT;
1359         editor.grabHorizontal = true;
1360         editor.minimumWidth = 50;
1361
1362         setBasicListeners();
1363         setDefaultProcessors();
1364         
1365         this.toolTip = new GraphExplorerToolTip(explorerContext, tree);
1366     }
1367
1368     @Override
1369     public IThreadWorkQueue getThread() {
1370         return thread;
1371     }
1372
1373     TreeItem previousSingleSelection = null;
1374     long focusGainedAt = Long.MIN_VALUE;
1375
1376     protected GraphExplorerToolTip toolTip;
1377
1378     protected void setBasicListeners() {
1379         // Keep track of the previous single selection to help
1380         // decide whether to start editing a tree node on mouse
1381         // downs or not.
1382         tree.addListener(SWT.Selection, new Listener() {
1383             @Override
1384             public void handleEvent(Event event) {
1385                 TreeItem[] selection = tree.getSelection();
1386                 if (selection.length == 1) {
1387                     //for (TreeItem item : selection)
1388                     //    System.out.println("selection: " + item);
1389                     previousSingleSelection = selection[0];
1390                 } else {
1391                     previousSingleSelection = null;
1392                 }
1393             }
1394         });
1395
1396         // Try to start editing of tree column when clicked for the second time.
1397         Listener mouseEditListener = new Listener() {
1398
1399             Future<?> startEdit = null;
1400
1401             @Override
1402             public void handleEvent(Event event) {
1403                 if (event.type == SWT.DragDetect) {
1404                     // Needed to prevent editing from being started when in fact
1405                     // the user starts dragging an item.
1406                     //System.out.println("DRAG DETECT: " + event);
1407                     cancelEdit();
1408                     return;
1409                 }
1410                 //System.out.println("mouse down: " + event);
1411                 if (event.button == 1) {
1412                     // Always ignore the first mouse button press that focuses
1413                     // the control. Do not let it start in-line editing since
1414                     // that is very annoying to users and not how the UI's that
1415                     // people are used to behave.
1416                     long eventTime = ((long) event.time) & 0xFFFFFFFFL;
1417                     if ((eventTime - focusGainedAt) < 250L) {
1418                         //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));
1419                         return;
1420                     }
1421                     //System.out.println("testing whether to start editing");
1422
1423                     final Point point = new Point(event.x, event.y);
1424                     final TreeItem item = tree.getItem(point);
1425                     if (item == null)
1426                         return;
1427                     //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
1428
1429                     // Only start editing if the item was already selected.
1430                     if (!item.equals(previousSingleSelection)) {
1431                         cancelEdit();
1432                         return;
1433                     }
1434
1435                     if (tree.getColumnCount() > 1) {
1436                         // TODO: reconsider this logic, might not be good in general.
1437                         for (int i = 0; i < tree.getColumnCount(); i++) {
1438                             if (item.getBounds(i).contains(point)) {
1439                                 tryScheduleEdit(event, item, point, 100, i);
1440                                 return;
1441                             }
1442                         }
1443                     } else {
1444                         //System.out.println("clicks: " + event.count);
1445                         if (item.getBounds().contains(point)) {
1446                             if (event.count == 1) {
1447                                 tryScheduleEdit(event, item, point, 500, 0);
1448                             } else {
1449                                 cancelEdit();
1450                             }
1451                         }
1452                     }
1453                 }
1454             }
1455
1456             void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {
1457                 //System.out.println("\tCONTAINS: " + item);
1458                 if (!cancelEdit())
1459                     return;
1460
1461                 //System.out.println("\tScheduling edit: " + item);
1462                 startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
1463                     @Override
1464                     public void run() {
1465                         ThreadUtils.asyncExec(thread, new Runnable() {
1466                             @Override
1467                             public void run() {
1468                                 if (item.isDisposed())
1469                                     return;
1470                                 startEditing(item, column, null);
1471                             }
1472                         });
1473                     }
1474                 }, delayMs, TimeUnit.MILLISECONDS);
1475             }
1476
1477             boolean cancelEdit() {
1478                 Future<?> f = startEdit;
1479                 if (f != null) {
1480                     // Try to cancel the start edit task if it's not running yet.
1481                     startEdit = null;
1482                     if (!f.isDone()) {
1483                         boolean ret = f.cancel(false);
1484                         //System.out.println("\tCancelled edit: " + ret);
1485                         return ret;
1486                     }
1487                 }
1488                 //System.out.println("\tNo edit in progress to cancel");
1489                 return true;
1490             }
1491         };
1492         tree.addListener(SWT.MouseDown, mouseEditListener);
1493         tree.addListener(SWT.DragDetect, mouseEditListener);
1494         tree.addListener(SWT.DragDetect, new Listener() {
1495             @Override
1496             public void handleEvent(Event event) {
1497                 Point test = new Point(event.x, event.y);
1498                 TreeItem item = tree.getItem(test);
1499                 if(item != null) {
1500                     for(int i=0;i<tree.getColumnCount();i++) {
1501                         Rectangle rect = item.getBounds(i);
1502                         if(rect.contains(test)) {
1503                             tree.setData(KEY_DRAG_COLUMN, i);
1504                             return;
1505                         }
1506                     }
1507                 }
1508                 tree.setData(KEY_DRAG_COLUMN, -1);
1509             }
1510         });
1511         tree.addListener(SWT.MouseMove, new Listener() {
1512             @Override
1513             public void handleEvent(Event event) {
1514                 Point test = new Point(event.x, event.y);
1515                 TreeItem item = tree.getItem(test);
1516                 if(item != null) {
1517                     for(int i=0;i<tree.getColumnCount();i++) {
1518                         Rectangle rect = item.getBounds(i);
1519                         if(rect.contains(test)) {
1520                                 transientState.setActiveColumn(i);
1521                             return;
1522                         }
1523                     }
1524                 }
1525                 transientState.setActiveColumn(null);
1526             }
1527         });
1528
1529         // Add focus/mouse/key listeners for supporting the respective
1530         // add/remove listener methods in IGraphExplorer.
1531         tree.addFocusListener(new FocusListener() {
1532             @Override
1533             public void focusGained(FocusEvent e) {
1534                 focusGainedAt = ((long) e.time) & 0xFFFFFFFFL;
1535                 for (FocusListener listener : focusListeners)
1536                     listener.focusGained(e);
1537             }
1538             @Override
1539             public void focusLost(FocusEvent e) {
1540                 for (FocusListener listener : focusListeners)
1541                     listener.focusLost(e);
1542             }
1543         });
1544         tree.addMouseListener(new MouseListener() {
1545             @Override
1546             public void mouseDoubleClick(MouseEvent e) {
1547                 for (MouseListener listener : mouseListeners) {
1548                     listener.mouseDoubleClick(e);
1549                 }
1550             }
1551             @Override
1552             public void mouseDown(MouseEvent e) {
1553                 for (MouseListener listener : mouseListeners) {
1554                     listener.mouseDown(e);
1555                 }
1556             }
1557             @Override
1558             public void mouseUp(MouseEvent e) {
1559                 for (MouseListener listener : mouseListeners) {
1560                     listener.mouseUp(e);
1561                 }
1562             }
1563         });
1564         tree.addKeyListener(new KeyListener() {
1565             @Override
1566             public void keyPressed(KeyEvent e) {
1567                 for (KeyListener listener : keyListeners) {
1568                     listener.keyPressed(e);
1569                 }
1570             }
1571             @Override
1572             public void keyReleased(KeyEvent e) {
1573                 for (KeyListener listener : keyListeners) {
1574                     listener.keyReleased(e);
1575                 }
1576             }
1577         });
1578
1579                 // Add a tree selection listener for keeping the selection of
1580                 // GraphExplorer's ISelectionProvider up-to-date.
1581         tree.addSelectionListener(new SelectionListener() {
1582             @Override
1583             public void widgetDefaultSelected(SelectionEvent e) {
1584                 widgetSelected(e);
1585             }
1586             @Override
1587             public void widgetSelected(SelectionEvent e) {
1588                 widgetSelectionChanged(false);
1589             }
1590         });
1591
1592         // This listener takes care of updating the set of currently selected
1593         // TreeItem instances. This set is needed because we need to know in
1594         // #setData(Event) whether the updated item was a part of the current
1595         // selection in which case the selection must be updated.
1596         selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
1597             @Override
1598             public void selectionChanged(SelectionChangedEvent event) {
1599                 //System.out.println("selection changed: " + event.getSelection());
1600                 Set<NodeContext> set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);
1601                 selectedItems.clear();
1602                 for (NodeContext nc : set) {
1603                     TreeItem item = contextToItem.getRight(nc);
1604                     if (item != null)
1605                         selectedItems.put(item, nc);
1606                 }
1607                 //System.out.println("newly selected items: " + selectedItems);
1608             }
1609         });
1610     }
1611
1612     /**
1613      * Mod count for delaying post selection changed events.
1614      */
1615     int postSelectionModCount = 0;
1616
1617     /**
1618      * Last tree selection modification time for implementing a quiet
1619      * time for selection changes.
1620      */
1621     long lastSelectionModTime = System.currentTimeMillis() - 10000;
1622
1623     /**
1624      * Current target time for the selection to be set. Calculated
1625      * according to the set quiet time and last selection modification
1626      * time.
1627      */
1628     long selectionSetTargetTime = 0;
1629
1630     /**
1631      * <code>true</code> if delayed selection runnable is current scheduled or
1632      * running.
1633      */
1634     boolean delayedSelectionScheduled = false;
1635
1636     Runnable SELECTION_DELAY = new Runnable() {
1637         @Override
1638         public void run() {
1639             if (tree.isDisposed())
1640                 return;
1641             long now = System.currentTimeMillis();
1642             long waitTimeLeft = selectionSetTargetTime - now;
1643             if (waitTimeLeft > 0) {
1644                 // Not enough quiet time, reschedule.
1645                 delayedSelectionScheduled = true;
1646                 tree.getDisplay().timerExec((int) waitTimeLeft, this);
1647             } else {
1648                 // Time to perform selection, stop rescheduling.
1649                 delayedSelectionScheduled = false;
1650                 resetSelection();
1651             }
1652         }
1653     };
1654
1655     private void widgetSelectionChanged(boolean forceSelectionChange) {
1656         long modTime = System.currentTimeMillis();
1657         long delta = modTime - lastSelectionModTime;
1658         lastSelectionModTime = modTime;
1659         if (!forceSelectionChange && delta < SELECTION_CHANGE_QUIET_TIME) {
1660             long msToWait = SELECTION_CHANGE_QUIET_TIME - delta;
1661             selectionSetTargetTime = modTime + msToWait;
1662             if (!delayedSelectionScheduled) {
1663                 delayedSelectionScheduled = true;
1664                 tree.getDisplay().timerExec((int) msToWait, SELECTION_DELAY);
1665             }
1666             // Make sure that post selection change events do not fire.
1667             ++postSelectionModCount;
1668             return;
1669         }
1670
1671         // Immediate selection reconstruction.
1672         resetSelection();
1673     }
1674
1675     private void resetSelection() {
1676         final ISelection selection = getWidgetSelection();
1677
1678         //System.out.println("resetSelection(" + postSelectionModCount + ")");
1679         //System.out.println("    provider selection: " + selectionProvider.getSelection());
1680         //System.out.println("    widget selection:   " + selection);
1681
1682         selectionProvider.setAndFireNonEqualSelection(selection);
1683
1684         // Implement deferred firing of post selection events
1685         final int count = ++postSelectionModCount;
1686         //System.out.println("[" + System.currentTimeMillis() + "] scheduling postSelectionChanged " + count + ": " + selection);
1687         ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
1688             @Override
1689             public void run() {
1690                 int newCount = postSelectionModCount;
1691                 // Don't publish selection yet, there's another change incoming.
1692                 //System.out.println("[" + System.currentTimeMillis() + "] checking post selection publish: " + count + " vs. " + newCount + ": " + selection);
1693                 if (newCount != count)
1694                     return;
1695                 //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);
1696
1697                 if (tree.isDisposed())
1698                     return;
1699
1700                 //System.out.println("scheduling fire post selection changed: " + selection);
1701                 tree.getDisplay().asyncExec(new Runnable() {
1702                     @Override
1703                     public void run() {
1704                         if (tree.isDisposed() || selectionProvider == null)
1705                             return;
1706                         //System.out.println("firing post selection changed: " + selection);
1707                         selectionProvider.firePostSelection(selection);
1708                     }
1709                 });
1710             }
1711         }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);
1712     }
1713     
1714     protected void setDefaultProcessors() {
1715         // Add a simple IMAGER query processor that always returns null.
1716         // With this processor no images will ever be shown.
1717 //        setPrimitiveProcessor(new StaticImagerProcessor(null));
1718
1719         setProcessor(new DefaultComparableChildrenProcessor());
1720         setProcessor(new DefaultLabelDecoratorsProcessor());
1721         setProcessor(new DefaultImageDecoratorsProcessor());
1722         setProcessor(new DefaultSelectedLabelerProcessor());
1723         setProcessor(new DefaultLabelerFactoriesProcessor());
1724         setProcessor(new DefaultSelectedImagerProcessor());
1725         setProcessor(new DefaultImagerFactoriesProcessor());
1726         setPrimitiveProcessor(new DefaultLabelerProcessor());
1727         setPrimitiveProcessor(new DefaultCheckedStateProcessor());
1728         setPrimitiveProcessor(new DefaultImagerProcessor());
1729         setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());
1730         setPrimitiveProcessor(new DefaultImageDecoratorProcessor());
1731         setPrimitiveProcessor(new NoSelectionRequestProcessor());
1732
1733         setProcessor(new DefaultFinalChildrenProcessor(this));
1734
1735         setProcessor(new DefaultPrunedChildrenProcessor());
1736         setProcessor(new DefaultSelectedViewpointProcessor());
1737         setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());
1738         setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());
1739         setProcessor(new DefaultViewpointContributionsProcessor());
1740
1741         setPrimitiveProcessor(new DefaultViewpointProcessor());
1742         setPrimitiveProcessor(new DefaultViewpointContributionProcessor());
1743         setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());
1744         setPrimitiveProcessor(new DefaultIsExpandedProcessor());
1745         setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());
1746     }
1747
1748     @Override
1749     public <T> void setProcessor(NodeQueryProcessor<T> processor) {
1750         assertNotDisposed();
1751         if (processor == null)
1752             throw new IllegalArgumentException("null processor");
1753
1754         processors.put(processor.getIdentifier(), processor);
1755     }
1756
1757     @Override
1758     public <T> void setPrimitiveProcessor(PrimitiveQueryProcessor<T> processor) {
1759         assertNotDisposed();
1760         if (processor == null)
1761             throw new IllegalArgumentException("null processor");
1762
1763         PrimitiveQueryProcessor<?> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
1764
1765         if (oldProcessor instanceof ProcessorLifecycle)
1766             ((ProcessorLifecycle) oldProcessor).detached(this);
1767         if (processor instanceof ProcessorLifecycle)
1768             ((ProcessorLifecycle) processor).attached(this);
1769     }
1770
1771     @Override
1772     public <T> void setDataSource(DataSource<T> provider) {
1773         assertNotDisposed();
1774         if (provider == null)
1775             throw new IllegalArgumentException("null provider");
1776         dataSources.put(provider.getProvidedClass(), provider);
1777     }
1778
1779     @SuppressWarnings("unchecked")
1780     @Override
1781     public <T> DataSource<T> removeDataSource(Class<T> forProvidedClass) {
1782         assertNotDisposed();
1783         if (forProvidedClass == null)
1784             throw new IllegalArgumentException("null class");
1785         return dataSources.remove(forProvidedClass);
1786     }
1787
1788     @Override
1789     public void setPersistor(StatePersistor persistor) {
1790         this.persistor = persistor;
1791     }
1792     
1793     @Override
1794     public SelectionDataResolver getSelectionDataResolver() {
1795         return selectionDataResolver;
1796     }
1797
1798     @Override
1799     public void setSelectionDataResolver(SelectionDataResolver r) {
1800         this.selectionDataResolver = r;
1801     }
1802
1803     @Override
1804     public SelectionFilter getSelectionFilter() {
1805         return selectionFilter;
1806     }
1807
1808     @Override
1809     public void setSelectionFilter(SelectionFilter f) {
1810         this.selectionFilter = f;
1811         // TODO: re-filter current selection?
1812     }
1813
1814     @Override
1815     public void setSelectionTransformation(BinaryFunction<Object[], GraphExplorer, Object[]> f) {
1816         this.selectionTransformation = f;
1817     }
1818
1819     @Override
1820     public <T> void addListener(T listener) {
1821         if(listener instanceof FocusListener) {
1822             focusListeners.add((FocusListener)listener);
1823         } else if(listener instanceof MouseListener) {
1824             mouseListeners.add((MouseListener)listener);
1825         } else if(listener instanceof KeyListener) {
1826             keyListeners.add((KeyListener)listener);
1827         }
1828     }
1829
1830     @Override
1831     public <T> void removeListener(T listener) {
1832         if(listener instanceof FocusListener) {
1833             focusListeners.remove(listener);
1834         } else if(listener instanceof MouseListener) {
1835             mouseListeners.remove(listener);
1836         } else if(listener instanceof KeyListener) {
1837             keyListeners.remove(listener);
1838         }
1839     }
1840
1841     public void addSelectionListener(SelectionListener listener) {
1842         tree.addSelectionListener(listener);
1843     }
1844
1845     public void removeSelectionListener(SelectionListener listener) {
1846         tree.removeSelectionListener(listener);
1847     }
1848
1849     private Set<String> uiContexts;
1850     
1851     @Override
1852     public void setUIContexts(Set<String> contexts) {
1853         this.uiContexts = contexts;
1854     }
1855     
1856     @Override
1857     public void setRoot(final Object root) {
1858         if(uiContexts != null && uiContexts.size() == 1)
1859                 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
1860         else
1861                 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
1862     }
1863
1864     @Override
1865     public void setRootContext(final NodeContext context) {
1866         setRootContext0(context);
1867     }
1868
1869     private void setRootContext0(final NodeContext context) {
1870         Assert.isNotNull(context, "root must not be null");
1871         if (isDisposed() || tree.isDisposed())
1872             return;
1873         Display display = tree.getDisplay();
1874         if (display.getThread() == Thread.currentThread()) {
1875             doSetRoot(context);
1876         } else {
1877             display.asyncExec(new Runnable() {
1878                 @Override
1879                 public void run() {
1880                     doSetRoot(context);
1881                 }
1882             });
1883         }
1884     }
1885     
1886     private void initializeState() {
1887         if (persistor == null)
1888             return;
1889
1890         ExplorerState state = persistor.deserialize(
1891                 Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
1892                 getRoot());
1893
1894         // topNodeToSet will be processed by #setData when it encounters a
1895         // NodeContext that matches this one.
1896 //        topNodePath = state.topNodePath;
1897 //        topNodePathChildIndex = state.topNodePathChildIndex;
1898 //        currentTopNodePathIndex = 0;
1899
1900         Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1901         if (processor instanceof DefaultIsExpandedProcessor) {
1902             DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
1903             for(NodeContext expanded : state.expandedNodes) {
1904                 isExpandedProcessor.setExpanded(expanded, true);
1905             }
1906         }
1907     }
1908
1909     private void saveState() {
1910         if (persistor == null)
1911             return;
1912
1913         NodeContext[] topNodePath = NodeContext.NONE;
1914         int[] topNodePathChildIndex = {};
1915         Collection<NodeContext> expandedNodes = Collections.emptyList();
1916         Map<String, Integer> columnWidths = Collections.<String, Integer> emptyMap();
1917
1918         // Resolve top node path
1919         TreeItem topItem = tree.getTopItem();
1920         if (topItem != null) {
1921             NodeContext topNode = (NodeContext) topItem.getData();
1922             if (topNode != null) {
1923                 topNodePath = getNodeContextPathSegments(topNode);
1924                 topNodePathChildIndex = new int[topNodePath.length];
1925                 for (int i = 0; i < topNodePath.length; ++i) {
1926                     // TODO: get child indexes
1927                     topNodePathChildIndex[i] = 0;
1928                 }
1929             }
1930         }
1931         
1932         // Resolve expanded nodes
1933         Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1934         if (processor instanceof IsExpandedProcessor) {
1935             IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;
1936             expandedNodes = isExpandedProcessor.getExpanded();
1937         }
1938
1939         // Column widths
1940         TreeColumn[] columns = tree.getColumns();
1941         if (columns.length > 1) {
1942                     columnWidths = new HashMap<String, Integer>();
1943                     for (int i = 0; i < columns.length; ++i) {
1944                         columnWidths.put(columns[i].getText(), columns[i].getWidth());
1945                     }
1946         }
1947                     
1948         persistor.serialize(
1949                 Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
1950                 getRoot(),
1951                 new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
1952     }
1953
1954     /**
1955      * Invoke only from SWT thread to reset the root of the graph explorer tree.
1956      * 
1957      * @param root
1958      */
1959     private void doSetRoot(NodeContext root) {
1960         if (tree.isDisposed())
1961             return;
1962         if (root.getConstant(BuiltinKeys.INPUT) == null) {
1963             ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));
1964             return;
1965         }
1966
1967         // Empty caches, release queries.
1968         GraphExplorerContext oldContext = explorerContext;
1969         GraphExplorerContext newContext = new GraphExplorerContext();
1970         GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));
1971         this.explorerContext = newContext;
1972         oldContext.safeDispose();
1973         toolTip.setGraphExplorerContext(explorerContext);
1974
1975         // Need to empty these or otherwise they won't be emptied until the
1976         // explorer is disposed which would mean that many unwanted references
1977         // will be held by this map.
1978         clearPrimitiveProcessors();
1979
1980         this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
1981                 : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
1982
1983         explorerContext.getCache().incRef(this.rootContext);
1984         
1985         initializeState();
1986         
1987         NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
1988
1989         tree.setItemCount(contexts.length);
1990
1991         select(rootContext);
1992         refreshColumnSizes();
1993     }
1994
1995     @Override
1996     public NodeContext getRoot() {
1997         return rootContext;
1998     }
1999
2000     @Override
2001     public NodeContext getParentContext(NodeContext context) {
2002         if (disposed)
2003             throw new IllegalStateException("disposed");
2004         if (!thread.currentThreadAccess())
2005             throw new IllegalStateException("not in SWT display thread " + thread.getThread());
2006
2007         TreeItem item = contextToItem.getRight(context);
2008         if(item == null) return null;
2009         TreeItem parentItem = item.getParentItem();
2010         if(parentItem == null) return null;
2011         return (NodeContext)parentItem.getData();
2012     }
2013
2014     Point previousTreeSize;
2015     Point previousTreeParentSize;
2016     boolean activatedBefore = false;
2017
2018     @Override
2019     public void handleEvent(Event event) {
2020         //System.out.println("EVENT: " + event);
2021         switch(event.type) {
2022             case SWT.Expand:
2023                 //System.out.println("EXPAND: " + event.item);
2024                 if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
2025                     expandVirtual(event);
2026                 } else {
2027                     System.out.println("TODO: non-virtual tree item expand");
2028                 }
2029                 break;
2030             case SWT.SetData:
2031                 // Only invoked for SWT.VIRTUAL trees
2032                 if (disposed)
2033                     // Happened for Hannu once during program startup.
2034                     // java.lang.AssertionError
2035                     //   at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)
2036                     //   at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)
2037                     //   at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)
2038                     // I do not know whether SWT guarantees that SetData events
2039                     // don't come after Dispose event has been issued, but I
2040                     // think its better to have this check here just incase.
2041                     return;
2042                 setData(event);
2043                 break;
2044             case SWT.Activate:
2045                 // This ensures that column sizes are refreshed at
2046                 // least once when the GE is first shown.
2047                 if (!activatedBefore) {
2048                     refreshColumnSizes();
2049                     activatedBefore = true;
2050                 }
2051                 break;
2052             case SWT.Dispose:
2053                 //new Exception().printStackTrace();
2054                 if (disposed)
2055                     return;
2056                 disposed = true;
2057                 doDispose();
2058                 break;
2059             case SWT.Resize:
2060                 if (event.widget == tree) {
2061                     // This case is meant for listening to tree width increase.
2062                     // The column resizing must be performed only after the tree
2063                     // itself as been resized.
2064                     Point size = tree.getSize();
2065                     int dx = 0;
2066                     if (previousTreeSize != null) {
2067                         dx = size.x - previousTreeSize.x;
2068                     }
2069                     previousTreeSize = size;
2070                     //System.out.println("RESIZE: " + dx + " - size=" + size);
2071
2072                     if (dx > 0) {
2073                         tree.setRedraw(false);
2074                         refreshColumnSizes(size);
2075                         tree.setRedraw(true);
2076                     }
2077                 } else if (event.widget == tree.getParent()) {
2078                     // This case is meant for listening to tree width decrease.
2079                     // The columns must be resized before the tree widget itself
2080                     // is resized to prevent scroll bar flicker. This can be achieved
2081                     // by listening to the resize events of the tree parent widget.
2082                     Composite parent = tree.getParent();
2083                     Point size = parent.getSize();
2084
2085                     // We must subtract the parent's border and possible
2086                     // scroll bar width from the new target width of the columns.
2087                     size.x -= tree.getParent().getBorderWidth() * 2;
2088                     ScrollBar vBar = parent.getVerticalBar();
2089                     if (vBar != null && vBar.isVisible())
2090                         size.x -= vBar.getSize().x;
2091
2092                     int dx = 0;
2093                     if (previousTreeParentSize != null) {
2094                         dx = size.x - previousTreeParentSize.x;
2095                     }
2096                     previousTreeParentSize = size;
2097                     //System.out.println("RESIZE: " + dx + " - size=" + size);
2098
2099                     if (dx < 0) {
2100                         tree.setRedraw(false);
2101                         refreshColumnSizes(size);
2102                         tree.setRedraw(true);
2103                     }
2104                 }
2105                 break;
2106             default:
2107                 break;
2108         }
2109
2110     }
2111
2112     protected void refreshColumnSizes() {
2113 //        Composite treeParent = tree.getParent();
2114 //        Point size = treeParent.getSize();
2115 //        size.x -= treeParent.getBorderWidth() * 2;
2116         Point size = tree.getSize();
2117         refreshColumnSizes(size);
2118         tree.getParent().layout();
2119     }
2120
2121     /**
2122      * This has been disabled since the logic of handling column widths has been
2123      * externalized to parties creating {@link GraphExplorerImpl} instances.
2124      */
2125     protected void refreshColumnSizes(Point toSize) {
2126         /*
2127         refreshingColumnSizes = true;
2128         try {
2129             int columnCount = tree.getColumnCount();
2130             if (columnCount > 0) {
2131                 Point size = toSize;
2132                 int targetWidth = size.x - tree.getBorderWidth() * 2;
2133                 targetWidth -= 0;
2134
2135                 // Take the vertical scroll bar existence into to account when
2136                 // calculating the overflow column width.
2137                 ScrollBar vBar = tree.getVerticalBar();
2138                 //if (vBar != null && vBar.isVisible())
2139                 if (vBar != null)
2140                     targetWidth -= vBar.getSize().x;
2141
2142                 List<TreeColumn> resizing = new ArrayList<TreeColumn>();
2143                 int usedWidth = 0;
2144                 int resizingWidth = 0;
2145                 int totalWeight = 0;
2146                 for (int i = 0; i < columnCount - 1; ++i) {
2147                     TreeColumn col = tree.getColumn(i);
2148                     //System.out.println("  " + col.getText() + ": " + col.getWidth());
2149                     int width = col.getWidth();
2150                     usedWidth += width;
2151                     Column c = (Column) col.getData();
2152                     if (c.hasGrab()) {
2153                         resizing.add(col);
2154                         resizingWidth += width;
2155                         totalWeight += c.getWeight();
2156                     }
2157                 }
2158
2159                 int requiredWidthAdjustment = targetWidth - usedWidth;
2160                 if (requiredWidthAdjustment < 0)
2161                     requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());
2162                 double diff = requiredWidthAdjustment;
2163                 //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);
2164
2165                 // Decide how much to give space to / take space from each grabbing column
2166                 double wrel = 1.0 / resizing.size();
2167
2168                 double[] weightedShares = new double[resizing.size()];
2169                 for (int i = 0; i < resizing.size(); ++i) {
2170                     TreeColumn col = resizing.get(i);
2171                     Column c = (Column) col.getData();
2172                     if (totalWeight == 0) {
2173                         weightedShares[i] = wrel;
2174                     } else {
2175                         weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
2176                     }
2177                 }
2178                 //System.out.println("grabbing columns:" + resizing);
2179                 //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
2180
2181                 // Always shrink the columns if necessary, but don't enlarge before
2182                 // there is sufficient space to at least give all resizable columns
2183                 // some more width.
2184                 if (diff < 0 || (diff > 0 && diff > resizing.size())) {
2185                     // Need to either shrink or enlarge the resizable columns if possible.
2186                     for (int i = 0; i < resizing.size(); ++i) {
2187                         TreeColumn col = resizing.get(i);
2188                         Column c = (Column) col.getData();
2189                         int cw = col.getWidth();
2190                         //double wrel = (double) cw / (double) resizingWidth;
2191                         //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);
2192                         double ddelta = weightedShares[i] * diff;
2193                         int delta = 0;
2194                         if (diff < 0) {
2195                             delta = (int) Math.floor(ddelta);
2196                         } else {
2197                             delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
2198                         }
2199                         //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);
2200                         //System.out.println("argh(" + col.getText() + "): " + c.getWidth() +  " vs. " + col.getWidth() + " vs. " + (cw+delta));
2201                         int newWidth = Math.max(c.getWidth(), cw + delta);
2202                         requiredWidthAdjustment -= (newWidth - cw);
2203                         col.setWidth(newWidth);
2204                     }
2205                 }
2206
2207                 //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
2208
2209                 TreeColumn last = tree.getColumn(columnCount - 1);
2210                 // HACK: see #setColumns for why this is here.
2211                 if (FILLER.equals(last.getText())) {
2212                     last.setWidth(Math.max(0, requiredWidthAdjustment));
2213                 }
2214             }
2215         } finally {
2216             refreshingColumnSizes = false;
2217         }
2218          */
2219     }
2220
2221     private void doDispose() {
2222         explorerContext.dispose();
2223
2224         // No longer necessary, the used executors are shared.
2225         //scheduler.shutdown();
2226         //scheduler2.shutdown();
2227
2228         processors.clear();
2229         detachPrimitiveProcessors();
2230         primitiveProcessors.clear();
2231         dataSources.clear();
2232
2233         pendingItems.clear();
2234
2235         rootContext = null;
2236
2237         contextToItem.clear();
2238
2239         mouseListeners.clear();
2240
2241         selectionProvider.clearListeners();
2242         selectionProvider = null;
2243         selectionDataResolver = null;
2244         selectionRefreshContexts.clear();
2245         selectedItems.clear();
2246         originalFont = null;
2247
2248         localResourceManager.dispose();
2249
2250         // Must shutdown image loader job before disposing its ResourceManager
2251         imageLoaderJob.dispose();
2252         imageLoaderJob.cancel();
2253         try {
2254             imageLoaderJob.join();
2255         } catch (InterruptedException e) {
2256             ErrorLogger.defaultLogError(e);
2257         }
2258         resourceManager.dispose();
2259         
2260         postSelectionProvider.dispose();
2261
2262     }
2263
2264     private void expandVirtual(final Event event) {
2265         TreeItem item = (TreeItem) event.item;
2266         assert (item != null);
2267         NodeContext context = (NodeContext) item.getData();
2268         assert (context != null);
2269
2270         GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));
2271         NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);
2272         int maxChildren = getMaxChildren(manager, context);
2273         item.setItemCount(children.length < maxChildren ? children.length : maxChildren);
2274     }
2275
2276     private NodeContext getNodeContext(TreeItem item) {
2277         assert(item != null);
2278
2279         NodeContext context = (NodeContext)item.getData();
2280         assert(context != null);
2281
2282         return context;
2283     }
2284
2285     private NodeContext getParentContext(TreeItem item) {
2286         TreeItem parentItem = item.getParentItem();
2287         if(parentItem != null) {
2288             return getNodeContext(parentItem);
2289         } else {
2290             return rootContext;
2291         }
2292     }
2293
2294     private static final String LISTENER_SET_INDICATOR = "LSI";
2295     private static final String PENDING = "PENDING";
2296     private int contextSelectionChangeModCount = 0;
2297
2298     /**
2299      * Only invoked for SWT.VIRTUAL widgets.
2300      * 
2301      * @param event
2302      */
2303     private void setData(final Event event) {
2304         assert (event != null);
2305         TreeItem item = (TreeItem) event.item;
2306         assert (item != null);
2307
2308         // Based on experience it seems to be possible that
2309         // SetData events are sent for disposed TreeItems.
2310         if (item.isDisposed() || item.getData(PENDING) != null)
2311             return;
2312
2313         //System.out.println("GE.SetData " + item);
2314
2315         GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
2316
2317         NodeContext parentContext = getParentContext(item);
2318         assert (parentContext != null);
2319
2320         NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2321
2322         // Some children have disappeared since counting
2323         if (event.index < 0) {
2324             ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());
2325             return;
2326         }
2327         if (event.index >= parentChildren.length)
2328             return;
2329
2330         NodeContext context = parentChildren[event.index];
2331         assert (context != null);
2332         item.setData(context);
2333         
2334         // Manage NodeContext -> TreeItem mappings
2335         contextToItem.map(context, item);
2336         if (item.getData(LISTENER_SET_INDICATOR) == null) {
2337             // This "if" exists because setData will get called many
2338             // times for the same (NodeContext, TreeItem) pairs.
2339             // Each TreeItem only needs one listener, but this
2340             // is needed to tell whether it already has a listener
2341             // or not.
2342             item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
2343             item.addListener(SWT.Dispose, itemDisposeListener);
2344         }
2345
2346         boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
2347
2348         PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);
2349         int maxChildren = getMaxChildren(manager, context);
2350         //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);
2351
2352      NodeContext[] pruned = children.getPrunedChildren(); 
2353      int count = Math.min(pruned.length, maxChildren);
2354
2355         if (isExpanded || item.getItemCount() > 1) {
2356             item.setItemCount(count);
2357             TreeItem[] childItems = item.getItems();
2358          for(int i=0;i<count;i++)
2359              contextToItem.map(pruned[i], childItems[i]);
2360         } else {
2361             if (children.getPrunedChildren().length == 0) {
2362                 item.setItemCount(0);
2363             } else {
2364 //                item.setItemCount(1);
2365                 item.setItemCount(count);
2366                 TreeItem[] childItems = item.getItems();
2367              for(int i=0;i<count;i++)
2368                  contextToItem.map(pruned[i], childItems[i]);
2369 //                item.getItem(0).setData(PENDING, PENDING);
2370 //                item.getItem(0).setItemCount(o);
2371             }
2372         }
2373
2374         setTextAndImage(item, manager, context, event.index);
2375
2376         // Check if the node should be auto-expanded?
2377         if ((autoExpandLevel == ALL_LEVELS || autoExpandLevel > 1) && !isExpanded) {
2378             //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");
2379             int level = getTreeItemLevel(item);
2380             if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)
2381                     && !explorerContext.autoExpanded.containsKey(context))
2382             {
2383                 //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
2384                 explorerContext.autoExpanded.put(context, Boolean.TRUE);
2385                 setExpanded(context, true);
2386             }
2387         }
2388
2389         item.setExpanded(isExpanded);
2390
2391         if ((tree.getStyle() & SWT.CHECK) != 0) {
2392             CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);
2393             item.setChecked(CheckedState.CHECKED_STATES.contains(checked));
2394             item.setGrayed(CheckedState.GRAYED == checked);
2395         }
2396
2397         //System.out.println("GE.SetData completed " + item);
2398
2399         // This test makes sure that selectionProvider holds the correct
2400         // selection with respect to the actual selection stored by the virtual
2401         // SWT Tree widget.
2402         // The data items shown below the items occupied by the selected and now removed data
2403         // will be squeezed to use the tree items previously used for the now
2404         // removed data. When this happens, the NodeContext items stored by the
2405         // tree items will be different from what the GraphExplorer's
2406         // ISelectionProvider thinks the selection currently is. To compensate,
2407         // 1. Recognize the situation
2408         // 2. ASAP set the selection provider selection to what is actually
2409         // offered by the tree widget.
2410         NodeContext selectedContext = selectedItems.get(item);
2411 //        System.out.println("selectedContext(" + item + "): " + selectedContext);
2412         if (selectedContext != null && !selectedContext.equals(context)) {
2413                 final int modCount = ++contextSelectionChangeModCount;
2414 //            System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);
2415 //            System.out.println("    old context: " + selectedContext);
2416 //            System.out.println("    new context: " + context);
2417 //            System.out.println("    provider selection: " + selectionProvider.getSelection());
2418 //            System.out.println("    widget   selection: " + getWidgetSelection());
2419             ThreadUtils.asyncExec(thread, new Runnable() {
2420                 @Override
2421                 public void run() {
2422                     if (isDisposed())
2423                         return;
2424                     int count = contextSelectionChangeModCount;
2425 //                    System.out.println("MODCOUNT: " + modCount + " vs. " + count);
2426                     if (modCount != count)
2427                         return;
2428                     widgetSelectionChanged(true);
2429                 }
2430             });
2431         }
2432
2433         // This must be done to keep the visible tree selection properly
2434         // in sync with the selectionProvider JFace proxy of this class in
2435         // cases where an in-line editor was previously active for the node
2436         // context.
2437         if (selectionRefreshContexts.remove(context)) {
2438             final ISelection currentSelection = selectionProvider.getSelection();
2439             // asyncExec is here to prevent ui glitches that
2440             // seem to occur if the selection setting is done
2441             // directly here in between setData invocations.
2442             ThreadUtils.asyncExec(thread, new Runnable() {
2443                 @Override
2444                 public void run() {
2445                     if (isDisposed())
2446                         return;
2447 //                    System.out.println("REFRESHING SELECTION: " + currentSelection);
2448 //                    System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));
2449 //                    System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());
2450                     setSelection(currentSelection, true);
2451 //                    System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));
2452 //                    System.out.println("AFTER setSelection: " + selectionProvider.getSelection());
2453                 }
2454             });
2455         }
2456
2457         // TODO: doesn't work if any part of the node path that should be
2458         // revealed is out of view.
2459         // Disabled until a better solution is devised.
2460         // Suggestion: include item indexes into the stored node context path
2461         // to make it possible for this method to know whether the current
2462         // node path segment is currently out of view based on event.index.
2463         // If out of view, this code needs to scroll the view programmatically
2464         // onwards.
2465 //        if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {
2466 //            NodeContext topNode = topNodePath[currentTopNodePathIndex];
2467 //            if (topNode.equals(context)) {
2468 //                final TreeItem topItem = item;
2469 //                ++currentTopNodePathIndex;
2470 //                if (currentTopNodePathIndex >= topNodePath.length) {
2471 //                    // Mission accomplished. End search for top node here.
2472 //                    topNodePath = NodeContext.NONE;
2473 //                    currentTopNodePathIndex = -1;
2474 //                }
2475 //                ThreadUtils.asyncExec(thread, new Runnable() {
2476 //                    @Override
2477 //                    public void run() {
2478 //                        if (isDisposed())
2479 //                            return;
2480 //                        tree.setTopItem(topItem);
2481 //                    }
2482 //                });
2483 //            }
2484 //        }
2485
2486         // Check if vertical scroll bar has become visible and refresh layout.
2487         ScrollBar verticalBar = tree.getVerticalBar();
2488         if(verticalBar != null) {
2489                 boolean currentlyVerticalBarVisible = verticalBar.isVisible();
2490                 if (verticalBarVisible != currentlyVerticalBarVisible) {
2491                     verticalBarVisible = currentlyVerticalBarVisible;
2492                     Composite parent = tree.getParent();
2493                     if (parent != null)
2494                         parent.layout();
2495                 }
2496         }
2497     }
2498
2499     /**
2500      * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the
2501      *         return value is calculated. Items without parents have level=2,
2502      *         their children level=3, etc. Returns 0 for invalid items
2503      */
2504     private int getTreeItemLevel(TreeItem item) {
2505         if (item == null)
2506             return 0;
2507         int level = 1;
2508         for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);
2509         //System.out.println("\tgetTreeItemLevel(" + parent + ")");
2510         //System.out.println("level(" + item + "): " + level);
2511         return level;
2512     }
2513
2514     /**
2515      * @param node
2516      * @return
2517      */
2518     private NodeContext[] getNodeContextPathSegments(NodeContext node) {
2519         TreeItem item = contextToItem.getRight(node);
2520         if (item == null)
2521             return NodeContext.NONE;
2522         int level = getTreeItemLevel(item);
2523         if (level == 0)
2524             return NodeContext.NONE;
2525         // Exclude root from the saved node path.
2526         --level;
2527         NodeContext[] segments = new NodeContext[level];
2528         for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {
2529             NodeContext ctx = (NodeContext) item.getData();
2530             if (ctx == null)
2531                 return NodeContext.NONE;
2532             segments[level-1] = ctx;
2533         }
2534         return segments;
2535     }
2536
2537     /**
2538      * @param node
2539      * @return
2540      */
2541     @SuppressWarnings("unused")
2542     private NodeContextPath getNodeContextPath(NodeContext node) {
2543         NodeContext[] path = getNodeContextPathSegments(node);
2544         return new NodeContextPath(path);
2545     }
2546
2547     void setImage(NodeContext node, TreeItem item, Imager imager, Collection<ImageDecorator> decorators, int itemIndex) {
2548         Image[] images = columnImageArray;
2549         Arrays.fill(images, null);
2550         if (imager == null) {
2551             item.setImage(images);
2552             return;
2553         }
2554
2555         Object[] descOrImage = columnDescOrImageArray;
2556         Arrays.fill(descOrImage, null);
2557         boolean finishLoadingInJob = false;
2558         int index = 0;
2559         for (Column column : columns) {
2560             String key = column.getKey();
2561             ImageDescriptor desc = imager.getImage(key);
2562             if (desc != null) {
2563                 // Attempt to decorate the label
2564                 if (!decorators.isEmpty()) {
2565                     for (ImageDecorator id : decorators) {
2566                         ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);
2567                         if (ds != null)
2568                             desc = ds;
2569                     }
2570                 }
2571
2572                 // Try resolving only cached images here and now
2573                 Object img = localResourceManager.find(desc);
2574                 if (img == null)
2575                     img = resourceManager.find(desc);
2576
2577                 images[index] = img != null ? (Image) img : null;
2578                 descOrImage[index] = img == null ? desc : img;
2579                 finishLoadingInJob |= img == null;
2580             }
2581             ++index;
2582         }
2583
2584         // Finish loading the final image in the image loader job if necessary.
2585         if (finishLoadingInJob) {
2586             // Prevent UI from flashing unnecessarily by reusing the old image
2587             // in the item if it exists.
2588             for (int c = 0; c < columns.length; ++c) {
2589                 Image img = item.getImage(c);
2590                 if (img != null)
2591                     images[c] = img;
2592             }
2593             item.setImage(images);
2594
2595             // Schedule loading to another thread to refrain from blocking
2596             // the UI with database operations.
2597             queueImageTask(item, new ImageTask(
2598                     node,
2599                     item,
2600                     Arrays.copyOf(descOrImage, descOrImage.length)));
2601         } else {
2602             // Set any images that were resolved.
2603             item.setImage(images);
2604         }
2605     }
2606
2607     private void queueImageTask(TreeItem item, ImageTask task) {
2608         synchronized (imageTasks) {
2609             imageTasks.put(item, task);
2610         }
2611         imageLoaderJob.scheduleIfNecessary(100);
2612     }
2613
2614     /**
2615      * Invoked in a job worker thread.
2616      * 
2617      * @param monitor
2618      * @see ImageLoaderJob
2619      */
2620     @Override
2621         protected IStatus setPendingImages(IProgressMonitor monitor) {
2622         ImageTask[] tasks = null;
2623         synchronized (imageTasks) {
2624             tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);
2625             imageTasks.clear();
2626         }
2627         if (tasks.length == 0)
2628             return Status.OK_STATUS;
2629
2630         MultiStatus status = null;
2631
2632         // Load missing images
2633         for (ImageTask task : tasks) {
2634             Object[] descs = task.descsOrImages;
2635             for (int i = 0; i < descs.length; ++i) {
2636                 Object obj = descs[i];
2637                 if (obj instanceof ImageDescriptor) {
2638                     ImageDescriptor desc = (ImageDescriptor) obj; 
2639                     try {
2640                         descs[i] = resourceManager.get((ImageDescriptor) desc);
2641                     } catch (DeviceResourceException e) {
2642                         if (status == null)
2643                             status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);
2644                         status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));
2645                     }
2646                 }
2647             }
2648         }
2649
2650         // Perform final UI updates in the UI thread.
2651         final ImageTask[] _tasks = tasks;
2652         thread.asyncExec(new Runnable() {
2653             @Override
2654             public void run() {
2655                 if (!tree.isDisposed()) {
2656                     tree.setRedraw(false);
2657                     setImages(_tasks);
2658                     tree.setRedraw(true);
2659                 }
2660             }
2661         });
2662
2663         return status != null ? status : Status.OK_STATUS;
2664     }
2665
2666     /**
2667      * Invoked in the UI thread only.
2668      * 
2669      * @param task
2670      */
2671     void setImages(ImageTask[] tasks) {
2672         for (ImageTask task : tasks)
2673             if (task != null)
2674                 setImage(task);
2675     }
2676
2677     /**
2678      * Invoked in the UI thread only.
2679      * 
2680      * @param task
2681      */
2682     void setImage(ImageTask task) {
2683         // Be sure not to process disposed items.
2684         if (task.item.isDisposed())
2685             return;
2686         // Discard this task if the TreeItem has switched owning NodeContext.
2687         if (!contextToItem.contains(task.node, task.item))
2688             return;
2689
2690         Object[] descs = task.descsOrImages;
2691         Image[] images = columnImageArray;
2692         Arrays.fill(images, null);
2693         for (int i = 0; i < descs.length; ++i) {
2694             Object desc = descs[i];
2695             if (desc instanceof Image) {
2696                 images[i] = (Image) desc;
2697             }
2698         }
2699         task.item.setImage(images);
2700     }
2701
2702     void setText(TreeItem item, Labeler labeler, Collection<LabelDecorator> decorators, int itemIndex) {
2703         if (labeler != null) {
2704             String[] texts = new String[columns.length];
2705             int index = 0;
2706             Map<String, String> labels = labeler.getLabels();
2707             Map<String, String> runtimeLabels = labeler.getRuntimeLabels();
2708             for (Column column : columns) {
2709                 String key = column.getKey();
2710                 String s = null;
2711                 if (runtimeLabels != null) s = runtimeLabels.get(key);
2712                 if (s == null) s = labels.get(key);
2713                 if (s != null) {
2714                     FontDescriptor font = originalFont;
2715                     ColorDescriptor bg = originalBackground;
2716                     ColorDescriptor fg = originalForeground;
2717
2718                     // Attempt to decorate the label
2719                     if (!decorators.isEmpty()) {
2720                         for (LabelDecorator ld : decorators) {
2721                             String ds = ld.decorateLabel(s, key, itemIndex);
2722                             if (ds != null)
2723                                 s = ds;
2724
2725                             FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
2726                             if (dfont != null)
2727                                 font = dfont;
2728
2729                             ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
2730                             if (dbg != null)
2731                                 bg = dbg;
2732
2733                             ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
2734                             if (dfg != null)
2735                                 fg = dfg;
2736                         }
2737                     }
2738
2739                     if (font != originalFont) {
2740                         //System.out.println("set font: " + index + ": " + font);
2741                         item.setFont(index, (Font) localResourceManager.get(font));
2742                     }
2743                     if (bg != originalBackground)
2744                         item.setBackground(index, (Color) localResourceManager.get(bg));
2745                     if (fg != originalForeground)
2746                         item.setForeground(index, (Color) localResourceManager.get(fg));
2747
2748                     texts[index] = s;
2749                 }
2750                 ++index;
2751             }
2752             item.setText(texts);
2753         } else {
2754             item.setText(Labeler.NO_LABEL);
2755         }
2756     }
2757
2758     void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {
2759         Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
2760         if (labeler != null) {
2761             labeler.setListener(labelListener);
2762         }
2763         Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);
2764         Collection<LabelDecorator> labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);
2765         Collection<ImageDecorator> imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);
2766
2767         setText(item, labeler, labelDecorators, itemIndex);
2768         setImage(context, item, imager, imageDecorators, itemIndex);
2769     }
2770
2771     @Override
2772     public void setFocus() {
2773         tree.setFocus();
2774     }
2775
2776     @Override
2777     public <T> T query(NodeContext context, CacheKey<T> key) {
2778         return this.explorerContext.cache.get(context, key);
2779     }
2780
2781     @Override
2782     public boolean isDisposed() {
2783         return disposed;
2784     }
2785
2786     protected void assertNotDisposed() {
2787         if (isDisposed())
2788             throw new IllegalStateException("disposed");
2789     }
2790
2791
2792
2793     /**
2794      * @param selection
2795      * @param forceControlUpdate
2796      * @thread any
2797      */
2798     public void setSelection(final ISelection selection, boolean forceControlUpdate) {
2799         assertNotDisposed();
2800         boolean equalsOld = selectionProvider.selectionEquals(selection);
2801         if (equalsOld && !forceControlUpdate) {
2802             // Just set the selection object instance, fire no events nor update
2803             // the viewer selection.
2804             selectionProvider.setSelection(selection);
2805         } else {
2806             // Schedule viewer and selection update if necessary.
2807             if (tree.isDisposed())
2808                 return;
2809             Display d = tree.getDisplay();
2810             if (d.getThread() == Thread.currentThread()) {
2811                 updateSelectionToControl(selection);
2812             } else {
2813                 d.asyncExec(new Runnable() {
2814                     @Override
2815                     public void run() {
2816                         if (tree.isDisposed())
2817                             return;
2818                         updateSelectionToControl(selection);
2819                     }
2820                 });
2821             }
2822         }
2823     }
2824     
2825
2826     /* Contains the best currently found tree item and its priority
2827      */
2828     private static class SelectionResolutionStatus {
2829         int bestPriority = Integer.MAX_VALUE;
2830         TreeItem bestItem;
2831     }
2832
2833     /**
2834      * @param selection
2835      * @thread SWT
2836      */
2837     private void updateSelectionToControl(ISelection selection) {
2838         if (selectionDataResolver == null)
2839             return;
2840         if (!(selection instanceof IStructuredSelection))
2841             return;
2842         
2843         // Initialize selection resolution status map 
2844         IStructuredSelection iss = (IStructuredSelection) selection;
2845         final THashMap<Object,SelectionResolutionStatus> statusMap =
2846                 new THashMap<Object,SelectionResolutionStatus>(iss.size());
2847         for(Iterator<?> it = iss.iterator(); it.hasNext();) {
2848             Object selectionElement = it.next();
2849             Object resolvedElement = selectionDataResolver.resolve(selectionElement);
2850             statusMap.put(
2851                     resolvedElement,
2852                     new SelectionResolutionStatus());
2853         }
2854         
2855         // Iterate all tree items and try to match them to the selection
2856         iterateTreeItems(new TObjectProcedure<TreeItem>() {
2857             @Override
2858             public boolean execute(TreeItem treeItem) {
2859                 NodeContext nodeContext = (NodeContext)treeItem.getData();
2860                 if(nodeContext == null)
2861                     return true;
2862                 SelectionResolutionStatus status = statusMap.get(nodeContext);
2863                 if(status != null) {
2864                     status.bestPriority = 0; // best possible match
2865                     status.bestItem = treeItem;
2866                     return true;
2867                 }
2868                 
2869                 Object input = nodeContext.getConstant(BuiltinKeys.INPUT);
2870                 status = statusMap.get(input);
2871                 if(status != null) {
2872                     NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);
2873                     int curPriority = nodeType instanceof EntityNodeType 
2874                             ? 1 // Prefer EntityNodeType matches to other node types
2875                             : 2;
2876                     if(curPriority < status.bestPriority) {
2877                         status.bestPriority = curPriority;
2878                         status.bestItem = treeItem;
2879                     }
2880                 }
2881                 return true;
2882             }
2883         });
2884         
2885         // Update selection
2886         ArrayList<TreeItem> items = new ArrayList<TreeItem>(statusMap.size());
2887         for(SelectionResolutionStatus status : statusMap.values())
2888             if(status.bestItem != null)
2889                 items.add(status.bestItem);
2890         select(items.toArray(new TreeItem[items.size()]));
2891     }
2892
2893     /**
2894      * @thread SWT
2895      */
2896     public ISelection getWidgetSelection() {
2897         TreeItem[] items = tree.getSelection();
2898         if (items.length == 0)
2899             return StructuredSelection.EMPTY;
2900
2901         List<NodeContext> nodes = new ArrayList<NodeContext>(items.length);
2902
2903         // Caches for resolving node contexts the hard way if necessary.
2904         GENodeQueryManager manager = null;
2905         NodeContext lastParentContext = null;
2906         NodeContext[] lastChildren = null;
2907
2908         for (int i = 0; i < items.length; i++) {
2909             TreeItem item = items[i];
2910             NodeContext ctx = (NodeContext) item.getData();
2911             // It may happen due to the virtual nature of the tree control
2912             // that it contains TreeItems which have not yet been ran through
2913             // #setData(Event).
2914             if (ctx != null) {
2915                 nodes.add(ctx);
2916             } else {
2917                 TreeItem parentItem = item.getParentItem();
2918                 NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;
2919                 if (parentContext != null) {
2920                     NodeContext[] children = lastChildren;
2921                     if (parentContext != lastParentContext) {
2922                         if (manager == null)
2923                             manager = new GENodeQueryManager(this.explorerContext, null, null, null);
2924                         lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2925                         lastParentContext = parentContext;
2926                     }
2927                     int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);
2928                     if (index >= 0 && index < children.length) {
2929                         NodeContext child = children[index];
2930                         if (child != null) {
2931                             nodes.add(child);
2932                             // Cache NodeContext in TreeItem for faster access
2933                             item.setData(child);
2934                         }
2935                     }
2936                 }
2937             }
2938         }
2939         //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
2940         ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
2941         return selection;
2942     }
2943     
2944     @Override
2945     public TransientExplorerState getTransientState() {
2946         if (!thread.currentThreadAccess())
2947             throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());
2948         return transientState;
2949     }
2950
2951     /**
2952      * @param item
2953      * @thread SWT
2954      */
2955     private void select(TreeItem item) {
2956         tree.setSelection(item);
2957         tree.showSelection();
2958         selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
2959     }
2960
2961     /**
2962      * @param items
2963      * @thread SWT
2964      */
2965     private void select(TreeItem[] items) {
2966         //System.out.println("Select: " + Arrays.toString(items));
2967         tree.setSelection(items);
2968         tree.showSelection();
2969         NodeContext[] data = new NodeContext[items.length];
2970         for (int i = 0; i < data.length; i++) {
2971             data[i] = (NodeContext) items[i].getData();
2972         }
2973         selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
2974     }
2975     
2976     private void iterateTreeItems(TObjectProcedure<TreeItem> procedure) {
2977         for(TreeItem item : tree.getItems())
2978             if(!iterateTreeItems(item, procedure))
2979                 return;
2980     }
2981
2982     private boolean iterateTreeItems(TreeItem item,
2983             TObjectProcedure<TreeItem> procedure) {
2984         if(!procedure.execute(item))
2985             return false;
2986         if(item.getExpanded())
2987             for(TreeItem child : item.getItems())
2988                 if(!iterateTreeItems(child, procedure))
2989                     return false;
2990         return true;
2991     }
2992
2993     /**
2994      * @param item
2995      * @param context
2996      * @return
2997      */
2998     private boolean trySelect(TreeItem item, Object input) {
2999         NodeContext itemCtx = (NodeContext) item.getData();
3000         if (itemCtx != null) {
3001             if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {
3002                 select(item);
3003                 return true;
3004             }
3005         }
3006         if (item.getExpanded()) {
3007             for (TreeItem child : item.getItems()) {
3008                 if (trySelect(child, input))
3009                     return true;
3010             }
3011         }
3012         return false;
3013     }
3014
3015     private boolean equalsEnough(NodeContext c1, NodeContext c2) {
3016         
3017         Object input1 = c1.getConstant(BuiltinKeys.INPUT);
3018         Object input2 = c2.getConstant(BuiltinKeys.INPUT);
3019         if(!ObjectUtils.objectEquals(input1, input2)) 
3020                 return false;
3021
3022         Object type1 = c1.getConstant(NodeType.TYPE);
3023         Object type2 = c2.getConstant(NodeType.TYPE);
3024         if(!ObjectUtils.objectEquals(type1, type2)) 
3025                 return false;
3026         
3027         return true;
3028         
3029     }
3030     
3031     private NodeContext tryFind(NodeContext context) {
3032         for (TreeItem item : tree.getItems()) {
3033                 NodeContext found = tryFind(item, context);
3034                 if(found != null) return found;
3035         }
3036         return null;
3037     }
3038     
3039     private NodeContext tryFind(TreeItem item, NodeContext context) {
3040         NodeContext itemCtx = (NodeContext) item.getData();
3041         if (itemCtx != null) {
3042             if (equalsEnough(context, itemCtx)) {
3043                 return itemCtx;
3044             }
3045         }
3046         if (item.getExpanded()) {
3047             for (TreeItem child : item.getItems()) {
3048                 NodeContext found = tryFind(child, context);
3049                 if(found != null) return found;
3050             }
3051         }
3052         return null;
3053     }
3054     
3055     @Override
3056     public boolean select(NodeContext context) {
3057
3058         assertNotDisposed();
3059
3060         if (context == null || context.equals(rootContext)) {
3061             tree.deselectAll();
3062             selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
3063             return true;
3064         }
3065
3066 //        if (context.equals(rootContext)) {
3067 //            tree.deselectAll();
3068 //            selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
3069 //            return;
3070 //        }
3071
3072         Object input = context.getConstant(BuiltinKeys.INPUT);
3073
3074         for (TreeItem item : tree.getItems()) {
3075             if (trySelect(item, input))
3076                 return true;
3077         }
3078         
3079         return false;
3080         
3081     }
3082     
3083     private NodeContext tryFind2(NodeContext context) {
3084         Set<NodeContext> ctxs = contextToItem.getLeftSet();
3085         for(NodeContext c : ctxs) 
3086                 if(equalsEnough(c, context)) 
3087                         return c;
3088         return null;
3089     }
3090
3091     private boolean waitVisible(NodeContext parent, NodeContext context) {
3092         long start = System.nanoTime();
3093         
3094         TreeItem parentItem = contextToItem.getRight(parent);
3095         
3096         if(parentItem == null) 
3097                 return false; 
3098         
3099         while(true) {
3100                 NodeContext target = tryFind2(context);
3101                 if(target != null) {
3102                         TreeItem item = contextToItem.getRight(target);
3103                         if (!(item.getParentItem().equals(parentItem)))
3104                                 return false;
3105                         tree.setTopItem(item);
3106                         return true;
3107                 }
3108
3109                 Display.getCurrent().readAndDispatch();
3110                 long duration = System.nanoTime() - start;
3111                 if(duration > 10e9)
3112                         return false;                   
3113         }
3114     }
3115     
3116     private boolean selectPathInternal(NodeContext[] contexts, int position) {
3117         //System.out.println("NodeContext path : " + contexts);
3118
3119         NodeContext head = tryFind(contexts[position]);
3120
3121         if(position == contexts.length-1) {
3122                 return select(head);
3123
3124         }
3125
3126         //setExpanded(head, true);
3127         
3128         if(!waitVisible(head, contexts[position+1])) 
3129                 return false;
3130         
3131         setExpanded(head, true);
3132         
3133         return selectPathInternal(contexts, position+1);
3134         
3135     }
3136     
3137     @Override
3138     public boolean selectPath(Collection<NodeContext> contexts) {
3139         
3140         if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");
3141         if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");
3142         
3143         return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
3144         
3145     }
3146     
3147     @Override
3148     public boolean isVisible(NodeContext context) {
3149         
3150         for (TreeItem item : tree.getItems()) {
3151                 NodeContext found = tryFind(item, context);
3152                 if(found != null) 
3153                         return true;
3154         }
3155         
3156         return false;
3157         
3158     }
3159
3160     protected ISelection constructSelection(NodeContext... contexts) {
3161         if (contexts ==  null)
3162             throw new IllegalArgumentException("null contexts");
3163         if (contexts.length == 0)
3164             return StructuredSelection.EMPTY;
3165         if (selectionFilter == null)
3166             return new StructuredSelection(transformSelection(contexts));
3167         return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );
3168     }
3169
3170     protected Object[] transformSelection(Object[] objects) {
3171         return selectionTransformation.call(this, objects);
3172     }
3173
3174     protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
3175         int len = contexts.length;
3176         Object[] objects = new Object[len];
3177         for (int i = 0; i < len; ++i)
3178             objects[i] = filter.filter(contexts[i]);
3179         return objects;
3180     }
3181
3182     @Override
3183     public void setExpanded(final NodeContext context, final boolean expanded) {
3184         assertNotDisposed();
3185         ThreadUtils.asyncExec(thread, new Runnable() {
3186             @Override
3187             public void run() {
3188                 if (!isDisposed())
3189                     doSetExpanded(context, expanded);
3190             }
3191         });
3192     }
3193
3194     private void doSetExpanded(NodeContext context, boolean expanded) {
3195         //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
3196         TreeItem item = contextToItem.getRight(context);
3197         if (item != null) {
3198             item.setExpanded(expanded);
3199         }
3200         PrimitiveQueryProcessor<?> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
3201         if (pqp instanceof IsExpandedProcessor) {
3202             IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
3203             iep.replaceExpanded(context, expanded);
3204         }
3205     }
3206
3207     @Override
3208     public void setColumnsVisible(boolean visible) {
3209         columnsAreVisible = visible;
3210         if(tree != null) tree.setHeaderVisible(columnsAreVisible);
3211     }
3212
3213     @Override
3214     public void setColumns(final Column[] columns) {
3215         setColumns(columns, null);
3216     }
3217
3218     @Override
3219     public void setColumns(final Column[] columns, Consumer<Map<Column, Object>> callback) {
3220         assertNotDisposed();
3221         checkUniqueColumnKeys(columns);
3222
3223         Display d = tree.getDisplay();
3224         if (d.getThread() == Thread.currentThread())
3225             doSetColumns(columns, callback);
3226         else
3227             d.asyncExec(() -> {
3228                 if (tree.isDisposed())
3229                     return;
3230                 doSetColumns(columns, callback);
3231             });
3232     }
3233
3234     private void checkUniqueColumnKeys(Column[] cols) {
3235         Set<String> usedColumnKeys = new HashSet<String>();
3236         List<Column> duplicateColumns = new ArrayList<Column>();
3237         for (Column c : cols) {
3238             if (!usedColumnKeys.add(c.getKey()))
3239                 duplicateColumns.add(c);
3240         }
3241         if (!duplicateColumns.isEmpty()) {
3242             throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
3243         }
3244     }
3245
3246     /**
3247      * Only meant to be invoked from the SWT UI thread.
3248      * 
3249      * @param cols
3250      */
3251     private void doSetColumns(Column[] cols, Consumer<Map<Column, Object>> callback) {
3252         // Attempt to keep previous column widths.
3253         Map<String, Integer> prevWidths = new HashMap<String, Integer>();
3254         for (TreeColumn column : tree.getColumns()) {
3255             prevWidths.put(column.getText(), column.getWidth());
3256             column.dispose();
3257         }
3258
3259         HashMap<String, Integer> keyToIndex = new HashMap<String, Integer>();
3260         for (int i = 0; i < cols.length; ++i) {
3261             keyToIndex.put(cols[i].getKey(), i);
3262         }
3263
3264         this.columns = Arrays.copyOf(cols, cols.length);
3265         //this.columns[cols.length] = FILLER_COLUMN;
3266         this.columnKeyToIndex = keyToIndex;
3267         this.columnImageArray = new Image[cols.length];
3268         this.columnDescOrImageArray = new Object[cols.length];
3269
3270         Map<Column, Object> map = new HashMap<Column, Object>();
3271
3272         tree.setHeaderVisible(columnsAreVisible);
3273         for (Column column : columns) {
3274             TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));
3275             map.put(column, c);
3276             c.setData(column);
3277             c.setText(column.getLabel());
3278             c.setToolTipText(column.getTooltip());
3279
3280             int cw = column.getWidth();
3281
3282             // Try to keep previous widths
3283             Integer w = prevWidths.get(column);
3284             if (w != null)
3285                 c.setWidth(w);
3286             else if (cw != Column.DEFAULT_CONTROL_WIDTH)
3287                 c.setWidth(cw);
3288             else {
3289                 // Go for some kind of default settings then...
3290                 if (ColumnKeys.PROPERTY.equals(column.getKey()))
3291                     c.setWidth(150);
3292                 else
3293                     c.setWidth(50);
3294             }
3295
3296 //            if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
3297 //                c.addListener(SWT.Resize, resizeListener);
3298 //                c.setResizable(true);
3299 //            } else {
3300 //                //c.setResizable(false);
3301 //            }
3302
3303         }
3304
3305         if(callback != null) callback.accept(map);
3306
3307         // Make sure the explorer fits the columns properly after initialization.
3308         tree.getDisplay().asyncExec(new Runnable() {
3309             @Override
3310             public void run() {
3311                 if (tree.isDisposed())
3312                     return;
3313                 refreshColumnSizes();
3314             }
3315         });
3316     }
3317
3318     int toSWT(Align alignment) {
3319         switch (alignment) {
3320             case LEFT: return SWT.LEFT;
3321             case CENTER: return SWT.CENTER;
3322             case RIGHT: return SWT.RIGHT;
3323             default: throw new Error("unhandled alignment: " + alignment);
3324         }
3325     }
3326
3327     @Override
3328     public Column[] getColumns() {
3329         return Arrays.copyOf(columns, columns.length);
3330     }
3331
3332     private void detachPrimitiveProcessors() {
3333         for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3334             if (p instanceof ProcessorLifecycle) {
3335                 ((ProcessorLifecycle) p).detached(this);
3336             }
3337         }
3338     }
3339
3340     private void clearPrimitiveProcessors() {
3341         for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3342             if (p instanceof ProcessorLifecycle) {
3343                 ((ProcessorLifecycle) p).clear();
3344             }
3345         }
3346     }
3347
3348     Listener resizeListener = new Listener() {
3349         @Override
3350         public void handleEvent(Event event) {
3351             // Prevent infinite recursion.
3352             if (refreshingColumnSizes)
3353                 return;
3354             //TreeColumn column = (TreeColumn) event.widget;
3355             //Column c = (Column) column.getData();
3356             refreshColumnSizes();
3357         }
3358     };
3359
3360     Listener itemDisposeListener = new Listener() {
3361         @Override
3362         public void handleEvent(Event event) {
3363             if (event.type == SWT.Dispose) {
3364                 if (event.widget instanceof TreeItem) {
3365                     TreeItem ti = (TreeItem) event.widget;
3366                     //NodeContext ctx = (NodeContext) ti.getData();
3367 //                    System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));
3368 //                    System.out.println("  map size BEFORE: " + contextToItem.size());
3369                     @SuppressWarnings("unused")
3370                     NodeContext removed = contextToItem.removeWithRight(ti);
3371 //                    System.out.println("  REMOVED: " + removed);
3372 //                    System.out.println("  map size AFTER: " + contextToItem.size());
3373                 }
3374             }
3375         }
3376     };
3377
3378     /**
3379      * 
3380      */
3381     LabelerListener labelListener = new LabelerListener() {
3382         @Override
3383         public boolean columnModified(final NodeContext context, final String key, final String newLabel) {
3384             //System.out.println("column " + key + " modified for " + context + " to " + newLabel);
3385             if (tree.isDisposed())
3386                 return false;
3387
3388             synchronized (labelRefreshRunnables) {
3389                 Runnable refresher = new Runnable() {
3390                     @Override
3391                     public void run() {
3392                         // Tree is guaranteed to be non-disposed if this is invoked.
3393
3394                         // contextToItem should be accessed only in the SWT thread to keep things thread-safe.
3395                         final TreeItem item = contextToItem.getRight(context);
3396                         if (item == null || item.isDisposed())
3397                             return;
3398
3399                         final Integer index = columnKeyToIndex.get(key);
3400                         if (index == null)
3401                             return;
3402
3403                         //System.out.println(" found index: " + index);
3404                         //System.out.println("  found item: " + item);
3405                         try {
3406                             GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
3407
3408                             // FIXME: indexOf is quadratic
3409                             int itemIndex = 0;
3410                             TreeItem parentItem = item.getParentItem();
3411                             if (parentItem == null) {
3412                                 itemIndex = tree.indexOf(item);
3413                                 //tree.clear(parentIndex, false);
3414                             } else {
3415                                 itemIndex = parentItem.indexOf(item);
3416                                 //item.clear(parentIndex, false);
3417                             }
3418                             setTextAndImage(item, manager, context, itemIndex);
3419                         } catch (SWTException e) {
3420                             ErrorLogger.defaultLogError(e);
3421                         }
3422                     }
3423                 };
3424                 //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
3425                 labelRefreshRunnables.put(context, refresher);
3426
3427                 if (!refreshIsQueued) {
3428                     refreshIsQueued = true;
3429                     long delay = 0;
3430                     long now = System.currentTimeMillis();
3431                     long elapsed = now - lastLabelRefreshScheduled;
3432                     if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)
3433                         delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;
3434                     //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");
3435                     if (delay > 0) {
3436                         ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
3437                             @Override
3438                             public void run() {
3439                                 scheduleImmediateLabelRefresh();
3440                             }
3441                         }, delay, TimeUnit.MILLISECONDS);
3442                     } else {
3443                         scheduleImmediateLabelRefresh();
3444                     }
3445                     lastLabelRefreshScheduled = now;
3446                 }
3447             }
3448             return true;
3449         }
3450
3451         @Override
3452         public boolean columnsModified(final NodeContext context, final Map<String, String> columns) {
3453             System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
3454             return false;
3455         }
3456     };
3457
3458     private void scheduleImmediateLabelRefresh() {
3459         Runnable[] runnables = null;
3460         synchronized (labelRefreshRunnables) {
3461             if (labelRefreshRunnables.isEmpty())
3462                 return;
3463
3464             runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
3465             labelRefreshRunnables.clear();
3466             refreshIsQueued = false;
3467         }
3468         final Runnable[] rs = runnables;
3469
3470         if (tree.isDisposed())
3471             return;
3472         tree.getDisplay().asyncExec(new Runnable() {
3473             @Override
3474             public void run() {
3475                 if (tree.isDisposed())
3476                     return;
3477                 //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");
3478                 tree.setRedraw(false);
3479                 for (Runnable r : rs) {
3480                     r.run();
3481                 }
3482                 tree.setRedraw(true);
3483             }
3484         });
3485     }
3486
3487     long                       lastLabelRefreshScheduled = 0;
3488     boolean                    refreshIsQueued           = false;
3489     Map<NodeContext, Runnable> labelRefreshRunnables     = new HashMap<NodeContext, Runnable>();
3490
3491     @SuppressWarnings("unchecked")
3492     @Override
3493     public <T> T getAdapter(Class<T> adapter) {
3494         if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;
3495         else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;
3496         return null;
3497     }
3498
3499     @SuppressWarnings("unchecked")
3500     @Override
3501     public <T> T getControl() {
3502         return (T) tree;
3503     }
3504
3505     /* (non-Javadoc)
3506      * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
3507      */
3508     @Override
3509     public void setAutoExpandLevel(int level) {
3510         this.autoExpandLevel = level;
3511     }
3512
3513     @Override
3514     public <T> NodeQueryProcessor<T> getProcessor(QueryKey<T> key) {
3515         return explorerContext.getProcessor(key);
3516     }
3517
3518     @Override
3519     public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(PrimitiveQueryKey<T> key) {
3520         return explorerContext.getPrimitiveProcessor(key);
3521     }
3522
3523     @Override
3524     public boolean isEditable() {
3525         return editable;
3526     }
3527
3528     @Override
3529     public void setEditable(boolean editable) {
3530         if (!thread.currentThreadAccess())
3531             throw new IllegalStateException("not in SWT display thread " + thread.getThread());
3532
3533         this.editable = editable;
3534         Display display = tree.getDisplay();
3535         tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
3536     }
3537
3538     /**
3539      * For setting a more local service locator for the explorer than the global
3540      * workbench service locator. Sometimes required to give this implementation
3541      * access to local workbench services like IFocusService.
3542      * 
3543      * <p>
3544      * Must be invoked during right after construction.
3545      * 
3546      * @param serviceLocator
3547      *            a specific service locator or <code>null</code> to use the
3548      *            workbench global service locator
3549      */
3550     public void setServiceLocator(IServiceLocator serviceLocator) {
3551         if (serviceLocator == null && PlatformUI.isWorkbenchRunning())
3552             serviceLocator = PlatformUI.getWorkbench();
3553         this.serviceLocator = serviceLocator;
3554         if (serviceLocator != null) {
3555             this.contextService = (IContextService) serviceLocator.getService(IContextService.class);
3556             this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);
3557         }
3558     }
3559     
3560     @Override
3561     public Object getClicked(Object event) {
3562         MouseEvent e = (MouseEvent)event;
3563         final Tree tree = (Tree) e.getSource();
3564         Point point = new Point(e.x, e.y);
3565         TreeItem item = tree.getItem(point);
3566
3567         // No selectable item at point?
3568         if (item == null)
3569             return null;
3570
3571         Object data = item.getData();
3572         return data;
3573     }
3574
3575 }