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