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