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