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