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