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