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
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.browsing.ui.swt;
\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
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
174 import gnu.trove.map.hash.THashMap;
\r
175 import gnu.trove.procedure.TObjectProcedure;
\r
176 import gnu.trove.set.hash.THashSet;
\r
179 * @see #getMaxChildren()
\r
180 * @see #setMaxChildren(int)
\r
181 * @see #getMaxChildren(NodeQueryManager, NodeContext)
\r
183 class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
\r
185 private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
\r
187 private GraphExplorerImpl ge;
\r
189 GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
\r
198 public void setSelection(final ISelection selection) {
\r
199 if(ge == null) return;
\r
200 ge.setSelection(selection, false);
\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
212 //assertNotDisposed();
\r
213 //System.out.println("Remove selection changed listener: " + listener);
\r
214 ge.selectionProvider.removeSelectionChangedListener(listener);
\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
226 //System.out.println("Add POST selection changed listener: " + listener);
\r
227 ge.selectionProvider.addPostSelectionChangedListener(listener);
\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
238 // assertNotDisposed();
\r
239 //System.out.println("Remove POST selection changed listener: " + listener);
\r
240 ge.selectionProvider.removePostSelectionChangedListener(listener);
\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
255 ge.selectionProvider.addSelectionChangedListener(listener);
\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
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
277 * This context information can be used to for UI handler activity testing.
\r
279 private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
\r
281 private static final String KEY_DRAG_COLUMN = "dragColumn";
\r
283 private static final boolean DEBUG_SELECTION_LISTENERS = false;
\r
285 private static final int DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
\r
287 public static final int DEFAULT_MAX_CHILDREN = 1000;
\r
289 private static final long POST_SELECTION_DELAY = 300;
\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
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
305 private static final long SELECTION_CHANGE_QUIET_TIME = 150;
\r
307 private final IThreadWorkQueue thread;
\r
310 * Local method for checking from whether resources are loaded in
\r
313 private final LocalResourceManager localResourceManager;
\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
319 private final ResourceManager resourceManager;
\r
322 * Package visibility.
\r
323 * TODO: Get rid of these.
\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
334 class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
\r
335 // This is for query debugging only.
\r
336 int queryIndent = 0;
\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
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
351 AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<Runnable>();
\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
359 Map<NodeContext, Boolean> autoExpanded = new WeakHashMap<NodeContext, Boolean>();
\r
363 protected void doDispose() {
\r
365 autoExpanded.clear();
\r
369 public IGECache getCache() {
\r
374 public int queryIndent() {
\r
375 return queryIndent;
\r
379 public int queryIndent(int offset) {
\r
380 queryIndent += offset;
\r
381 return queryIndent;
\r
385 @SuppressWarnings("unchecked")
\r
386 public <T> NodeQueryProcessor<T> getProcessor(Object o) {
\r
387 return processors.get(o);
\r
391 @SuppressWarnings("unchecked")
\r
392 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(Object o) {
\r
393 return primitiveProcessors.get(o);
\r
396 @SuppressWarnings("unchecked")
\r
398 public <T> DataSource<T> getDataSource(Class<T> clazz) {
\r
399 return dataSources.get(clazz);
\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
413 public Object getPropagateLock() {
\r
418 public Object getPropagateListLock() {
\r
419 return propagateList;
\r
423 public boolean isPropagating() {
\r
424 return propagating.get();
\r
428 public void setPropagating(boolean b) {
\r
429 this.propagating.set(b);
\r
433 public List<Runnable> getScheduleList() {
\r
434 return scheduleList;
\r
438 public void setScheduleList(List<Runnable> list) {
\r
439 this.scheduleList = list;
\r
443 public Deque<Integer> getActivity() {
\r
448 public void setActivityInt(int i) {
\r
449 this.activityInt = i;
\r
453 public int getActivityInt() {
\r
454 return activityInt;
\r
458 public void scheduleQueryUpdate(Runnable r) {
\r
459 if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
\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
468 Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
\r
470 public void run() {
\r
471 Runnable r = currentQueryUpdater.getAndSet(null);
\r
473 //System.out.println("Running query update runnable " + r);
\r
480 GraphExplorerContext explorerContext = new GraphExplorerContext();
\r
482 HashSet<TreeItem> pendingItems = new HashSet<TreeItem>();
\r
483 boolean updating = false;
\r
484 boolean pendingRoot = false;
\r
486 @SuppressWarnings("deprecation")
\r
487 ModificationContext modificationContext = null;
\r
489 NodeContext rootContext;
\r
491 StatePersistor persistor = null;
\r
493 boolean editable = true;
\r
496 * This is a reverse mapping from {@link NodeContext} tree objects back to
\r
497 * their owner TreeItems.
\r
500 * Access this map only in the SWT thread to keep it thread-safe.
\r
503 BijectionMap<NodeContext, TreeItem> contextToItem = new BijectionMap<NodeContext, TreeItem>();
\r
506 * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
\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
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
520 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
\r
522 Image[] columnImageArray = { null };
\r
525 * Used for collecting Image or ImageDescriptor instances for a single
\r
526 * TreeItem when initially setting images for a TreeItem.
\r
528 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
\r
530 Object[] columnDescOrImageArray = { null };
\r
532 final ExecutorService queryUpdateScheduler = Threads.getExecutor();
\r
533 final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor();
\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
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
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
560 protected FontDescriptor originalFont;
\r
561 protected ColorDescriptor originalForeground;
\r
562 protected ColorDescriptor originalBackground;
\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
570 private final Map<TreeItem, NodeContext> selectedItems = new HashMap<TreeItem, NodeContext>();
\r
573 * TODO: specify what this is for
\r
575 private final Set<NodeContext> selectionRefreshContexts = new HashSet<NodeContext>();
\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
585 * This is related to {@link #initializeState()}, i.e. explorer state
\r
588 // private NodeContext[] topNodePath = NodeContext.NONE;
\r
589 // private int[] topNodePath = {};
\r
590 // private int currentTopNodePathIndex = -1;
\r
593 * See {@link #setAutoExpandLevel(int)}
\r
595 private int autoExpandLevel = 0;
\r
598 * <code>null</code> if not explicitly set through
\r
599 * {@link #setServiceLocator(IServiceLocator)}.
\r
601 private IServiceLocator serviceLocator;
\r
604 * The global workbench context service, if the workbench is available.
\r
605 * Retrieved in the constructor.
\r
607 private IContextService contextService = null;
\r
610 * The global workbench IFocusService, if the workbench is available.
\r
611 * Retrieved in the constructor.
\r
613 private IFocusService focusService = null;
\r
616 * A Workbench UI context activation that is activated when starting inline
\r
617 * editing through {@link #startEditing(TreeItem, int)}.
\r
619 * @see #activateEditingContext()
\r
620 * @see #deactivateEditingContext()
\r
622 private IContextActivation editingContext = null;
\r
624 static class ImageTask {
\r
627 Object[] descsOrImages;
\r
628 public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
\r
631 this.descsOrImages = descsOrImages;
\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
639 * @see #setPendingImages(IProgressMonitor)
\r
641 ImageLoaderJob imageLoaderJob;
\r
644 * The set of currently gathered up image loading tasks for
\r
645 * {@link #imageLoaderJob} to execute.
\r
647 * @see #setPendingImages(IProgressMonitor)
\r
649 Map<TreeItem, ImageTask> imageTasks = new THashMap<TreeItem, ImageTask>();
\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
658 * @see #setData(Event)
\r
660 private boolean verticalBarVisible = false;
\r
662 static class TransientStateImpl implements TransientExplorerState {
\r
664 private Integer activeColumn = null;
\r
667 public synchronized Integer getActiveColumn() {
\r
668 return activeColumn;
\r
671 public synchronized void setActiveColumn(Integer column) {
\r
672 activeColumn = column;
\r
677 private TransientStateImpl transientState = new TransientStateImpl();
\r
679 boolean scheduleUpdater() {
\r
681 if (tree.isDisposed())
\r
684 if (pendingRoot == true || !pendingItems.isEmpty()) {
\r
685 assert(!tree.isDisposed());
\r
687 int activity = explorerContext.activityInt;
\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
695 // System.out.println("Scheduling update after 3000ms.");
\r
701 //System.out.println("Scheduling UI update after " + delay + " ms.");
\r
702 uiUpdateScheduler.schedule(new Runnable() {
\r
704 public void run() {
\r
706 if (tree.isDisposed())
\r
709 if (updateCounter > 0) {
\r
711 uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
\r
713 tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
\r
717 }, delay, TimeUnit.MILLISECONDS);
\r
726 int updateCounter = 0;
\r
728 void update(TreeItem item) {
\r
730 synchronized(pendingItems) {
\r
732 // System.out.println("update " + item);
\r
736 if(item == null) pendingRoot = true;
\r
737 else pendingItems.add(item);
\r
739 if(updating == true) return;
\r
747 private int maxChildren = DEFAULT_MAX_CHILDREN;
\r
750 public int getMaxChildren() {
\r
751 return maxChildren;
\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
760 throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
\r
763 return maxChildren;
\r
767 public void setMaxChildren(int maxChildren) {
\r
768 this.maxChildren = maxChildren;
\r
772 public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
\r
773 this.modificationContext = modificationContext;
\r
777 * @param parent the parent SWT composite
\r
779 public GraphExplorerImpl(Composite parent) {
\r
780 this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
\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
788 private Set<NodeContext> currentlyModifiedNodes = new THashSet<NodeContext>();
\r
790 private final TreeEditor editor;
\r
791 private Color invalidModificationColor = null;
\r
794 * @param item the TreeItem to start editing
\r
795 * @param columnIndex the index of the column to edit, starts counting from
\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
801 private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
\r
803 return "Rename not supported for selection";
\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
811 if(columnKey == null) columnKey = columns[columnIndex].getKey();
\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
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
824 if (modifier instanceof DeniedModifier) {
\r
825 DeniedModifier dm = (DeniedModifier)modifier;
\r
826 return dm.getMessage();
\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
835 // Clean up any previous editor control
\r
836 Control oldEditor = editor.getEditor();
\r
837 if (oldEditor != null)
\r
838 oldEditor.dispose();
\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
847 startTextEditing(item, columnIndex, context, modifier);
\r
855 * @param columnIndex
\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
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
876 currentlyModifiedNodes.add(context);
\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
882 currentlyModifiedNodes.remove(context);
\r
883 disposed.set(true);
\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
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
904 reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
\r
909 * @param columnIndex
\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
920 // final int insetX = 0;
\r
921 // final int insetY = 0;
\r
922 // control.addListener(SWT.Resize, new Listener() {
\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
929 control.addListener(SWT.Dispose, new Listener() {
\r
931 public void handleEvent(Event event) {
\r
932 currentlyModifiedNodes.remove(context);
\r
933 queueSelectionRefresh(context);
\r
934 deactivateEditingContext();
\r
937 editor.setEditor(control, item, columnIndex);
\r
939 control.setFocus();
\r
941 GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
\r
943 activateEditingContext(control);
\r
945 // Removed in disposeListener above
\r
946 currentlyModifiedNodes.add(context);
\r
947 //System.out.println("START CUSTOM EDITING: " + item);
\r
952 * @param columnIndex
\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
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
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
971 for (String value : values) {
\r
974 combo.select(selectedIndex);
\r
976 Listener comboListener = new Listener() {
\r
977 boolean arrowTraverseUsed = false;
\r
979 public void handleEvent(final Event e) {
\r
980 //System.out.println("FOO: " + e);
\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
994 } else if (e.keyCode == SWT.ESC) {
\r
995 // Cancel editing immediately
\r
1000 case SWT.Selection:
\r
1002 if (arrowTraverseUsed) {
\r
1003 arrowTraverseUsed = false;
\r
1007 String text = combo.getText();
\r
1008 modifier.modify(text);
\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
1018 case SWT.FocusOut: {
\r
1019 String text = combo.getText();
\r
1020 modifier.modify(text);
\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
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
1039 arrowTraverseUsed = false;
\r
1041 case SWT.TRAVERSE_ESCAPE:
\r
1045 case SWT.TRAVERSE_ARROW_NEXT:
\r
1046 case SWT.TRAVERSE_ARROW_PREVIOUS:
\r
1047 arrowTraverseUsed = true;
\r
1050 //System.out.println("unhandled traversal: " + e.detail);
\r
1056 currentlyModifiedNodes.remove(context);
\r
1057 deactivateEditingContext();
\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
1069 editor.setEditor(combo, item, columnIndex);
\r
1072 combo.setListVisible(true);
\r
1074 GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
\r
1076 activateEditingContext(combo);
\r
1078 // Removed in comboListener
\r
1079 currentlyModifiedNodes.add(context);
\r
1081 //System.out.println("START ENUMERATION EDITING: " + item);
\r
1086 * @param columnIndex
\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
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
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
1108 final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
\r
1109 Listener textListener = new Listener() {
\r
1111 boolean modified = false;
\r
1114 public void handleEvent(final Event e) {
\r
1118 case SWT.FocusOut:
\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
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
1132 // System.out.println("validation error: " + error);
\r
1135 composite.dispose();
\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
1145 text.setBackground(null);
\r
1146 errorStatus(null);
\r
1152 // Safety check since it seems that this may happen with
\r
1154 if (item.isDisposed())
\r
1157 // Filter input if necessary
\r
1158 e.text = filter != null ? filter.filter(e.text) : e.text;
\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
1167 case SWT.Traverse:
\r
1168 switch (e.detail) {
\r
1169 case SWT.TRAVERSE_RETURN:
\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
1182 case SWT.TRAVERSE_ESCAPE:
\r
1183 composite.dispose();
\r
1187 //System.out.println("unhandled traversal: " + e.detail);
\r
1193 currentlyModifiedNodes.remove(context);
\r
1194 deactivateEditingContext();
\r
1195 errorStatus(null);
\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
1211 // Initialize TreeEditor properly.
\r
1212 GraphExplorerImpl.this.reconfigureTreeEditorForText(
\r
1213 item, columnIndex, text, initialText,
\r
1214 SWT.DEFAULT, insetX, insetY);
\r
1216 // Removed in textListener
\r
1217 currentlyModifiedNodes.add(context);
\r
1219 activateEditingContext(text);
\r
1221 //System.out.println("START TEXT EDITING: " + item);
\r
1224 protected void errorStatus(String error) {
\r
1225 IStatusLineManager status = getStatusLineManager();
\r
1226 if (status != null) {
\r
1227 status.setErrorMessage(error);
\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
1240 protected void activateEditingContext(Control control) {
\r
1241 if (contextService != null) {
\r
1242 editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
\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
1251 protected void deactivateEditingContext() {
\r
1252 IContextActivation a = editingContext;
\r
1254 editingContext = null;
\r
1255 contextService.deactivateContext(a);
\r
1260 * @param forContext
\r
1262 void queueSelectionRefresh(NodeContext forContext) {
\r
1263 selectionRefreshContexts.add(forContext);
\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
1272 String columnKey = columnKey_;
\r
1273 if(columnKey.startsWith("#")) {
\r
1274 columnKey = columnKey.substring(1);
\r
1277 Integer columnIndex = columnKeyToIndex.get(columnKey);
\r
1278 if (columnIndex == null)
\r
1279 return "Rename not supported for selection";
\r
1281 TreeItem item = contextToItem.getRight(context);
\r
1283 return "Rename not supported for selection";
\r
1285 return startEditing(item, columnIndex, columnKey_);
\r
1290 public String startEditing(String columnKey) {
\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
1297 return startEditing(context, columnKey);
\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
1309 * @see SWT#FULL_SELECTION
\r
1310 * @see SWT#NO_SCROLL
\r
1311 * @see SWT#H_SCROLL
\r
1312 * @see SWT#V_SCROLL
\r
1314 public GraphExplorerImpl(Composite parent, int style) {
\r
1316 setServiceLocator(null);
\r
1318 this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
\r
1319 this.resourceManager = new DeviceResourceManager(parent.getDisplay());
\r
1321 this.imageLoaderJob = new ImageLoaderJob(this);
\r
1322 this.imageLoaderJob.setPriority(Job.DECORATE);
\r
1324 invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
\r
1326 this.thread = SWTThread.getThreadAccess(parent);
\r
1328 for(int i=0;i<10;i++) explorerContext.activity.push(0);
\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
1336 tree.setData(KEY_GRAPH_EXPLORER, this);
\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
1343 originalFont = JFaceResources.getDefaultFontDescriptor();
\r
1344 // originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
\r
1345 // originalForeground = tree.getForeground();
\r
1347 tree.setFont((Font) localResourceManager.get(originalFont));
\r
1349 columns = new Column[] { new Column(ColumnKeys.SINGLE) };
\r
1350 columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
\r
1352 editor = new TreeEditor(tree);
\r
1353 editor.horizontalAlignment = SWT.LEFT;
\r
1354 editor.grabHorizontal = true;
\r
1355 editor.minimumWidth = 50;
\r
1357 setBasicListeners();
\r
1358 setDefaultProcessors();
\r
1362 public IThreadWorkQueue getThread() {
\r
1366 TreeItem previousSingleSelection = null;
\r
1367 long focusGainedAt = Long.MIN_VALUE;
\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
1373 tree.addListener(SWT.Selection, new Listener() {
\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
1382 previousSingleSelection = null;
\r
1387 // Try to start editing of tree column when clicked for the second time.
\r
1388 Listener mouseEditListener = new Listener() {
\r
1390 Future<?> startEdit = null;
\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
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
1412 //System.out.println("testing whether to start editing");
\r
1414 final Point point = new Point(event.x, event.y);
\r
1415 final TreeItem item = tree.getItem(point);
\r
1418 //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
\r
1420 // Only start editing if the item was already selected.
\r
1421 if (!item.equals(previousSingleSelection)) {
\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
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
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
1452 //System.out.println("\tScheduling edit: " + item);
\r
1453 startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
\r
1455 public void run() {
\r
1456 ThreadUtils.asyncExec(thread, new Runnable() {
\r
1458 public void run() {
\r
1459 if (item.isDisposed())
\r
1461 startEditing(item, column, null);
\r
1465 }, delayMs, TimeUnit.MILLISECONDS);
\r
1468 boolean cancelEdit() {
\r
1469 Future<?> f = startEdit;
\r
1471 // Try to cancel the start edit task if it's not running yet.
\r
1473 if (!f.isDone()) {
\r
1474 boolean ret = f.cancel(false);
\r
1475 //System.out.println("\tCancelled edit: " + ret);
\r
1479 //System.out.println("\tNo edit in progress to cancel");
\r
1483 tree.addListener(SWT.MouseDown, mouseEditListener);
\r
1484 tree.addListener(SWT.DragDetect, mouseEditListener);
\r
1485 tree.addListener(SWT.DragDetect, new Listener() {
\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
1499 tree.setData(KEY_DRAG_COLUMN, -1);
\r
1502 tree.addListener(SWT.MouseMove, new Listener() {
\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
1516 transientState.setActiveColumn(null);
\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
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
1530 public void focusLost(FocusEvent e) {
\r
1531 for (FocusListener listener : focusListeners)
\r
1532 listener.focusLost(e);
\r
1535 tree.addMouseListener(new MouseListener() {
\r
1537 public void mouseDoubleClick(MouseEvent e) {
\r
1538 for (MouseListener listener : mouseListeners) {
\r
1539 listener.mouseDoubleClick(e);
\r
1543 public void mouseDown(MouseEvent e) {
\r
1544 for (MouseListener listener : mouseListeners) {
\r
1545 listener.mouseDown(e);
\r
1549 public void mouseUp(MouseEvent e) {
\r
1550 for (MouseListener listener : mouseListeners) {
\r
1551 listener.mouseUp(e);
\r
1555 tree.addKeyListener(new KeyListener() {
\r
1557 public void keyPressed(KeyEvent e) {
\r
1558 for (KeyListener listener : keyListeners) {
\r
1559 listener.keyPressed(e);
\r
1563 public void keyReleased(KeyEvent e) {
\r
1564 for (KeyListener listener : keyListeners) {
\r
1565 listener.keyReleased(e);
\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
1574 public void widgetDefaultSelected(SelectionEvent e) {
\r
1575 widgetSelected(e);
\r
1578 public void widgetSelected(SelectionEvent e) {
\r
1579 widgetSelectionChanged(false);
\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
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
1596 selectedItems.put(item, nc);
\r
1598 //System.out.println("newly selected items: " + selectedItems);
\r
1604 * Mod count for delaying post selection changed events.
\r
1606 int postSelectionModCount = 0;
\r
1609 * Last tree selection modification time for implementing a quiet
\r
1610 * time for selection changes.
\r
1612 long lastSelectionModTime = System.currentTimeMillis() - 10000;
\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
1619 long selectionSetTargetTime = 0;
\r
1622 * <code>true</code> if delayed selection runnable is current scheduled or
\r
1625 boolean delayedSelectionScheduled = false;
\r
1627 Runnable SELECTION_DELAY = new Runnable() {
\r
1629 public void run() {
\r
1630 if (tree.isDisposed())
\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
1639 // Time to perform selection, stop rescheduling.
\r
1640 delayedSelectionScheduled = false;
\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
1657 // Make sure that post selection change events do not fire.
\r
1658 ++postSelectionModCount;
\r
1662 // Immediate selection reconstruction.
\r
1666 private void resetSelection() {
\r
1667 final ISelection selection = getWidgetSelection();
\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
1673 selectionProvider.setAndFireNonEqualSelection(selection);
\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
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
1686 //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);
\r
1688 if (tree.isDisposed())
\r
1691 //System.out.println("scheduling fire post selection changed: " + selection);
\r
1692 tree.getDisplay().asyncExec(new Runnable() {
\r
1694 public void run() {
\r
1695 if (tree.isDisposed() || selectionProvider == null)
\r
1697 //System.out.println("firing post selection changed: " + selection);
\r
1698 selectionProvider.firePostSelection(selection);
\r
1702 }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);
\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
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
1724 setProcessor(new DefaultFinalChildrenProcessor(this));
\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
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
1740 public <T> void setProcessor(NodeQueryProcessor<T> processor) {
\r
1741 assertNotDisposed();
\r
1742 if (processor == null)
\r
1743 throw new IllegalArgumentException("null processor");
\r
1745 processors.put(processor.getIdentifier(), processor);
\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
1754 PrimitiveQueryProcessor<?> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
\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
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
1770 @SuppressWarnings("unchecked")
\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
1780 public void setPersistor(StatePersistor persistor) {
\r
1781 this.persistor = persistor;
\r
1785 public SelectionDataResolver getSelectionDataResolver() {
\r
1786 return selectionDataResolver;
\r
1790 public void setSelectionDataResolver(SelectionDataResolver r) {
\r
1791 this.selectionDataResolver = r;
\r
1795 public SelectionFilter getSelectionFilter() {
\r
1796 return selectionFilter;
\r
1800 public void setSelectionFilter(SelectionFilter f) {
\r
1801 this.selectionFilter = f;
\r
1802 // TODO: re-filter current selection?
\r
1806 public void setSelectionTransformation(BinaryFunction<Object[], GraphExplorer, Object[]> f) {
\r
1807 this.selectionTransformation = f;
\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
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
1832 public void addSelectionListener(SelectionListener listener) {
\r
1833 tree.addSelectionListener(listener);
\r
1836 public void removeSelectionListener(SelectionListener listener) {
\r
1837 tree.removeSelectionListener(listener);
\r
1840 private Set<String> uiContexts;
\r
1843 public void setUIContexts(Set<String> contexts) {
\r
1844 this.uiContexts = contexts;
\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
1852 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
\r
1856 public void setRootContext(final NodeContext context) {
\r
1857 setRootContext0(context);
\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
1864 Display display = tree.getDisplay();
\r
1865 if (display.getThread() == Thread.currentThread()) {
\r
1866 doSetRoot(context);
\r
1868 display.asyncExec(new Runnable() {
\r
1870 public void run() {
\r
1871 doSetRoot(context);
\r
1877 private void initializeState() {
\r
1878 if (persistor == null)
\r
1881 ExplorerState state = persistor.deserialize(
\r
1882 Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
\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
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
1900 private void saveState() {
\r
1901 if (persistor == null)
\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
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
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
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
1939 persistor.serialize(
\r
1940 Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
\r
1942 new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
\r
1946 * Invoke only from SWT thread to reset the root of the graph explorer tree.
\r
1950 private void doSetRoot(NodeContext root) {
\r
1951 if (tree.isDisposed())
\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
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
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
1970 this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
\r
1971 : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
\r
1973 explorerContext.getCache().incRef(this.rootContext);
\r
1975 initializeState();
\r
1977 NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
\r
1979 tree.setItemCount(contexts.length);
\r
1981 select(rootContext);
\r
1982 refreshColumnSizes();
\r
1986 public NodeContext getRoot() {
\r
1987 return rootContext;
\r
1991 public NodeContext getParentContext(NodeContext context) {
\r
1993 throw new IllegalStateException("disposed");
\r
1994 if (!thread.currentThreadAccess())
\r
1995 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
\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
2004 Point previousTreeSize;
\r
2005 Point previousTreeParentSize;
\r
2006 boolean activatedBefore = false;
\r
2009 public void handleEvent(Event event) {
\r
2010 //System.out.println("EVENT: " + event);
\r
2011 switch(event.type) {
\r
2013 //System.out.println("EXPAND: " + event.item);
\r
2014 if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
\r
2015 expandVirtual(event);
\r
2017 System.out.println("TODO: non-virtual tree item expand");
\r
2021 // Only invoked for SWT.VIRTUAL trees
\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
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
2043 //new Exception().printStackTrace();
\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
2056 if (previousTreeSize != null) {
\r
2057 dx = size.x - previousTreeSize.x;
\r
2059 previousTreeSize = size;
\r
2060 //System.out.println("RESIZE: " + dx + " - size=" + size);
\r
2063 tree.setRedraw(false);
\r
2064 refreshColumnSizes(size);
\r
2065 tree.setRedraw(true);
\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
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
2083 if (previousTreeParentSize != null) {
\r
2084 dx = size.x - previousTreeParentSize.x;
\r
2086 previousTreeParentSize = size;
\r
2087 //System.out.println("RESIZE: " + dx + " - size=" + size);
\r
2090 tree.setRedraw(false);
\r
2091 refreshColumnSizes(size);
\r
2092 tree.setRedraw(true);
\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
2112 * This has been disabled since the logic of handling column widths has been
\r
2113 * externalized to parties creating {@link GraphExplorerImpl} instances.
\r
2115 protected void refreshColumnSizes(Point toSize) {
\r
2117 refreshingColumnSizes = true;
\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
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
2130 targetWidth -= vBar.getSize().x;
\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
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
2155 // Decide how much to give space to / take space from each grabbing column
\r
2156 double wrel = 1.0 / resizing.size();
\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
2165 weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
\r
2168 //System.out.println("grabbing columns:" + resizing);
\r
2169 //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
\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
2185 delta = (int) Math.floor(ddelta);
\r
2187 delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
\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
2197 //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
\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
2206 refreshingColumnSizes = false;
\r
2211 private void doDispose() {
\r
2212 explorerContext.dispose();
\r
2214 // No longer necessary, the used executors are shared.
\r
2215 //scheduler.shutdown();
\r
2216 //scheduler2.shutdown();
\r
2218 processors.clear();
\r
2219 detachPrimitiveProcessors();
\r
2220 primitiveProcessors.clear();
\r
2221 dataSources.clear();
\r
2223 pendingItems.clear();
\r
2225 rootContext = null;
\r
2227 contextToItem.clear();
\r
2229 mouseListeners.clear();
\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
2238 localResourceManager.dispose();
\r
2240 // Must shutdown image loader job before disposing its ResourceManager
\r
2241 imageLoaderJob.dispose();
\r
2242 imageLoaderJob.cancel();
\r
2244 imageLoaderJob.join();
\r
2245 } catch (InterruptedException e) {
\r
2246 ErrorLogger.defaultLogError(e);
\r
2248 resourceManager.dispose();
\r
2250 postSelectionProvider.dispose();
\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
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
2266 private NodeContext getNodeContext(TreeItem item) {
\r
2267 assert(item != null);
\r
2269 NodeContext context = (NodeContext)item.getData();
\r
2270 assert(context != null);
\r
2275 private NodeContext getParentContext(TreeItem item) {
\r
2276 TreeItem parentItem = item.getParentItem();
\r
2277 if(parentItem != null) {
\r
2278 return getNodeContext(parentItem);
\r
2280 return rootContext;
\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
2289 * Only invoked for SWT.VIRTUAL widgets.
\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
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
2303 //System.out.println("GE.SetData " + item);
\r
2305 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
\r
2307 NodeContext parentContext = getParentContext(item);
\r
2308 assert (parentContext != null);
\r
2310 NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
\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
2317 if (event.index >= parentChildren.length)
\r
2320 NodeContext context = parentChildren[event.index];
\r
2321 assert (context != null);
\r
2322 item.setData(context);
\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
2332 item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
\r
2333 item.addListener(SWT.Dispose, itemDisposeListener);
\r
2336 boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
\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
2342 NodeContext[] pruned = children.getPrunedChildren();
\r
2343 int count = Math.min(pruned.length, maxChildren);
\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
2351 if (children.getPrunedChildren().length == 0) {
\r
2352 item.setItemCount(0);
\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
2364 setTextAndImage(item, manager, context, event.index);
\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
2373 //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
\r
2374 explorerContext.autoExpanded.put(context, Boolean.TRUE);
\r
2375 setExpanded(context, true);
\r
2379 item.setExpanded(isExpanded);
\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
2387 //System.out.println("GE.SetData completed " + item);
\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
2411 public void run() {
\r
2414 int count = contextSelectionChangeModCount;
\r
2415 // System.out.println("MODCOUNT: " + modCount + " vs. " + count);
\r
2416 if (modCount != count)
\r
2418 widgetSelectionChanged(true);
\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
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
2434 public void run() {
\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
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
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
2465 // ThreadUtils.asyncExec(thread, new Runnable() {
\r
2467 // public void run() {
\r
2468 // if (isDisposed())
\r
2470 // tree.setTopItem(topItem);
\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
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
2494 private int getTreeItemLevel(TreeItem item) {
\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
2508 private NodeContext[] getNodeContextPathSegments(NodeContext node) {
\r
2509 TreeItem item = contextToItem.getRight(node);
\r
2511 return NodeContext.NONE;
\r
2512 int level = getTreeItemLevel(item);
\r
2514 return NodeContext.NONE;
\r
2515 // Exclude root from the saved node path.
\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
2521 return NodeContext.NONE;
\r
2522 segments[level-1] = ctx;
\r
2531 @SuppressWarnings("unused")
\r
2532 private NodeContextPath getNodeContextPath(NodeContext node) {
\r
2533 NodeContext[] path = getNodeContextPathSegments(node);
\r
2534 return new NodeContextPath(path);
\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
2545 Object[] descOrImage = columnDescOrImageArray;
\r
2546 Arrays.fill(descOrImage, null);
\r
2547 boolean finishLoadingInJob = false;
\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
2562 // Try resolving only cached images here and now
\r
2563 Object img = localResourceManager.find(desc);
\r
2565 img = resourceManager.find(desc);
\r
2567 images[index] = img != null ? (Image) img : null;
\r
2568 descOrImage[index] = img == null ? desc : img;
\r
2569 finishLoadingInJob |= img == null;
\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
2583 item.setImage(images);
\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
2590 Arrays.copyOf(descOrImage, descOrImage.length)));
\r
2592 // Set any images that were resolved.
\r
2593 item.setImage(images);
\r
2597 private void queueImageTask(TreeItem item, ImageTask task) {
\r
2598 synchronized (imageTasks) {
\r
2599 imageTasks.put(item, task);
\r
2601 imageLoaderJob.scheduleIfNecessary(100);
\r
2605 * Invoked in a job worker thread.
\r
2608 * @see ImageLoaderJob
\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
2617 if (tasks.length == 0)
\r
2618 return Status.OK_STATUS;
\r
2620 MultiStatus status = null;
\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
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
2640 // Perform final UI updates in the UI thread.
\r
2641 final ImageTask[] _tasks = tasks;
\r
2642 thread.asyncExec(new Runnable() {
\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
2653 return status != null ? status : Status.OK_STATUS;
\r
2657 * Invoked in the UI thread only.
\r
2661 void setImages(ImageTask[] tasks) {
\r
2662 for (ImageTask task : tasks)
\r
2668 * Invoked in the UI thread only.
\r
2672 void setImage(ImageTask task) {
\r
2673 // Be sure not to process disposed items.
\r
2674 if (task.item.isDisposed())
\r
2676 // Discard this task if the TreeItem has switched owning NodeContext.
\r
2677 if (!contextToItem.contains(task.node, task.item))
\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
2689 task.item.setImage(images);
\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
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
2701 if (runtimeLabels != null) s = runtimeLabels.get(key);
\r
2702 if (s == null) s = labels.get(key);
\r
2704 FontDescriptor font = originalFont;
\r
2705 ColorDescriptor bg = originalBackground;
\r
2706 ColorDescriptor fg = originalForeground;
\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
2715 FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
\r
2716 if (dfont != null)
\r
2719 ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
\r
2723 ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
\r
2729 if (font != originalFont) {
\r
2730 //System.out.println("set font: " + index + ": " + font);
\r
2731 item.setFont(index, (Font) localResourceManager.get(font));
\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
2742 item.setText(texts);
\r
2744 item.setText(Labeler.NO_LABEL);
\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
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
2757 setText(item, labeler, labelDecorators, itemIndex);
\r
2758 setImage(context, item, imager, imageDecorators, itemIndex);
\r
2762 public void setFocus() {
\r
2767 public <T> T query(NodeContext context, CacheKey<T> key) {
\r
2768 return this.explorerContext.cache.get(context, key);
\r
2772 public boolean isDisposed() {
\r
2776 protected void assertNotDisposed() {
\r
2778 throw new IllegalStateException("disposed");
\r
2784 * @param selection
\r
2785 * @param forceControlUpdate
\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
2796 // Schedule viewer and selection update if necessary.
\r
2797 if (tree.isDisposed())
\r
2799 Display d = tree.getDisplay();
\r
2800 if (d.getThread() == Thread.currentThread()) {
\r
2801 updateSelectionToControl(selection);
\r
2803 d.asyncExec(new Runnable() {
\r
2805 public void run() {
\r
2806 if (tree.isDisposed())
\r
2808 updateSelectionToControl(selection);
\r
2816 /* Contains the best currently found tree item and its priority
\r
2818 private static class SelectionResolutionStatus {
\r
2819 int bestPriority = Integer.MAX_VALUE;
\r
2820 TreeItem bestItem;
\r
2824 * @param selection
\r
2827 private void updateSelectionToControl(ISelection selection) {
\r
2828 if (selectionDataResolver == null)
\r
2830 if (!(selection instanceof IStructuredSelection))
\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
2842 new SelectionResolutionStatus());
\r
2845 // Iterate all tree items and try to match them to the selection
\r
2846 iterateTreeItems(new TObjectProcedure<TreeItem>() {
\r
2848 public boolean execute(TreeItem treeItem) {
\r
2849 NodeContext nodeContext = (NodeContext)treeItem.getData();
\r
2850 if(nodeContext == null)
\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
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
2866 if(curPriority < status.bestPriority) {
\r
2867 status.bestPriority = curPriority;
\r
2868 status.bestItem = treeItem;
\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
2886 public ISelection getWidgetSelection() {
\r
2887 TreeItem[] items = tree.getSelection();
\r
2888 if (items.length == 0)
\r
2889 return StructuredSelection.EMPTY;
\r
2891 List<NodeContext> nodes = new ArrayList<NodeContext>(items.length);
\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
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
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
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
2922 // Cache NodeContext in TreeItem for faster access
\r
2923 item.setData(child);
\r
2929 //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
\r
2930 ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
\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
2945 private void select(TreeItem item) {
\r
2946 tree.setSelection(item);
\r
2947 tree.showSelection();
\r
2948 selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
\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
2963 selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
\r
2966 private void iterateTreeItems(TObjectProcedure<TreeItem> procedure) {
\r
2967 for(TreeItem item : tree.getItems())
\r
2968 if(!iterateTreeItems(item, procedure))
\r
2972 private boolean iterateTreeItems(TreeItem item,
\r
2973 TObjectProcedure<TreeItem> procedure) {
\r
2974 if(!procedure.execute(item))
\r
2976 if(item.getExpanded())
\r
2977 for(TreeItem child : item.getItems())
\r
2978 if(!iterateTreeItems(child, procedure))
\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
2996 if (item.getExpanded()) {
\r
2997 for (TreeItem child : item.getItems()) {
\r
2998 if (trySelect(child, input))
\r
3005 private boolean equalsEnough(NodeContext c1, NodeContext c2) {
\r
3007 Object input1 = c1.getConstant(BuiltinKeys.INPUT);
\r
3008 Object input2 = c2.getConstant(BuiltinKeys.INPUT);
\r
3009 if(!ObjectUtils.objectEquals(input1, input2))
\r
3012 Object type1 = c1.getConstant(NodeType.TYPE);
\r
3013 Object type2 = c2.getConstant(NodeType.TYPE);
\r
3014 if(!ObjectUtils.objectEquals(type1, type2))
\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
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
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
3046 public boolean select(NodeContext context) {
\r
3048 assertNotDisposed();
\r
3050 if (context == null || context.equals(rootContext)) {
\r
3051 tree.deselectAll();
\r
3052 selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
\r
3056 // if (context.equals(rootContext)) {
\r
3057 // tree.deselectAll();
\r
3058 // selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
\r
3062 Object input = context.getConstant(BuiltinKeys.INPUT);
\r
3064 for (TreeItem item : tree.getItems()) {
\r
3065 if (trySelect(item, input))
\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
3081 private boolean waitVisible(NodeContext parent, NodeContext context) {
\r
3082 long start = System.nanoTime();
\r
3084 TreeItem parentItem = contextToItem.getRight(parent);
\r
3086 if(parentItem == null)
\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
3095 tree.setTopItem(item);
\r
3099 Display.getCurrent().readAndDispatch();
\r
3100 long duration = System.nanoTime() - start;
\r
3101 if(duration > 10e9)
\r
3106 private boolean selectPathInternal(NodeContext[] contexts, int position) {
\r
3107 //System.out.println("NodeContext path : " + contexts);
\r
3109 NodeContext head = tryFind(contexts[position]);
\r
3111 if(position == contexts.length-1) {
\r
3112 return select(head);
\r
3116 //setExpanded(head, true);
\r
3118 if(!waitVisible(head, contexts[position+1]))
\r
3121 setExpanded(head, true);
\r
3123 return selectPathInternal(contexts, position+1);
\r
3128 public boolean selectPath(Collection<NodeContext> contexts) {
\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
3133 return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
\r
3138 public boolean isVisible(NodeContext context) {
\r
3140 for (TreeItem item : tree.getItems()) {
\r
3141 NodeContext found = tryFind(item, context);
\r
3142 if(found != null)
\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
3160 protected Object[] transformSelection(Object[] objects) {
\r
3161 return selectionTransformation.call(this, objects);
\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
3173 public void setExpanded(final NodeContext context, final boolean expanded) {
\r
3174 assertNotDisposed();
\r
3175 ThreadUtils.asyncExec(thread, new Runnable() {
\r
3177 public void run() {
\r
3178 if (!isDisposed())
\r
3179 doSetExpanded(context, expanded);
\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
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
3198 public void setColumnsVisible(boolean visible) {
\r
3199 columnsAreVisible = visible;
\r
3200 if(tree != null) tree.setHeaderVisible(columnsAreVisible);
\r
3204 public void setColumns(final Column[] columns) {
\r
3205 setColumns(columns, null);
\r
3209 public void setColumns(final Column[] columns, Consumer<Map<Column, Object>> callback) {
\r
3210 assertNotDisposed();
\r
3211 checkUniqueColumnKeys(columns);
\r
3213 Display d = tree.getDisplay();
\r
3214 if (d.getThread() == Thread.currentThread())
\r
3215 doSetColumns(columns, callback);
\r
3217 d.asyncExec(() -> {
\r
3218 if (tree.isDisposed())
\r
3220 doSetColumns(columns, callback);
\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
3231 if (!duplicateColumns.isEmpty()) {
\r
3232 throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
\r
3237 * Only meant to be invoked from the SWT UI thread.
\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
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
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
3260 Map<Column, Object> map = new HashMap<Column, Object>();
\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
3270 int cw = column.getWidth();
\r
3272 // Try to keep previous widths
\r
3273 Integer w = prevWidths.get(column);
\r
3276 else if (cw != Column.DEFAULT_CONTROL_WIDTH)
\r
3279 // Go for some kind of default settings then...
\r
3280 if (ColumnKeys.PROPERTY.equals(column.getKey()))
\r
3286 // if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
\r
3287 // c.addListener(SWT.Resize, resizeListener);
\r
3288 // c.setResizable(true);
\r
3290 // //c.setResizable(false);
\r
3295 if(callback != null) callback.accept(map);
\r
3297 // Make sure the explorer fits the columns properly after initialization.
\r
3298 tree.getDisplay().asyncExec(new Runnable() {
\r
3300 public void run() {
\r
3301 if (tree.isDisposed())
\r
3303 refreshColumnSizes();
\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
3318 public Column[] getColumns() {
\r
3319 return Arrays.copyOf(columns, columns.length);
\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
3330 private void clearPrimitiveProcessors() {
\r
3331 for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
\r
3332 if (p instanceof ProcessorLifecycle) {
\r
3333 ((ProcessorLifecycle) p).clear();
\r
3338 Listener resizeListener = new Listener() {
\r
3340 public void handleEvent(Event event) {
\r
3341 // Prevent infinite recursion.
\r
3342 if (refreshingColumnSizes)
\r
3344 //TreeColumn column = (TreeColumn) event.widget;
\r
3345 //Column c = (Column) column.getData();
\r
3346 refreshColumnSizes();
\r
3350 Listener itemDisposeListener = new Listener() {
\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
3371 LabelerListener labelListener = new LabelerListener() {
\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
3378 synchronized (labelRefreshRunnables) {
\r
3379 Runnable refresher = new Runnable() {
\r
3381 public void run() {
\r
3382 // Tree is guaranteed to be non-disposed if this is invoked.
\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
3389 final Integer index = columnKeyToIndex.get(key);
\r
3390 if (index == null)
\r
3393 //System.out.println(" found index: " + index);
\r
3394 //System.out.println(" found item: " + item);
\r
3396 GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
\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
3405 itemIndex = parentItem.indexOf(item);
\r
3406 //item.clear(parentIndex, false);
\r
3408 setTextAndImage(item, manager, context, itemIndex);
\r
3409 } catch (SWTException e) {
\r
3410 ErrorLogger.defaultLogError(e);
\r
3414 //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
\r
3415 labelRefreshRunnables.put(context, refresher);
\r
3417 if (!refreshIsQueued) {
\r
3418 refreshIsQueued = true;
\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
3426 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
\r
3428 public void run() {
\r
3429 scheduleImmediateLabelRefresh();
\r
3431 }, delay, TimeUnit.MILLISECONDS);
\r
3433 scheduleImmediateLabelRefresh();
\r
3435 lastLabelRefreshScheduled = now;
\r
3442 public boolean columnsModified(final NodeContext context, final Map<String, String> columns) {
\r
3443 System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
\r
3448 private void scheduleImmediateLabelRefresh() {
\r
3449 Runnable[] runnables = null;
\r
3450 synchronized (labelRefreshRunnables) {
\r
3451 if (labelRefreshRunnables.isEmpty())
\r
3454 runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
\r
3455 labelRefreshRunnables.clear();
\r
3456 refreshIsQueued = false;
\r
3458 final Runnable[] rs = runnables;
\r
3460 if (tree.isDisposed())
\r
3462 tree.getDisplay().asyncExec(new Runnable() {
\r
3464 public void run() {
\r
3465 if (tree.isDisposed())
\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
3472 tree.setRedraw(true);
\r
3477 long lastLabelRefreshScheduled = 0;
\r
3478 boolean refreshIsQueued = false;
\r
3479 Map<NodeContext, Runnable> labelRefreshRunnables = new HashMap<NodeContext, Runnable>();
\r
3481 @SuppressWarnings("unchecked")
\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
3489 @SuppressWarnings("unchecked")
\r
3491 public <T> T getControl() {
\r
3496 * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
\r
3499 public void setAutoExpandLevel(int level) {
\r
3500 this.autoExpandLevel = level;
\r
3504 public <T> NodeQueryProcessor<T> getProcessor(QueryKey<T> key) {
\r
3505 return explorerContext.getProcessor(key);
\r
3509 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(PrimitiveQueryKey<T> key) {
\r
3510 return explorerContext.getPrimitiveProcessor(key);
\r
3514 public boolean isEditable() {
\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
3523 this.editable = editable;
\r
3524 Display display = tree.getDisplay();
\r
3525 tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
\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
3534 * Must be invoked during right after construction.
\r
3536 * @param serviceLocator
\r
3537 * a specific service locator or <code>null</code> to use the
\r
3538 * workbench global service locator
\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