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