1 /*******************************************************************************
2 * Copyright (c) 2007, 2012 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.browsing.ui.swt;
14 import java.util.ArrayList;
15 import java.util.Arrays;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.Deque;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.Iterator;
22 import java.util.LinkedList;
23 import java.util.List;
26 import java.util.WeakHashMap;
27 import java.util.concurrent.CopyOnWriteArrayList;
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Future;
30 import java.util.concurrent.ScheduledExecutorService;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.atomic.AtomicBoolean;
33 import java.util.concurrent.atomic.AtomicReference;
34 import java.util.function.BiFunction;
35 import java.util.function.Consumer;
37 import org.eclipse.core.runtime.Assert;
38 import org.eclipse.core.runtime.AssertionFailedException;
39 import org.eclipse.core.runtime.IProgressMonitor;
40 import org.eclipse.core.runtime.IStatus;
41 import org.eclipse.core.runtime.MultiStatus;
42 import org.eclipse.core.runtime.Status;
43 import org.eclipse.core.runtime.jobs.Job;
44 import org.eclipse.jface.action.IStatusLineManager;
45 import org.eclipse.jface.resource.ColorDescriptor;
46 import org.eclipse.jface.resource.DeviceResourceException;
47 import org.eclipse.jface.resource.DeviceResourceManager;
48 import org.eclipse.jface.resource.FontDescriptor;
49 import org.eclipse.jface.resource.ImageDescriptor;
50 import org.eclipse.jface.resource.JFaceResources;
51 import org.eclipse.jface.resource.LocalResourceManager;
52 import org.eclipse.jface.resource.ResourceManager;
53 import org.eclipse.jface.viewers.IPostSelectionProvider;
54 import org.eclipse.jface.viewers.ISelection;
55 import org.eclipse.jface.viewers.ISelectionChangedListener;
56 import org.eclipse.jface.viewers.ISelectionProvider;
57 import org.eclipse.jface.viewers.IStructuredSelection;
58 import org.eclipse.jface.viewers.SelectionChangedEvent;
59 import org.eclipse.jface.viewers.StructuredSelection;
60 import org.eclipse.jface.viewers.TreeSelection;
61 import org.eclipse.swt.SWT;
62 import org.eclipse.swt.SWTException;
63 import org.eclipse.swt.custom.CCombo;
64 import org.eclipse.swt.custom.TreeEditor;
65 import org.eclipse.swt.events.FocusEvent;
66 import org.eclipse.swt.events.FocusListener;
67 import org.eclipse.swt.events.KeyEvent;
68 import org.eclipse.swt.events.KeyListener;
69 import org.eclipse.swt.events.MouseEvent;
70 import org.eclipse.swt.events.MouseListener;
71 import org.eclipse.swt.events.SelectionEvent;
72 import org.eclipse.swt.events.SelectionListener;
73 import org.eclipse.swt.graphics.Color;
74 import org.eclipse.swt.graphics.Font;
75 import org.eclipse.swt.graphics.GC;
76 import org.eclipse.swt.graphics.Image;
77 import org.eclipse.swt.graphics.Point;
78 import org.eclipse.swt.graphics.RGB;
79 import org.eclipse.swt.graphics.Rectangle;
80 import org.eclipse.swt.widgets.Composite;
81 import org.eclipse.swt.widgets.Control;
82 import org.eclipse.swt.widgets.Display;
83 import org.eclipse.swt.widgets.Event;
84 import org.eclipse.swt.widgets.Listener;
85 import org.eclipse.swt.widgets.ScrollBar;
86 import org.eclipse.swt.widgets.Shell;
87 import org.eclipse.swt.widgets.Text;
88 import org.eclipse.swt.widgets.Tree;
89 import org.eclipse.swt.widgets.TreeColumn;
90 import org.eclipse.swt.widgets.TreeItem;
91 import org.eclipse.ui.IWorkbenchPart;
92 import org.eclipse.ui.IWorkbenchSite;
93 import org.eclipse.ui.PlatformUI;
94 import org.eclipse.ui.contexts.IContextActivation;
95 import org.eclipse.ui.contexts.IContextService;
96 import org.eclipse.ui.services.IServiceLocator;
97 import org.eclipse.ui.swt.IFocusService;
98 import org.simantics.browsing.ui.BuiltinKeys;
99 import org.simantics.browsing.ui.CheckedState;
100 import org.simantics.browsing.ui.Column;
101 import org.simantics.browsing.ui.Column.Align;
102 import org.simantics.browsing.ui.DataSource;
103 import org.simantics.browsing.ui.ExplorerState;
104 import org.simantics.browsing.ui.GraphExplorer;
105 import org.simantics.browsing.ui.NodeContext;
106 import org.simantics.browsing.ui.NodeContext.CacheKey;
107 import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey;
108 import org.simantics.browsing.ui.NodeContext.QueryKey;
109 import org.simantics.browsing.ui.NodeContextPath;
110 import org.simantics.browsing.ui.NodeQueryManager;
111 import org.simantics.browsing.ui.NodeQueryProcessor;
112 import org.simantics.browsing.ui.PrimitiveQueryProcessor;
113 import org.simantics.browsing.ui.SelectionDataResolver;
114 import org.simantics.browsing.ui.SelectionFilter;
115 import org.simantics.browsing.ui.StatePersistor;
116 import org.simantics.browsing.ui.common.AdaptableHintContext;
117 import org.simantics.browsing.ui.common.ColumnKeys;
118 import org.simantics.browsing.ui.common.ErrorLogger;
119 import org.simantics.browsing.ui.common.NodeContextBuilder;
120 import org.simantics.browsing.ui.common.NodeContextUtil;
121 import org.simantics.browsing.ui.common.internal.GECache;
122 import org.simantics.browsing.ui.common.internal.GENodeQueryManager;
123 import org.simantics.browsing.ui.common.internal.IGECache;
124 import org.simantics.browsing.ui.common.internal.IGraphExplorerContext;
125 import org.simantics.browsing.ui.common.internal.UIElementReference;
126 import org.simantics.browsing.ui.common.processors.DefaultCheckedStateProcessor;
127 import org.simantics.browsing.ui.common.processors.DefaultComparableChildrenProcessor;
128 import org.simantics.browsing.ui.common.processors.DefaultFinalChildrenProcessor;
129 import org.simantics.browsing.ui.common.processors.DefaultImageDecoratorProcessor;
130 import org.simantics.browsing.ui.common.processors.DefaultImagerFactoriesProcessor;
131 import org.simantics.browsing.ui.common.processors.DefaultImagerProcessor;
132 import org.simantics.browsing.ui.common.processors.DefaultLabelDecoratorProcessor;
133 import org.simantics.browsing.ui.common.processors.DefaultLabelerFactoriesProcessor;
134 import org.simantics.browsing.ui.common.processors.DefaultLabelerProcessor;
135 import org.simantics.browsing.ui.common.processors.DefaultPrunedChildrenProcessor;
136 import org.simantics.browsing.ui.common.processors.DefaultSelectedImageDecoratorFactoriesProcessor;
137 import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelDecoratorFactoriesProcessor;
138 import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelerProcessor;
139 import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointFactoryProcessor;
140 import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointProcessor;
141 import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionProcessor;
142 import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionsProcessor;
143 import org.simantics.browsing.ui.common.processors.DefaultViewpointProcessor;
144 import org.simantics.browsing.ui.common.processors.IsExpandedProcessor;
145 import org.simantics.browsing.ui.common.processors.NoSelectionRequestProcessor;
146 import org.simantics.browsing.ui.common.processors.ProcessorLifecycle;
147 import org.simantics.browsing.ui.common.state.ExplorerStates;
148 import org.simantics.browsing.ui.content.ImageDecorator;
149 import org.simantics.browsing.ui.content.Imager;
150 import org.simantics.browsing.ui.content.LabelDecorator;
151 import org.simantics.browsing.ui.content.Labeler;
152 import org.simantics.browsing.ui.content.Labeler.CustomModifier;
153 import org.simantics.browsing.ui.content.Labeler.DeniedModifier;
154 import org.simantics.browsing.ui.content.Labeler.DialogModifier;
155 import org.simantics.browsing.ui.content.Labeler.EnumerationModifier;
156 import org.simantics.browsing.ui.content.Labeler.FilteringModifier;
157 import org.simantics.browsing.ui.content.Labeler.LabelerListener;
158 import org.simantics.browsing.ui.content.Labeler.Modifier;
159 import org.simantics.browsing.ui.content.PrunedChildrenResult;
160 import org.simantics.browsing.ui.model.nodetypes.EntityNodeType;
161 import org.simantics.browsing.ui.model.nodetypes.NodeType;
162 import org.simantics.browsing.ui.swt.internal.Threads;
163 import org.simantics.db.layer0.SelectionHints;
164 import org.simantics.utils.ObjectUtils;
165 import org.simantics.utils.datastructures.BijectionMap;
166 import org.simantics.utils.datastructures.disposable.AbstractDisposable;
167 import org.simantics.utils.datastructures.hints.IHintContext;
168 import org.simantics.utils.threads.IThreadWorkQueue;
169 import org.simantics.utils.threads.SWTThread;
170 import org.simantics.utils.threads.ThreadUtils;
171 import org.simantics.utils.ui.ISelectionUtils;
172 import org.simantics.utils.ui.SWTUtils;
173 import org.simantics.utils.ui.jface.BasePostSelectionProvider;
174 import org.simantics.utils.ui.widgets.VetoingEventHandler;
175 import org.simantics.utils.ui.workbench.WorkbenchUtils;
177 import gnu.trove.map.hash.THashMap;
178 import gnu.trove.procedure.TObjectProcedure;
179 import gnu.trove.set.hash.THashSet;
182 * @see #getMaxChildren()
183 * @see #setMaxChildren(int)
184 * @see #getMaxChildren(NodeQueryManager, NodeContext)
186 class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
188 private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
190 private GraphExplorerImpl ge;
192 GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
201 public void setSelection(final ISelection selection) {
202 if(ge == null) return;
203 ge.setSelection(selection, false);
208 public void removeSelectionChangedListener(ISelectionChangedListener listener) {
209 if(ge == null) return;
210 if(ge.isDisposed()) {
211 if (DEBUG_SELECTION_LISTENERS)
212 System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);
215 //assertNotDisposed();
216 //System.out.println("Remove selection changed listener: " + listener);
217 ge.selectionProvider.removeSelectionChangedListener(listener);
221 public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
222 if(ge == null) return;
223 if (!ge.thread.currentThreadAccess())
224 throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
225 if(ge.isDisposed()) {
226 System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);
229 //System.out.println("Add POST selection changed listener: " + listener);
230 ge.selectionProvider.addPostSelectionChangedListener(listener);
234 public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
235 if(ge == null) return;
236 if(ge.isDisposed()) {
237 if (DEBUG_SELECTION_LISTENERS)
238 System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);
241 // assertNotDisposed();
242 //System.out.println("Remove POST selection changed listener: " + listener);
243 ge.selectionProvider.removePostSelectionChangedListener(listener);
248 public void addSelectionChangedListener(ISelectionChangedListener listener) {
249 if(ge == null) return;
250 if (!ge.thread.currentThreadAccess())
251 throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
252 //System.out.println("Add selection changed listener: " + listener);
253 if (ge.tree.isDisposed() || ge.selectionProvider == null) {
254 System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);
258 ge.selectionProvider.addSelectionChangedListener(listener);
263 public ISelection getSelection() {
264 if(ge == null) return StructuredSelection.EMPTY;
265 if (!ge.thread.currentThreadAccess())
266 throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());
267 if (ge.tree.isDisposed() || ge.selectionProvider == null)
268 return StructuredSelection.EMPTY;
269 return ge.selectionProvider.getSelection();
275 * If this explorer is running with an Eclipse workbench open, this
276 * Workbench UI context will be activated whenever inline editing is started
277 * through {@link #startEditing(TreeItem, int)} and deactivated when inline
280 * This context information can be used to for UI handler activity testing.
282 private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
284 private static final String KEY_DRAG_COLUMN = "dragColumn";
286 private static final boolean DEBUG_SELECTION_LISTENERS = false;
288 private static final int DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
290 public static final int DEFAULT_MAX_CHILDREN = 1000;
292 private static final long POST_SELECTION_DELAY = 300;
295 * The time in milliseconds that must elapse between consecutive
296 * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)}
297 * invocations in order for this class to construct a new selection.
300 * This is done because selection construction can be very expensive as the
301 * selected set grows larger when the user is pressing shift+arrow keys.
302 * GraphExplorerImpl will naturally listen to all changes in the tree
303 * selection, but as an optimization will not construct new
304 * StructuredSelection instances for every selection change event. A new
305 * selection will be constructed and set only if the selection hasn't
306 * changed for the amount of milliseconds specified by this constant.
308 private static final long SELECTION_CHANGE_QUIET_TIME = 150;
310 private final IThreadWorkQueue thread;
313 * Local method for checking from whether resources are loaded in
316 private final LocalResourceManager localResourceManager;
319 * Local device resource manager that is safe to use in
320 * {@link ImageLoaderJob} for creating images in a non-UI thread.
322 private final ResourceManager resourceManager;
325 * Package visibility.
326 * TODO: Get rid of these.
330 @SuppressWarnings({ "rawtypes" })
331 final HashMap<CacheKey<?>, NodeQueryProcessor> processors = new HashMap<CacheKey<?>, NodeQueryProcessor>();
332 @SuppressWarnings({ "rawtypes" })
333 final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors = new HashMap<Object, PrimitiveQueryProcessor>();
334 @SuppressWarnings({ "rawtypes" })
335 final HashMap<Class, DataSource> dataSources = new HashMap<Class, DataSource>();
337 class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
338 // This is for query debugging only.
341 GECache cache = new GECache();
342 AtomicBoolean propagating = new AtomicBoolean(false);
343 Object propagateList = new Object();
344 Object propagate = new Object();
345 List<Runnable> scheduleList = new ArrayList<Runnable>();
346 final Deque<Integer> activity = new LinkedList<Integer>();
350 * Stores the currently running query update runnable. If
351 * <code>null</code> there's nothing scheduled yet in which case
352 * scheduling can commence. Otherwise the update should be skipped.
354 AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<Runnable>();
357 * Keeps track of nodes that have already been auto-expanded. After
358 * being inserted into this set, nodes will not be forced to stay in an
359 * expanded state after that. This makes it possible for the user to
360 * close auto-expanded nodes.
362 Map<NodeContext, Boolean> autoExpanded = new WeakHashMap<NodeContext, Boolean>();
366 protected void doDispose() {
368 autoExpanded.clear();
372 public IGECache getCache() {
377 public int queryIndent() {
382 public int queryIndent(int offset) {
383 queryIndent += offset;
388 @SuppressWarnings("unchecked")
389 public <T> NodeQueryProcessor<T> getProcessor(Object o) {
390 return processors.get(o);
394 @SuppressWarnings("unchecked")
395 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(Object o) {
396 return primitiveProcessors.get(o);
399 @SuppressWarnings("unchecked")
401 public <T> DataSource<T> getDataSource(Class<T> clazz) {
402 return dataSources.get(clazz);
406 public void update(UIElementReference ref) {
407 //System.out.println("GE.update " + ref);
408 TreeItemReference tiref = (TreeItemReference) ref;
409 TreeItem item = tiref.getItem();
410 // NOTE: must be called regardless of the the item value.
411 // A null item is currently used to indicate a tree root update.
412 GraphExplorerImpl.this.update(item);
416 public Object getPropagateLock() {
421 public Object getPropagateListLock() {
422 return propagateList;
426 public boolean isPropagating() {
427 return propagating.get();
431 public void setPropagating(boolean b) {
432 this.propagating.set(b);
436 public List<Runnable> getScheduleList() {
441 public void setScheduleList(List<Runnable> list) {
442 this.scheduleList = list;
446 public Deque<Integer> getActivity() {
451 public void setActivityInt(int i) {
452 this.activityInt = i;
456 public int getActivityInt() {
461 public void scheduleQueryUpdate(Runnable r) {
462 if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
464 //System.out.println("Scheduling query update for runnable " + r);
465 if (currentQueryUpdater.compareAndSet(null, r)) {
466 //System.out.println("Scheduling query update for runnable " + r);
467 queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);
471 Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
474 Runnable r = currentQueryUpdater.getAndSet(null);
476 //System.out.println("Running query update runnable " + r);
483 GraphExplorerContext explorerContext = new GraphExplorerContext();
485 HashSet<TreeItem> pendingItems = new HashSet<TreeItem>();
486 boolean updating = false;
487 boolean pendingRoot = false;
489 @SuppressWarnings("deprecation")
490 ModificationContext modificationContext = null;
492 NodeContext rootContext;
494 StatePersistor persistor = null;
496 boolean editable = true;
499 * This is a reverse mapping from {@link NodeContext} tree objects back to
500 * their owner TreeItems.
503 * Access this map only in the SWT thread to keep it thread-safe.
506 BijectionMap<NodeContext, TreeItem> contextToItem = new BijectionMap<NodeContext, TreeItem>();
509 * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
512 Column[] columns = new Column[0];
513 Map<String, Integer> columnKeyToIndex = new HashMap<String, Integer>();
514 boolean refreshingColumnSizes = false;
515 boolean columnsAreVisible = true;
518 * An array reused for invoking {@link TreeItem#setImage(Image[])} instead
519 * of constantly allocating new arrays for setting each TreeItems images.
520 * This works because {@link TreeItem#setImage(Image[])} does not take hold
521 * of the array itself, only the contents of the array.
523 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
525 Image[] columnImageArray = { null };
528 * Used for collecting Image or ImageDescriptor instances for a single
529 * TreeItem when initially setting images for a TreeItem.
531 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
533 Object[] columnDescOrImageArray = { null };
535 final ExecutorService queryUpdateScheduler = Threads.getExecutor();
536 final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor();
538 /** Set to true when the Tree widget is disposed. */
539 private boolean disposed = false;
540 private final CopyOnWriteArrayList<FocusListener> focusListeners = new CopyOnWriteArrayList<FocusListener>();
541 private final CopyOnWriteArrayList<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();
542 private final CopyOnWriteArrayList<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
544 /** Selection provider */
545 private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
546 protected BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider();
547 protected SelectionDataResolver selectionDataResolver;
548 protected SelectionFilter selectionFilter;
549 protected BiFunction<GraphExplorer, Object[], Object[]> selectionTransformation = new BiFunction<GraphExplorer, Object[], Object[]>() {
552 public Object[] apply(GraphExplorer explorer, Object[] objects) {
553 Object[] result = new Object[objects.length];
554 for (int i = 0; i < objects.length; i++) {
555 IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
556 context.setHint(SelectionHints.KEY_MAIN, objects[i]);
563 protected FontDescriptor originalFont;
564 protected ColorDescriptor originalForeground;
565 protected ColorDescriptor originalBackground;
568 * The set of currently selected TreeItem instances. This set is needed
569 * because we need to know in {@link #setData(Event)} whether the updated
570 * item was a part of the current selection in which case the selection must
573 private final Map<TreeItem, NodeContext> selectedItems = new HashMap<TreeItem, NodeContext>();
576 * TODO: specify what this is for
578 private final Set<NodeContext> selectionRefreshContexts = new HashSet<NodeContext>();
581 * If this field is non-null, it means that if {@link #setData(Event)}
582 * encounters a NodeContext equal to this one, it must make the TreeItem
583 * assigned to that NodeContext the topmost item of the tree using
584 * {@link Tree#setTopItem(TreeItem)}. After this the field value is
588 * This is related to {@link #initializeState()}, i.e. explorer state
591 // private NodeContext[] topNodePath = NodeContext.NONE;
592 // private int[] topNodePath = {};
593 // private int currentTopNodePathIndex = -1;
596 * See {@link #setAutoExpandLevel(int)}
598 private int autoExpandLevel = 0;
601 * <code>null</code> if not explicitly set through
602 * {@link #setServiceLocator(IServiceLocator)}.
604 private IServiceLocator serviceLocator;
607 * The global workbench context service, if the workbench is available.
608 * Retrieved in the constructor.
610 private IContextService contextService = null;
613 * The global workbench IFocusService, if the workbench is available.
614 * Retrieved in the constructor.
616 private IFocusService focusService = null;
619 * A Workbench UI context activation that is activated when starting inline
620 * editing through {@link #startEditing(TreeItem, int)}.
622 * @see #activateEditingContext()
623 * @see #deactivateEditingContext()
625 private IContextActivation editingContext = null;
627 static class ImageTask {
630 Object[] descsOrImages;
631 public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
634 this.descsOrImages = descsOrImages;
639 * The job that is used for off-loading image loading tasks (see
640 * {@link ImageTask} to a worker thread from the main UI thread.
642 * @see #setPendingImages(IProgressMonitor)
644 ImageLoaderJob imageLoaderJob;
647 * The set of currently gathered up image loading tasks for
648 * {@link #imageLoaderJob} to execute.
650 * @see #setPendingImages(IProgressMonitor)
652 Map<TreeItem, ImageTask> imageTasks = new THashMap<TreeItem, ImageTask>();
655 * A state flag indicating whether the vertical scroll bar was visible for
656 * {@link #tree} the last time it was checked. Since there is no listener
657 * that can provide this information, we check it in {@link #setData(Event)}
658 * every time any data for a TreeItem is updated. If the visibility changes,
659 * we will force re-layouting of the tree's parent composite.
661 * @see #setData(Event)
663 private boolean verticalBarVisible = false;
665 static class TransientStateImpl implements TransientExplorerState {
667 private Integer activeColumn = null;
670 public synchronized Integer getActiveColumn() {
674 public synchronized void setActiveColumn(Integer column) {
675 activeColumn = column;
680 private TransientStateImpl transientState = new TransientStateImpl();
682 boolean scheduleUpdater() {
684 if (tree.isDisposed())
687 if (pendingRoot == true || !pendingItems.isEmpty()) {
688 assert(!tree.isDisposed());
690 int activity = explorerContext.activityInt;
692 if (activity < 100) {
693 // System.out.println("Scheduling update immediately.");
694 } else if (activity < 1000) {
695 // System.out.println("Scheduling update after 500ms.");
698 // System.out.println("Scheduling update after 3000ms.");
704 //System.out.println("Scheduling UI update after " + delay + " ms.");
705 uiUpdateScheduler.schedule(new Runnable() {
709 if (tree.isDisposed())
712 if (updateCounter > 0) {
714 uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
716 tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
720 }, delay, TimeUnit.MILLISECONDS);
729 int updateCounter = 0;
731 void update(TreeItem item) {
733 synchronized(pendingItems) {
735 // System.out.println("update " + item);
739 if(item == null) pendingRoot = true;
740 else pendingItems.add(item);
742 if(updating == true) return;
750 private int maxChildren = DEFAULT_MAX_CHILDREN;
753 public int getMaxChildren() {
758 public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
759 Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
760 //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
761 if (result != null) {
763 throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
770 public void setMaxChildren(int maxChildren) {
771 this.maxChildren = maxChildren;
775 public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
776 this.modificationContext = modificationContext;
780 * @param parent the parent SWT composite
782 public GraphExplorerImpl(Composite parent) {
783 this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
787 * Stores the node context and the modifier that is currently being
788 * modified. These are used internally to prevent duplicate edits from being
789 * initiated which should always be a sensible thing to do.
791 private Set<NodeContext> currentlyModifiedNodes = new THashSet<NodeContext>();
793 private final TreeEditor editor;
794 private Color invalidModificationColor = null;
797 * @param item the TreeItem to start editing
798 * @param columnIndex the index of the column to edit, starts counting from
800 * @return <code>true</code> if the editing was initiated successfully or
801 * <code>false</code> if editing could not be started due to lack of
802 * {@link Modifier} for the labeler in question.
804 private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
806 return "Rename not supported for selection";
808 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
809 final NodeContext context = (NodeContext) item.getData();
810 Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
812 return "Rename not supported for selection";
814 if(columnKey == null) columnKey = columns[columnIndex].getKey();
816 // columnKey might be prefixed with '#' to indicate
817 // textual editing is preferred. Try to get modifier
818 // for that first and only if it fails, try without
820 Modifier modifier = labeler.getModifier(modificationContext, columnKey);
821 if (modifier == null) {
822 if(columnKey.startsWith("#"))
823 modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
824 if (modifier == null)
825 return "Rename not supported for selection";
827 if (modifier instanceof DeniedModifier) {
828 DeniedModifier dm = (DeniedModifier)modifier;
829 return dm.getMessage();
832 // Prevent editing of a single node context multiple times.
833 if (currentlyModifiedNodes.contains(context)) {
834 //System.out.println("discarding duplicate edit for context " + context);
835 return "Rename not supported for selection";
838 // Clean up any previous editor control
839 Control oldEditor = editor.getEditor();
840 if (oldEditor != null)
843 if (modifier instanceof DialogModifier) {
844 performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
845 } else if (modifier instanceof CustomModifier) {
846 startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
847 } else if (modifier instanceof EnumerationModifier) {
848 startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
850 startTextEditing(item, columnIndex, context, modifier);
862 void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
863 final DialogModifier modifier) {
864 final AtomicBoolean disposed = new AtomicBoolean(false);
865 Consumer<String> callback = result -> {
868 String error = modifier.isValid(result);
870 modifier.modify(result);
871 // Item may be disposed if the tree gets reset after a previous editing.
872 if (!item.isDisposed()) {
873 item.setText(columnIndex, result);
874 queueSelectionRefresh(context);
879 currentlyModifiedNodes.add(context);
881 String status = modifier.query(tree, item, columnIndex, context, callback);
883 ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
885 currentlyModifiedNodes.remove(context);
890 private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
891 Point size = control.computeSize(widthHint, heightHint);
892 editor.horizontalAlignment = SWT.LEFT;
893 Rectangle itemRect = item.getBounds(columnIndex),
894 rect = tree.getClientArea();
895 editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
896 int left = itemRect.x,
897 right = rect.x + rect.width;
898 editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
899 editor.minimumHeight = size.y + insetY * 2;
903 void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
904 GC gc = new GC(control);
905 Point size = gc.textExtent(text);
907 reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
916 void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
917 final CustomModifier modifier) {
918 final Object obj = modifier.createControl(tree, item, columnIndex, context);
919 if (!(obj instanceof Control))
920 throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
921 final Control control = (Control) obj;
923 // final int insetX = 0;
924 // final int insetY = 0;
925 // control.addListener(SWT.Resize, new Listener() {
927 // public void handleEvent(Event e) {
928 // Rectangle rect = control.getBounds();
929 // control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
932 control.addListener(SWT.Dispose, new Listener() {
934 public void handleEvent(Event event) {
935 currentlyModifiedNodes.remove(context);
936 queueSelectionRefresh(context);
937 deactivateEditingContext();
941 if (!(control instanceof Shell)) {
942 editor.setEditor(control, item, columnIndex);
948 GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
950 activateEditingContext(control);
952 // Removed in disposeListener above
953 currentlyModifiedNodes.add(context);
954 //System.out.println("START CUSTOM EDITING: " + item);
963 void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
964 String initialText = modifier.getValue();
965 if (initialText == null)
966 throw new AssertionError("Labeler.Modifier.getValue() returned null");
968 List<String> values = modifier.getValues();
969 String selectedValue = modifier.getValue();
970 int selectedIndex = values.indexOf(selectedValue);
971 if (selectedIndex == -1)
972 throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
974 final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
975 combo.setVisibleItemCount(10);
976 //combo.setEditable(false);
978 for (String value : values) {
981 combo.select(selectedIndex);
983 Listener comboListener = new Listener() {
984 boolean arrowTraverseUsed = false;
986 public void handleEvent(final Event e) {
987 //System.out.println("FOO: " + e);
990 if (e.character == SWT.CR) {
991 // Commit edit directly on ENTER press.
992 String text = combo.getText();
993 modifier.modify(text);
994 // Item may be disposed if the tree gets reset after a previous editing.
995 if (!item.isDisposed()) {
996 item.setText(columnIndex, text);
997 queueSelectionRefresh(context);
1001 } else if (e.keyCode == SWT.ESC) {
1002 // Cancel editing immediately
1009 if (arrowTraverseUsed) {
1010 arrowTraverseUsed = false;
1014 String text = combo.getText();
1015 modifier.modify(text);
1017 // Item may be disposed if the tree gets reset after a previous editing.
1018 if (!item.isDisposed()) {
1019 item.setText(columnIndex, text);
1020 queueSelectionRefresh(context);
1025 case SWT.FocusOut: {
1026 String text = combo.getText();
1027 modifier.modify(text);
1029 // Item may be disposed if the tree gets reset after a previous editing.
1030 if (!item.isDisposed()) {
1031 item.setText(columnIndex, text);
1032 queueSelectionRefresh(context);
1037 case SWT.Traverse: {
1039 case SWT.TRAVERSE_RETURN:
1040 String text = combo.getText();
1041 modifier.modify(text);
1042 if (!item.isDisposed()) {
1043 item.setText(columnIndex, text);
1044 queueSelectionRefresh(context);
1046 arrowTraverseUsed = false;
1048 case SWT.TRAVERSE_ESCAPE:
1052 case SWT.TRAVERSE_ARROW_NEXT:
1053 case SWT.TRAVERSE_ARROW_PREVIOUS:
1054 arrowTraverseUsed = true;
1057 //System.out.println("unhandled traversal: " + e.detail);
1063 currentlyModifiedNodes.remove(context);
1064 deactivateEditingContext();
1069 combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
1070 combo.addListener(SWT.KeyDown, comboListener);
1071 combo.addListener(SWT.FocusOut, comboListener);
1072 combo.addListener(SWT.Traverse, comboListener);
1073 combo.addListener(SWT.Selection, comboListener);
1074 combo.addListener(SWT.Dispose, comboListener);
1076 editor.setEditor(combo, item, columnIndex);
1080 GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
1082 activateEditingContext(combo);
1084 // Removed in comboListener
1085 currentlyModifiedNodes.add(context);
1087 combo.setListVisible(true);
1088 //System.out.println("START ENUMERATION EDITING: " + item);
1093 * @param columnIndex
1097 void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {
1098 String initialText = modifier.getValue();
1099 if (initialText == null)
1100 throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);
1102 final Composite composite = new Composite(tree, SWT.NONE);
1103 //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));
1104 final Text text = new Text(composite, SWT.BORDER);
1105 final int insetX = 0;
1106 final int insetY = 0;
1107 composite.addListener(SWT.Resize, new Listener() {
1109 public void handleEvent(Event e) {
1110 Rectangle rect = composite.getClientArea();
1111 text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height
1115 final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
1116 Listener textListener = new Listener() {
1118 boolean modified = false;
1121 public void handleEvent(final Event e) {
1127 //System.out.println("FOCUS OUT " + item);
1128 newText = text.getText();
1129 error = modifier.isValid(newText);
1130 if (error == null) {
1131 modifier.modify(newText);
1133 // Item may be disposed if the tree gets reset after a previous editing.
1134 if (!item.isDisposed()) {
1135 item.setText(columnIndex, newText);
1136 queueSelectionRefresh(context);
1139 // System.out.println("validation error: " + error);
1142 composite.dispose();
1145 newText = text.getText();
1146 error = modifier.isValid(newText);
1147 if (error != null) {
1148 text.setBackground(invalidModificationColor);
1150 //System.out.println("validation error: " + error);
1152 text.setBackground(null);
1159 // Safety check since it seems that this may happen with
1161 if (item.isDisposed())
1164 // Filter input if necessary
1165 e.text = filter != null ? filter.filter(e.text) : e.text;
1167 newText = text.getText();
1168 String leftText = newText.substring(0, e.start);
1169 String rightText = newText.substring(e.end, newText.length());
1170 GraphExplorerImpl.this.reconfigureTreeEditorForText(
1171 item, columnIndex, text, leftText + e.text + rightText,
1172 SWT.DEFAULT, insetX, insetY);
1176 case SWT.TRAVERSE_RETURN:
1178 newText = text.getText();
1179 error = modifier.isValid(newText);
1180 if (error == null) {
1181 modifier.modify(newText);
1182 if (!item.isDisposed()) {
1183 item.setText(columnIndex, newText);
1184 queueSelectionRefresh(context);
1189 case SWT.TRAVERSE_ESCAPE:
1190 composite.dispose();
1194 //System.out.println("unhandled traversal: " + e.detail);
1200 currentlyModifiedNodes.remove(context);
1201 deactivateEditingContext();
1207 // Set the initial text before registering a listener. We do not want immediate modification!
1208 text.setText(initialText);
1209 text.addListener(SWT.FocusOut, textListener);
1210 text.addListener(SWT.Traverse, textListener);
1211 text.addListener(SWT.Verify, textListener);
1212 text.addListener(SWT.Modify, textListener);
1213 text.addListener(SWT.Dispose, textListener);
1214 editor.setEditor(composite, item, columnIndex);
1218 // Initialize TreeEditor properly.
1219 GraphExplorerImpl.this.reconfigureTreeEditorForText(
1220 item, columnIndex, text, initialText,
1221 SWT.DEFAULT, insetX, insetY);
1223 // Removed in textListener
1224 currentlyModifiedNodes.add(context);
1226 activateEditingContext(text);
1228 //System.out.println("START TEXT EDITING: " + item);
1231 protected void errorStatus(String error) {
1232 IStatusLineManager status = getStatusLineManager();
1233 if (status != null) {
1234 status.setErrorMessage(error);
1238 protected IStatusLineManager getStatusLineManager() {
1239 if (serviceLocator instanceof IWorkbenchPart) {
1240 return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);
1241 } else if (serviceLocator instanceof IWorkbenchSite) {
1242 return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);
1247 protected void activateEditingContext(Control control) {
1248 if (contextService != null) {
1249 editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
1251 if (control != null && focusService != null) {
1252 focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);
1253 // No need to remove the control, it will be
1254 // removed automatically when it is disposed.
1258 protected void deactivateEditingContext() {
1259 IContextActivation a = editingContext;
1261 editingContext = null;
1262 contextService.deactivateContext(a);
1269 void queueSelectionRefresh(NodeContext forContext) {
1270 selectionRefreshContexts.add(forContext);
1274 public String startEditing(NodeContext context, String columnKey_) {
1275 assertNotDisposed();
1276 if (!thread.currentThreadAccess())
1277 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
1279 String columnKey = columnKey_;
1280 if(columnKey.startsWith("#")) {
1281 columnKey = columnKey.substring(1);
1284 Integer columnIndex = columnKeyToIndex.get(columnKey);
1285 if (columnIndex == null)
1286 return "Rename not supported for selection";
1288 TreeItem item = contextToItem.getRight(context);
1290 return "Rename not supported for selection";
1292 return startEditing(item, columnIndex, columnKey_);
1297 public String startEditing(String columnKey) {
1299 ISelection selection = postSelectionProvider.getSelection();
1300 if(selection == null) return "Rename not supported for selection";
1301 NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
1302 if(context == null) return "Rename not supported for selection";
1304 return startEditing(context, columnKey);
1309 * @param site <code>null</code> if the explorer is detached from the workbench
1310 * @param parent the parent SWT composite
1311 * @param style the tree style to use, check the see tags for the available flags
1316 * @see SWT#FULL_SELECTION
1317 * @see SWT#NO_SCROLL
1321 public GraphExplorerImpl(Composite parent, int style) {
1323 setServiceLocator(null);
1325 this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
1326 this.resourceManager = new DeviceResourceManager(parent.getDisplay());
1328 this.imageLoaderJob = new ImageLoaderJob(this);
1329 this.imageLoaderJob.setPriority(Job.DECORATE);
1331 invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
1333 this.thread = SWTThread.getThreadAccess(parent);
1335 for(int i=0;i<10;i++) explorerContext.activity.push(0);
1337 tree = new Tree(parent, style);
1338 tree.addListener(SWT.SetData, this);
1339 tree.addListener(SWT.Expand, this);
1340 tree.addListener(SWT.Dispose, this);
1341 tree.addListener(SWT.Activate, this);
1343 tree.setData(KEY_GRAPH_EXPLORER, this);
1345 // These are both required for performing column resizing without flicker.
1346 // See SWT.Resize event handling in #handleEvent() for more explanations.
1347 parent.addListener(SWT.Resize, this);
1348 tree.addListener(SWT.Resize, this);
1350 originalFont = JFaceResources.getDefaultFontDescriptor();
1351 // originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
1352 // originalForeground = tree.getForeground();
1354 tree.setFont((Font) localResourceManager.get(originalFont));
1356 columns = new Column[] { new Column(ColumnKeys.SINGLE) };
1357 columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
1359 editor = new TreeEditor(tree);
1360 editor.horizontalAlignment = SWT.LEFT;
1361 editor.grabHorizontal = true;
1362 editor.minimumWidth = 50;
1364 setBasicListeners();
1365 setDefaultProcessors();
1367 this.toolTip = new GraphExplorerToolTip(explorerContext, tree);
1371 public IThreadWorkQueue getThread() {
1375 TreeItem previousSingleSelection = null;
1376 long focusGainedAt = Long.MIN_VALUE;
1378 protected GraphExplorerToolTip toolTip;
1380 protected void setBasicListeners() {
1381 // Keep track of the previous single selection to help
1382 // decide whether to start editing a tree node on mouse
1384 tree.addListener(SWT.Selection, new Listener() {
1386 public void handleEvent(Event event) {
1387 TreeItem[] selection = tree.getSelection();
1388 if (selection.length == 1) {
1389 //for (TreeItem item : selection)
1390 // System.out.println("selection: " + item);
1391 previousSingleSelection = selection[0];
1393 previousSingleSelection = null;
1398 // Try to start editing of tree column when clicked for the second time.
1399 Listener mouseEditListener = new Listener() {
1401 Future<?> startEdit = null;
1404 public void handleEvent(Event event) {
1405 if (event.type == SWT.DragDetect) {
1406 // Needed to prevent editing from being started when in fact
1407 // the user starts dragging an item.
1408 //System.out.println("DRAG DETECT: " + event);
1412 //System.out.println("mouse down: " + event);
1413 if (event.button == 1) {
1414 // Always ignore the first mouse button press that focuses
1415 // the control. Do not let it start in-line editing since
1416 // that is very annoying to users and not how the UI's that
1417 // people are used to behave.
1418 long eventTime = ((long) event.time) & 0xFFFFFFFFL;
1419 if ((eventTime - focusGainedAt) < 250L) {
1420 //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));
1423 //System.out.println("testing whether to start editing");
1425 final Point point = new Point(event.x, event.y);
1426 final TreeItem item = tree.getItem(point);
1429 //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
1431 // Only start editing if the item was already selected.
1432 if (!item.equals(previousSingleSelection)) {
1437 if (tree.getColumnCount() > 1) {
1438 // TODO: reconsider this logic, might not be good in general.
1439 for (int i = 0; i < tree.getColumnCount(); i++) {
1440 if (item.getBounds(i).contains(point)) {
1441 tryScheduleEdit(event, item, point, 100, i);
1446 //System.out.println("clicks: " + event.count);
1447 if (item.getBounds().contains(point)) {
1448 if (event.count == 1) {
1449 tryScheduleEdit(event, item, point, 500, 0);
1458 void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {
1459 //System.out.println("\tCONTAINS: " + item);
1463 //System.out.println("\tScheduling edit: " + item);
1464 startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
1467 ThreadUtils.asyncExec(thread, new Runnable() {
1470 if (item.isDisposed())
1472 startEditing(item, column, null);
1476 }, delayMs, TimeUnit.MILLISECONDS);
1479 boolean cancelEdit() {
1480 Future<?> f = startEdit;
1482 // Try to cancel the start edit task if it's not running yet.
1485 boolean ret = f.cancel(false);
1486 //System.out.println("\tCancelled edit: " + ret);
1490 //System.out.println("\tNo edit in progress to cancel");
1494 tree.addListener(SWT.MouseDown, mouseEditListener);
1495 tree.addListener(SWT.DragDetect, mouseEditListener);
1496 tree.addListener(SWT.DragDetect, new Listener() {
1498 public void handleEvent(Event event) {
1499 Point test = new Point(event.x, event.y);
1500 TreeItem item = tree.getItem(test);
1502 for(int i=0;i<tree.getColumnCount();i++) {
1503 Rectangle rect = item.getBounds(i);
1504 if(rect.contains(test)) {
1505 tree.setData(KEY_DRAG_COLUMN, i);
1510 tree.setData(KEY_DRAG_COLUMN, -1);
1513 tree.addListener(SWT.MouseMove, new Listener() {
1515 public void handleEvent(Event event) {
1516 Point test = new Point(event.x, event.y);
1517 TreeItem item = tree.getItem(test);
1519 for(int i=0;i<tree.getColumnCount();i++) {
1520 Rectangle rect = item.getBounds(i);
1521 if(rect.contains(test)) {
1522 transientState.setActiveColumn(i);
1527 transientState.setActiveColumn(null);
1531 // Add focus/mouse/key listeners for supporting the respective
1532 // add/remove listener methods in IGraphExplorer.
1533 tree.addFocusListener(new FocusListener() {
1535 public void focusGained(FocusEvent e) {
1536 focusGainedAt = ((long) e.time) & 0xFFFFFFFFL;
1537 for (FocusListener listener : focusListeners)
1538 listener.focusGained(e);
1541 public void focusLost(FocusEvent e) {
1542 for (FocusListener listener : focusListeners)
1543 listener.focusLost(e);
1546 tree.addMouseListener(new MouseListener() {
1548 public void mouseDoubleClick(MouseEvent e) {
1549 for (MouseListener listener : mouseListeners) {
1550 listener.mouseDoubleClick(e);
1554 public void mouseDown(MouseEvent e) {
1555 for (MouseListener listener : mouseListeners) {
1556 listener.mouseDown(e);
1560 public void mouseUp(MouseEvent e) {
1561 for (MouseListener listener : mouseListeners) {
1562 listener.mouseUp(e);
1566 tree.addKeyListener(new KeyListener() {
1568 public void keyPressed(KeyEvent e) {
1569 for (KeyListener listener : keyListeners) {
1570 listener.keyPressed(e);
1574 public void keyReleased(KeyEvent e) {
1575 for (KeyListener listener : keyListeners) {
1576 listener.keyReleased(e);
1581 // Add a tree selection listener for keeping the selection of
1582 // GraphExplorer's ISelectionProvider up-to-date.
1583 tree.addSelectionListener(new SelectionListener() {
1585 public void widgetDefaultSelected(SelectionEvent e) {
1589 public void widgetSelected(SelectionEvent e) {
1590 widgetSelectionChanged(false);
1594 // This listener takes care of updating the set of currently selected
1595 // TreeItem instances. This set is needed because we need to know in
1596 // #setData(Event) whether the updated item was a part of the current
1597 // selection in which case the selection must be updated.
1598 selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
1600 public void selectionChanged(SelectionChangedEvent event) {
1601 //System.out.println("selection changed: " + event.getSelection());
1602 Set<NodeContext> set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);
1603 selectedItems.clear();
1604 for (NodeContext nc : set) {
1605 TreeItem item = contextToItem.getRight(nc);
1607 selectedItems.put(item, nc);
1609 //System.out.println("newly selected items: " + selectedItems);
1615 * Mod count for delaying post selection changed events.
1617 int postSelectionModCount = 0;
1620 * Last tree selection modification time for implementing a quiet
1621 * time for selection changes.
1623 long lastSelectionModTime = System.currentTimeMillis() - 10000;
1626 * Current target time for the selection to be set. Calculated
1627 * according to the set quiet time and last selection modification
1630 long selectionSetTargetTime = 0;
1633 * <code>true</code> if delayed selection runnable is current scheduled or
1636 boolean delayedSelectionScheduled = false;
1638 Runnable SELECTION_DELAY = new Runnable() {
1641 if (tree.isDisposed())
1643 long now = System.currentTimeMillis();
1644 long waitTimeLeft = selectionSetTargetTime - now;
1645 if (waitTimeLeft > 0) {
1646 // Not enough quiet time, reschedule.
1647 delayedSelectionScheduled = true;
1648 tree.getDisplay().timerExec((int) waitTimeLeft, this);
1650 // Time to perform selection, stop rescheduling.
1651 delayedSelectionScheduled = false;
1657 private void widgetSelectionChanged(boolean forceSelectionChange) {
1658 long modTime = System.currentTimeMillis();
1659 long delta = modTime - lastSelectionModTime;
1660 lastSelectionModTime = modTime;
1661 if (!forceSelectionChange && delta < SELECTION_CHANGE_QUIET_TIME) {
1662 long msToWait = SELECTION_CHANGE_QUIET_TIME - delta;
1663 selectionSetTargetTime = modTime + msToWait;
1664 if (!delayedSelectionScheduled) {
1665 delayedSelectionScheduled = true;
1666 tree.getDisplay().timerExec((int) msToWait, SELECTION_DELAY);
1668 // Make sure that post selection change events do not fire.
1669 ++postSelectionModCount;
1673 // Immediate selection reconstruction.
1677 private void resetSelection() {
1678 final ISelection selection = getWidgetSelection();
1680 //System.out.println("resetSelection(" + postSelectionModCount + ")");
1681 //System.out.println(" provider selection: " + selectionProvider.getSelection());
1682 //System.out.println(" widget selection: " + selection);
1684 selectionProvider.setAndFireNonEqualSelection(selection);
1686 // Implement deferred firing of post selection events
1687 final int count = ++postSelectionModCount;
1688 //System.out.println("[" + System.currentTimeMillis() + "] scheduling postSelectionChanged " + count + ": " + selection);
1689 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
1692 int newCount = postSelectionModCount;
1693 // Don't publish selection yet, there's another change incoming.
1694 //System.out.println("[" + System.currentTimeMillis() + "] checking post selection publish: " + count + " vs. " + newCount + ": " + selection);
1695 if (newCount != count)
1697 //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);
1699 if (tree.isDisposed())
1702 //System.out.println("scheduling fire post selection changed: " + selection);
1703 tree.getDisplay().asyncExec(new Runnable() {
1706 if (tree.isDisposed() || selectionProvider == null)
1708 //System.out.println("firing post selection changed: " + selection);
1709 selectionProvider.firePostSelection(selection);
1713 }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);
1716 protected void setDefaultProcessors() {
1717 // Add a simple IMAGER query processor that always returns null.
1718 // With this processor no images will ever be shown.
1719 // setPrimitiveProcessor(new StaticImagerProcessor(null));
1721 setProcessor(new DefaultComparableChildrenProcessor());
1722 setProcessor(new DefaultLabelDecoratorsProcessor());
1723 setProcessor(new DefaultImageDecoratorsProcessor());
1724 setProcessor(new DefaultSelectedLabelerProcessor());
1725 setProcessor(new DefaultLabelerFactoriesProcessor());
1726 setProcessor(new DefaultSelectedImagerProcessor());
1727 setProcessor(new DefaultImagerFactoriesProcessor());
1728 setPrimitiveProcessor(new DefaultLabelerProcessor());
1729 setPrimitiveProcessor(new DefaultCheckedStateProcessor());
1730 setPrimitiveProcessor(new DefaultImagerProcessor());
1731 setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());
1732 setPrimitiveProcessor(new DefaultImageDecoratorProcessor());
1733 setPrimitiveProcessor(new NoSelectionRequestProcessor());
1735 setProcessor(new DefaultFinalChildrenProcessor(this));
1737 setProcessor(new DefaultPrunedChildrenProcessor());
1738 setProcessor(new DefaultSelectedViewpointProcessor());
1739 setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());
1740 setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());
1741 setProcessor(new DefaultViewpointContributionsProcessor());
1743 setPrimitiveProcessor(new DefaultViewpointProcessor());
1744 setPrimitiveProcessor(new DefaultViewpointContributionProcessor());
1745 setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());
1746 setPrimitiveProcessor(new DefaultIsExpandedProcessor());
1747 setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());
1751 public <T> void setProcessor(NodeQueryProcessor<T> processor) {
1752 assertNotDisposed();
1753 if (processor == null)
1754 throw new IllegalArgumentException("null processor");
1756 processors.put(processor.getIdentifier(), processor);
1760 public <T> void setPrimitiveProcessor(PrimitiveQueryProcessor<T> processor) {
1761 assertNotDisposed();
1762 if (processor == null)
1763 throw new IllegalArgumentException("null processor");
1765 PrimitiveQueryProcessor<?> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
1767 if (oldProcessor instanceof ProcessorLifecycle)
1768 ((ProcessorLifecycle) oldProcessor).detached(this);
1769 if (processor instanceof ProcessorLifecycle)
1770 ((ProcessorLifecycle) processor).attached(this);
1774 public <T> void setDataSource(DataSource<T> provider) {
1775 assertNotDisposed();
1776 if (provider == null)
1777 throw new IllegalArgumentException("null provider");
1778 dataSources.put(provider.getProvidedClass(), provider);
1781 @SuppressWarnings("unchecked")
1783 public <T> DataSource<T> removeDataSource(Class<T> forProvidedClass) {
1784 assertNotDisposed();
1785 if (forProvidedClass == null)
1786 throw new IllegalArgumentException("null class");
1787 return dataSources.remove(forProvidedClass);
1791 public void setPersistor(StatePersistor persistor) {
1792 this.persistor = persistor;
1796 public SelectionDataResolver getSelectionDataResolver() {
1797 return selectionDataResolver;
1801 public void setSelectionDataResolver(SelectionDataResolver r) {
1802 this.selectionDataResolver = r;
1806 public SelectionFilter getSelectionFilter() {
1807 return selectionFilter;
1811 public void setSelectionFilter(SelectionFilter f) {
1812 this.selectionFilter = f;
1813 // TODO: re-filter current selection?
1817 public void setSelectionTransformation(BiFunction<GraphExplorer, Object[], Object[]> f) {
1818 this.selectionTransformation = f;
1822 public <T> void addListener(T listener) {
1823 if(listener instanceof FocusListener) {
1824 focusListeners.add((FocusListener)listener);
1825 } else if(listener instanceof MouseListener) {
1826 mouseListeners.add((MouseListener)listener);
1827 } else if(listener instanceof KeyListener) {
1828 keyListeners.add((KeyListener)listener);
1833 public <T> void removeListener(T listener) {
1834 if(listener instanceof FocusListener) {
1835 focusListeners.remove(listener);
1836 } else if(listener instanceof MouseListener) {
1837 mouseListeners.remove(listener);
1838 } else if(listener instanceof KeyListener) {
1839 keyListeners.remove(listener);
1843 public void addSelectionListener(SelectionListener listener) {
1844 tree.addSelectionListener(listener);
1847 public void removeSelectionListener(SelectionListener listener) {
1848 tree.removeSelectionListener(listener);
1851 private Set<String> uiContexts;
1854 public void setUIContexts(Set<String> contexts) {
1855 this.uiContexts = contexts;
1859 public void setRoot(final Object root) {
1860 if(uiContexts != null && uiContexts.size() == 1)
1861 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
1863 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
1867 public void setRootContext(final NodeContext context) {
1868 setRootContext0(context);
1871 private void setRootContext0(final NodeContext context) {
1872 Assert.isNotNull(context, "root must not be null");
1873 if (isDisposed() || tree.isDisposed())
1875 Display display = tree.getDisplay();
1876 if (display.getThread() == Thread.currentThread()) {
1879 display.asyncExec(new Runnable() {
1888 private void initializeState() {
1889 if (persistor == null)
1891 ExplorerStates.scheduleRead(getRoot(), persistor)
1892 .thenAccept(state -> SWTUtils.asyncExec(tree, () -> restoreState(state)));
1895 private void restoreState(ExplorerState state) {
1896 // topNodeToSet will be processed by #setData when it encounters a
1897 // NodeContext that matches this one.
1898 // topNodePath = state.topNodePath;
1899 // topNodePathChildIndex = state.topNodePathChildIndex;
1900 // currentTopNodePathIndex = 0;
1902 Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1903 if (processor instanceof DefaultIsExpandedProcessor) {
1904 DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
1905 for(NodeContext expanded : state.expandedNodes) {
1906 isExpandedProcessor.replaceExpanded(expanded, true);
1911 private void saveState() {
1912 if (persistor == null)
1915 NodeContext[] topNodePath = NodeContext.NONE;
1916 int[] topNodePathChildIndex = {};
1917 Collection<NodeContext> expandedNodes = Collections.emptyList();
1918 Map<String, Integer> columnWidths = Collections.<String, Integer> emptyMap();
1920 // Resolve top node path
1921 TreeItem topItem = tree.getTopItem();
1922 if (topItem != null) {
1923 NodeContext topNode = (NodeContext) topItem.getData();
1924 if (topNode != null) {
1925 topNodePath = getNodeContextPathSegments(topNode);
1926 topNodePathChildIndex = new int[topNodePath.length];
1927 for (int i = 0; i < topNodePath.length; ++i) {
1928 // TODO: get child indexes
1929 topNodePathChildIndex[i] = 0;
1934 // Resolve expanded nodes
1935 Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1936 if (processor instanceof IsExpandedProcessor) {
1937 IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;
1938 expandedNodes = isExpandedProcessor.getExpanded();
1942 TreeColumn[] columns = tree.getColumns();
1943 if (columns.length > 1) {
1944 columnWidths = new HashMap<String, Integer>();
1945 for (int i = 0; i < columns.length; ++i) {
1946 columnWidths.put(columns[i].getText(), columns[i].getWidth());
1950 persistor.serialize(
1951 ExplorerStates.explorerStateLocation(),
1953 new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
1957 * Invoke only from SWT thread to reset the root of the graph explorer tree.
1961 private void doSetRoot(NodeContext root) {
1962 if (tree.isDisposed())
1964 if (root.getConstant(BuiltinKeys.INPUT) == null) {
1965 ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));
1969 // Empty caches, release queries.
1970 GraphExplorerContext oldContext = explorerContext;
1971 GraphExplorerContext newContext = new GraphExplorerContext();
1972 GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));
1973 this.explorerContext = newContext;
1974 oldContext.safeDispose();
1975 toolTip.setGraphExplorerContext(explorerContext);
1977 // Need to empty these or otherwise they won't be emptied until the
1978 // explorer is disposed which would mean that many unwanted references
1979 // will be held by this map.
1980 clearPrimitiveProcessors();
1982 this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
1983 : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
1985 explorerContext.getCache().incRef(this.rootContext);
1989 NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
1991 tree.setItemCount(contexts.length);
1993 select(rootContext);
1994 refreshColumnSizes();
1998 public NodeContext getRoot() {
2003 public NodeContext getParentContext(NodeContext context) {
2005 throw new IllegalStateException("disposed");
2006 if (!thread.currentThreadAccess())
2007 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
2009 TreeItem item = contextToItem.getRight(context);
2010 if(item == null) return null;
2011 TreeItem parentItem = item.getParentItem();
2012 if(parentItem == null) return null;
2013 return (NodeContext)parentItem.getData();
2016 Point previousTreeSize;
2017 Point previousTreeParentSize;
2018 boolean activatedBefore = false;
2021 public void handleEvent(Event event) {
2022 //System.out.println("EVENT: " + event);
2023 switch(event.type) {
2025 //System.out.println("EXPAND: " + event.item);
2026 if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
2027 expandVirtual(event);
2029 System.out.println("TODO: non-virtual tree item expand");
2033 // Only invoked for SWT.VIRTUAL trees
2035 // Happened for Hannu once during program startup.
2036 // java.lang.AssertionError
2037 // at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)
2038 // at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)
2039 // at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)
2040 // I do not know whether SWT guarantees that SetData events
2041 // don't come after Dispose event has been issued, but I
2042 // think its better to have this check here just incase.
2047 // This ensures that column sizes are refreshed at
2048 // least once when the GE is first shown.
2049 if (!activatedBefore) {
2050 refreshColumnSizes();
2051 activatedBefore = true;
2055 //new Exception().printStackTrace();
2062 if (event.widget == tree) {
2063 // This case is meant for listening to tree width increase.
2064 // The column resizing must be performed only after the tree
2065 // itself as been resized.
2066 Point size = tree.getSize();
2068 if (previousTreeSize != null) {
2069 dx = size.x - previousTreeSize.x;
2071 previousTreeSize = size;
2072 //System.out.println("RESIZE: " + dx + " - size=" + size);
2075 tree.setRedraw(false);
2076 refreshColumnSizes(size);
2077 tree.setRedraw(true);
2079 } else if (event.widget == tree.getParent()) {
2080 // This case is meant for listening to tree width decrease.
2081 // The columns must be resized before the tree widget itself
2082 // is resized to prevent scroll bar flicker. This can be achieved
2083 // by listening to the resize events of the tree parent widget.
2084 Composite parent = tree.getParent();
2085 Point size = parent.getSize();
2087 // We must subtract the parent's border and possible
2088 // scroll bar width from the new target width of the columns.
2089 size.x -= tree.getParent().getBorderWidth() * 2;
2090 ScrollBar vBar = parent.getVerticalBar();
2091 if (vBar != null && vBar.isVisible())
2092 size.x -= vBar.getSize().x;
2095 if (previousTreeParentSize != null) {
2096 dx = size.x - previousTreeParentSize.x;
2098 previousTreeParentSize = size;
2099 //System.out.println("RESIZE: " + dx + " - size=" + size);
2102 tree.setRedraw(false);
2103 refreshColumnSizes(size);
2104 tree.setRedraw(true);
2114 protected void refreshColumnSizes() {
2115 // Composite treeParent = tree.getParent();
2116 // Point size = treeParent.getSize();
2117 // size.x -= treeParent.getBorderWidth() * 2;
2118 Point size = tree.getSize();
2119 refreshColumnSizes(size);
2120 tree.getParent().layout();
2124 * This has been disabled since the logic of handling column widths has been
2125 * externalized to parties creating {@link GraphExplorerImpl} instances.
2127 protected void refreshColumnSizes(Point toSize) {
2129 refreshingColumnSizes = true;
2131 int columnCount = tree.getColumnCount();
2132 if (columnCount > 0) {
2133 Point size = toSize;
2134 int targetWidth = size.x - tree.getBorderWidth() * 2;
2137 // Take the vertical scroll bar existence into to account when
2138 // calculating the overflow column width.
2139 ScrollBar vBar = tree.getVerticalBar();
2140 //if (vBar != null && vBar.isVisible())
2142 targetWidth -= vBar.getSize().x;
2144 List<TreeColumn> resizing = new ArrayList<TreeColumn>();
2146 int resizingWidth = 0;
2147 int totalWeight = 0;
2148 for (int i = 0; i < columnCount - 1; ++i) {
2149 TreeColumn col = tree.getColumn(i);
2150 //System.out.println(" " + col.getText() + ": " + col.getWidth());
2151 int width = col.getWidth();
2153 Column c = (Column) col.getData();
2156 resizingWidth += width;
2157 totalWeight += c.getWeight();
2161 int requiredWidthAdjustment = targetWidth - usedWidth;
2162 if (requiredWidthAdjustment < 0)
2163 requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());
2164 double diff = requiredWidthAdjustment;
2165 //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);
2167 // Decide how much to give space to / take space from each grabbing column
2168 double wrel = 1.0 / resizing.size();
2170 double[] weightedShares = new double[resizing.size()];
2171 for (int i = 0; i < resizing.size(); ++i) {
2172 TreeColumn col = resizing.get(i);
2173 Column c = (Column) col.getData();
2174 if (totalWeight == 0) {
2175 weightedShares[i] = wrel;
2177 weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
2180 //System.out.println("grabbing columns:" + resizing);
2181 //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
2183 // Always shrink the columns if necessary, but don't enlarge before
2184 // there is sufficient space to at least give all resizable columns
2186 if (diff < 0 || (diff > 0 && diff > resizing.size())) {
2187 // Need to either shrink or enlarge the resizable columns if possible.
2188 for (int i = 0; i < resizing.size(); ++i) {
2189 TreeColumn col = resizing.get(i);
2190 Column c = (Column) col.getData();
2191 int cw = col.getWidth();
2192 //double wrel = (double) cw / (double) resizingWidth;
2193 //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);
2194 double ddelta = weightedShares[i] * diff;
2197 delta = (int) Math.floor(ddelta);
2199 delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
2201 //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);
2202 //System.out.println("argh(" + col.getText() + "): " + c.getWidth() + " vs. " + col.getWidth() + " vs. " + (cw+delta));
2203 int newWidth = Math.max(c.getWidth(), cw + delta);
2204 requiredWidthAdjustment -= (newWidth - cw);
2205 col.setWidth(newWidth);
2209 //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
2211 TreeColumn last = tree.getColumn(columnCount - 1);
2212 // HACK: see #setColumns for why this is here.
2213 if (FILLER.equals(last.getText())) {
2214 last.setWidth(Math.max(0, requiredWidthAdjustment));
2218 refreshingColumnSizes = false;
2223 private void doDispose() {
2224 explorerContext.dispose();
2226 // No longer necessary, the used executors are shared.
2227 //scheduler.shutdown();
2228 //scheduler2.shutdown();
2231 detachPrimitiveProcessors();
2232 primitiveProcessors.clear();
2233 dataSources.clear();
2235 pendingItems.clear();
2239 contextToItem.clear();
2241 mouseListeners.clear();
2243 selectionProvider.clearListeners();
2244 selectionProvider = null;
2245 selectionDataResolver = null;
2246 selectionRefreshContexts.clear();
2247 selectedItems.clear();
2248 originalFont = null;
2250 localResourceManager.dispose();
2252 // Must shutdown image loader job before disposing its ResourceManager
2253 imageLoaderJob.dispose();
2254 imageLoaderJob.cancel();
2256 imageLoaderJob.join();
2257 } catch (InterruptedException e) {
2258 ErrorLogger.defaultLogError(e);
2260 resourceManager.dispose();
2262 postSelectionProvider.dispose();
2266 private void expandVirtual(final Event event) {
2267 TreeItem item = (TreeItem) event.item;
2268 assert (item != null);
2269 NodeContext context = (NodeContext) item.getData();
2270 assert (context != null);
2272 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));
2273 NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);
2274 int maxChildren = getMaxChildren(manager, context);
2275 item.setItemCount(children.length < maxChildren ? children.length : maxChildren);
2278 private NodeContext getNodeContext(TreeItem item) {
2279 assert(item != null);
2281 NodeContext context = (NodeContext)item.getData();
2282 assert(context != null);
2287 private NodeContext getParentContext(TreeItem item) {
2288 TreeItem parentItem = item.getParentItem();
2289 if(parentItem != null) {
2290 return getNodeContext(parentItem);
2296 private static final String LISTENER_SET_INDICATOR = "LSI";
2297 private static final String PENDING = "PENDING";
2298 private int contextSelectionChangeModCount = 0;
2301 * Only invoked for SWT.VIRTUAL widgets.
2305 private void setData(final Event event) {
2306 assert (event != null);
2307 TreeItem item = (TreeItem) event.item;
2308 assert (item != null);
2310 // Based on experience it seems to be possible that
2311 // SetData events are sent for disposed TreeItems.
2312 if (item.isDisposed() || item.getData(PENDING) != null)
2315 //System.out.println("GE.SetData " + item);
2317 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
2319 NodeContext parentContext = getParentContext(item);
2320 assert (parentContext != null);
2322 NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2324 // Some children have disappeared since counting
2325 if (event.index < 0) {
2326 ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());
2329 if (event.index >= parentChildren.length)
2332 NodeContext context = parentChildren[event.index];
2333 assert (context != null);
2334 item.setData(context);
2336 // Manage NodeContext -> TreeItem mappings
2337 contextToItem.map(context, item);
2338 if (item.getData(LISTENER_SET_INDICATOR) == null) {
2339 // This "if" exists because setData will get called many
2340 // times for the same (NodeContext, TreeItem) pairs.
2341 // Each TreeItem only needs one listener, but this
2342 // is needed to tell whether it already has a listener
2344 item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
2345 item.addListener(SWT.Dispose, itemDisposeListener);
2348 boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
2350 PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);
2351 int maxChildren = getMaxChildren(manager, context);
2352 //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);
2354 NodeContext[] pruned = children.getPrunedChildren();
2355 int count = Math.min(pruned.length, maxChildren);
2357 if (isExpanded || item.getItemCount() > 1) {
2358 item.setItemCount(count);
2359 TreeItem[] childItems = item.getItems();
2360 for(int i=0;i<count;i++)
2361 contextToItem.map(pruned[i], childItems[i]);
2363 if (children.getPrunedChildren().length == 0) {
2364 item.setItemCount(0);
2366 // item.setItemCount(1);
2367 item.setItemCount(count);
2368 TreeItem[] childItems = item.getItems();
2369 for(int i=0;i<count;i++)
2370 contextToItem.map(pruned[i], childItems[i]);
2371 // item.getItem(0).setData(PENDING, PENDING);
2372 // item.getItem(0).setItemCount(o);
2376 setTextAndImage(item, manager, context, event.index);
2378 // Check if the node should be auto-expanded?
2379 if ((autoExpandLevel == ALL_LEVELS || autoExpandLevel > 1) && !isExpanded) {
2380 //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");
2381 int level = getTreeItemLevel(item);
2382 if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)
2383 && !explorerContext.autoExpanded.containsKey(context))
2385 //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
2386 explorerContext.autoExpanded.put(context, Boolean.TRUE);
2387 setExpanded(context, true);
2391 item.setExpanded(isExpanded);
2393 if ((tree.getStyle() & SWT.CHECK) != 0) {
2394 CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);
2395 item.setChecked(CheckedState.CHECKED_STATES.contains(checked));
2396 item.setGrayed(CheckedState.GRAYED == checked);
2399 //System.out.println("GE.SetData completed " + item);
2401 // This test makes sure that selectionProvider holds the correct
2402 // selection with respect to the actual selection stored by the virtual
2404 // The data items shown below the items occupied by the selected and now removed data
2405 // will be squeezed to use the tree items previously used for the now
2406 // removed data. When this happens, the NodeContext items stored by the
2407 // tree items will be different from what the GraphExplorer's
2408 // ISelectionProvider thinks the selection currently is. To compensate,
2409 // 1. Recognize the situation
2410 // 2. ASAP set the selection provider selection to what is actually
2411 // offered by the tree widget.
2412 NodeContext selectedContext = selectedItems.get(item);
2413 // System.out.println("selectedContext(" + item + "): " + selectedContext);
2414 if (selectedContext != null && !selectedContext.equals(context)) {
2415 final int modCount = ++contextSelectionChangeModCount;
2416 // System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);
2417 // System.out.println(" old context: " + selectedContext);
2418 // System.out.println(" new context: " + context);
2419 // System.out.println(" provider selection: " + selectionProvider.getSelection());
2420 // System.out.println(" widget selection: " + getWidgetSelection());
2421 ThreadUtils.asyncExec(thread, new Runnable() {
2426 int count = contextSelectionChangeModCount;
2427 // System.out.println("MODCOUNT: " + modCount + " vs. " + count);
2428 if (modCount != count)
2430 widgetSelectionChanged(true);
2435 // This must be done to keep the visible tree selection properly
2436 // in sync with the selectionProvider JFace proxy of this class in
2437 // cases where an in-line editor was previously active for the node
2439 if (selectionRefreshContexts.remove(context)) {
2440 final ISelection currentSelection = selectionProvider.getSelection();
2441 // asyncExec is here to prevent ui glitches that
2442 // seem to occur if the selection setting is done
2443 // directly here in between setData invocations.
2444 ThreadUtils.asyncExec(thread, new Runnable() {
2449 // System.out.println("REFRESHING SELECTION: " + currentSelection);
2450 // System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));
2451 // System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());
2452 setSelection(currentSelection, true);
2453 // System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));
2454 // System.out.println("AFTER setSelection: " + selectionProvider.getSelection());
2459 // TODO: doesn't work if any part of the node path that should be
2460 // revealed is out of view.
2461 // Disabled until a better solution is devised.
2462 // Suggestion: include item indexes into the stored node context path
2463 // to make it possible for this method to know whether the current
2464 // node path segment is currently out of view based on event.index.
2465 // If out of view, this code needs to scroll the view programmatically
2467 // if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {
2468 // NodeContext topNode = topNodePath[currentTopNodePathIndex];
2469 // if (topNode.equals(context)) {
2470 // final TreeItem topItem = item;
2471 // ++currentTopNodePathIndex;
2472 // if (currentTopNodePathIndex >= topNodePath.length) {
2473 // // Mission accomplished. End search for top node here.
2474 // topNodePath = NodeContext.NONE;
2475 // currentTopNodePathIndex = -1;
2477 // ThreadUtils.asyncExec(thread, new Runnable() {
2479 // public void run() {
2480 // if (isDisposed())
2482 // tree.setTopItem(topItem);
2488 // Check if vertical scroll bar has become visible and refresh layout.
2489 ScrollBar verticalBar = tree.getVerticalBar();
2490 if(verticalBar != null) {
2491 boolean currentlyVerticalBarVisible = verticalBar.isVisible();
2492 if (verticalBarVisible != currentlyVerticalBarVisible) {
2493 verticalBarVisible = currentlyVerticalBarVisible;
2494 Composite parent = tree.getParent();
2502 * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the
2503 * return value is calculated. Items without parents have level=2,
2504 * their children level=3, etc. Returns 0 for invalid items
2506 private int getTreeItemLevel(TreeItem item) {
2510 for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);
2511 //System.out.println("\tgetTreeItemLevel(" + parent + ")");
2512 //System.out.println("level(" + item + "): " + level);
2520 private NodeContext[] getNodeContextPathSegments(NodeContext node) {
2521 TreeItem item = contextToItem.getRight(node);
2523 return NodeContext.NONE;
2524 int level = getTreeItemLevel(item);
2526 return NodeContext.NONE;
2527 // Exclude root from the saved node path.
2529 NodeContext[] segments = new NodeContext[level];
2530 for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {
2531 NodeContext ctx = (NodeContext) item.getData();
2533 return NodeContext.NONE;
2534 segments[level-1] = ctx;
2543 @SuppressWarnings("unused")
2544 private NodeContextPath getNodeContextPath(NodeContext node) {
2545 NodeContext[] path = getNodeContextPathSegments(node);
2546 return new NodeContextPath(path);
2549 void setImage(NodeContext node, TreeItem item, Imager imager, Collection<ImageDecorator> decorators, int itemIndex) {
2550 Image[] images = columnImageArray;
2551 Arrays.fill(images, null);
2552 if (imager == null) {
2553 item.setImage(images);
2557 Object[] descOrImage = columnDescOrImageArray;
2558 Arrays.fill(descOrImage, null);
2559 boolean finishLoadingInJob = false;
2561 for (Column column : columns) {
2562 String key = column.getKey();
2563 ImageDescriptor desc = imager.getImage(key);
2565 // Attempt to decorate the label
2566 if (!decorators.isEmpty()) {
2567 for (ImageDecorator id : decorators) {
2568 ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);
2574 // Try resolving only cached images here and now
2575 Object img = localResourceManager.find(desc);
2577 img = resourceManager.find(desc);
2579 images[index] = img != null ? (Image) img : null;
2580 descOrImage[index] = img == null ? desc : img;
2581 finishLoadingInJob |= img == null;
2586 // Finish loading the final image in the image loader job if necessary.
2587 if (finishLoadingInJob) {
2588 // Prevent UI from flashing unnecessarily by reusing the old image
2589 // in the item if it exists.
2590 for (int c = 0; c < columns.length; ++c) {
2591 Image img = item.getImage(c);
2595 item.setImage(images);
2597 // Schedule loading to another thread to refrain from blocking
2598 // the UI with database operations.
2599 queueImageTask(item, new ImageTask(
2602 Arrays.copyOf(descOrImage, descOrImage.length)));
2604 // Set any images that were resolved.
2605 item.setImage(images);
2609 private void queueImageTask(TreeItem item, ImageTask task) {
2610 synchronized (imageTasks) {
2611 imageTasks.put(item, task);
2613 imageLoaderJob.scheduleIfNecessary(100);
2617 * Invoked in a job worker thread.
2620 * @see ImageLoaderJob
2623 protected IStatus setPendingImages(IProgressMonitor monitor) {
2624 ImageTask[] tasks = null;
2625 synchronized (imageTasks) {
2626 tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);
2629 if (tasks.length == 0)
2630 return Status.OK_STATUS;
2632 MultiStatus status = null;
2634 // Load missing images
2635 for (ImageTask task : tasks) {
2636 Object[] descs = task.descsOrImages;
2637 for (int i = 0; i < descs.length; ++i) {
2638 Object obj = descs[i];
2639 if (obj instanceof ImageDescriptor) {
2640 ImageDescriptor desc = (ImageDescriptor) obj;
2642 descs[i] = resourceManager.get((ImageDescriptor) desc);
2643 } catch (DeviceResourceException e) {
2645 status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);
2646 status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));
2652 // Perform final UI updates in the UI thread.
2653 final ImageTask[] _tasks = tasks;
2654 thread.asyncExec(new Runnable() {
2657 if (!tree.isDisposed()) {
2658 tree.setRedraw(false);
2660 tree.setRedraw(true);
2665 return status != null ? status : Status.OK_STATUS;
2669 * Invoked in the UI thread only.
2673 void setImages(ImageTask[] tasks) {
2674 for (ImageTask task : tasks)
2680 * Invoked in the UI thread only.
2684 void setImage(ImageTask task) {
2685 // Be sure not to process disposed items.
2686 if (task.item.isDisposed())
2688 // Discard this task if the TreeItem has switched owning NodeContext.
2689 if (!contextToItem.contains(task.node, task.item))
2692 Object[] descs = task.descsOrImages;
2693 Image[] images = columnImageArray;
2694 Arrays.fill(images, null);
2695 for (int i = 0; i < descs.length; ++i) {
2696 Object desc = descs[i];
2697 if (desc instanceof Image) {
2698 images[i] = (Image) desc;
2701 task.item.setImage(images);
2704 void setText(TreeItem item, Labeler labeler, Collection<LabelDecorator> decorators, int itemIndex) {
2705 if (labeler != null) {
2706 String[] texts = new String[columns.length];
2708 Map<String, String> labels = labeler.getLabels();
2709 Map<String, String> runtimeLabels = labeler.getRuntimeLabels();
2710 for (Column column : columns) {
2711 String key = column.getKey();
2713 if (runtimeLabels != null) s = runtimeLabels.get(key);
2714 if (s == null) s = labels.get(key);
2716 FontDescriptor font = originalFont;
2717 ColorDescriptor bg = originalBackground;
2718 ColorDescriptor fg = originalForeground;
2720 // Attempt to decorate the label
2721 if (!decorators.isEmpty()) {
2722 for (LabelDecorator ld : decorators) {
2723 String ds = ld.decorateLabel(s, key, itemIndex);
2727 FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
2731 ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
2735 ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
2741 if (font != originalFont) {
2742 //System.out.println("set font: " + index + ": " + font);
2743 item.setFont(index, (Font) localResourceManager.get(font));
2745 if (bg != originalBackground)
2746 item.setBackground(index, (Color) localResourceManager.get(bg));
2747 if (fg != originalForeground)
2748 item.setForeground(index, (Color) localResourceManager.get(fg));
2754 item.setText(texts);
2756 item.setText(Labeler.NO_LABEL);
2760 void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {
2761 Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
2762 if (labeler != null) {
2763 labeler.setListener(labelListener);
2765 Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);
2766 Collection<LabelDecorator> labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);
2767 Collection<ImageDecorator> imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);
2769 setText(item, labeler, labelDecorators, itemIndex);
2770 setImage(context, item, imager, imageDecorators, itemIndex);
2774 public void setFocus() {
2779 public <T> T query(NodeContext context, CacheKey<T> key) {
2780 return this.explorerContext.cache.get(context, key);
2784 public boolean isDisposed() {
2788 protected void assertNotDisposed() {
2790 throw new IllegalStateException("disposed");
2797 * @param forceControlUpdate
2800 public void setSelection(final ISelection selection, boolean forceControlUpdate) {
2801 assertNotDisposed();
2802 boolean equalsOld = selectionProvider.selectionEquals(selection);
2803 if (equalsOld && !forceControlUpdate) {
2804 // Just set the selection object instance, fire no events nor update
2805 // the viewer selection.
2806 selectionProvider.setSelection(selection);
2808 // Schedule viewer and selection update if necessary.
2809 if (tree.isDisposed())
2811 Display d = tree.getDisplay();
2812 if (d.getThread() == Thread.currentThread()) {
2813 updateSelectionToControl(selection);
2815 d.asyncExec(new Runnable() {
2818 if (tree.isDisposed())
2820 updateSelectionToControl(selection);
2828 /* Contains the best currently found tree item and its priority
2830 private static class SelectionResolutionStatus {
2831 int bestPriority = Integer.MAX_VALUE;
2839 private void updateSelectionToControl(ISelection selection) {
2840 if (selectionDataResolver == null)
2842 if (!(selection instanceof IStructuredSelection))
2845 // Initialize selection resolution status map
2846 IStructuredSelection iss = (IStructuredSelection) selection;
2847 final THashMap<Object,SelectionResolutionStatus> statusMap =
2848 new THashMap<Object,SelectionResolutionStatus>(iss.size());
2849 for(Iterator<?> it = iss.iterator(); it.hasNext();) {
2850 Object selectionElement = it.next();
2851 Object resolvedElement = selectionDataResolver.resolve(selectionElement);
2854 new SelectionResolutionStatus());
2857 // Iterate all tree items and try to match them to the selection
2858 iterateTreeItems(new TObjectProcedure<TreeItem>() {
2860 public boolean execute(TreeItem treeItem) {
2861 NodeContext nodeContext = (NodeContext)treeItem.getData();
2862 if(nodeContext == null)
2864 SelectionResolutionStatus status = statusMap.get(nodeContext);
2865 if(status != null) {
2866 status.bestPriority = 0; // best possible match
2867 status.bestItem = treeItem;
2871 Object input = nodeContext.getConstant(BuiltinKeys.INPUT);
2872 status = statusMap.get(input);
2873 if(status != null) {
2874 NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);
2875 int curPriority = nodeType instanceof EntityNodeType
2876 ? 1 // Prefer EntityNodeType matches to other node types
2878 if(curPriority < status.bestPriority) {
2879 status.bestPriority = curPriority;
2880 status.bestItem = treeItem;
2888 ArrayList<TreeItem> items = new ArrayList<TreeItem>(statusMap.size());
2889 for(SelectionResolutionStatus status : statusMap.values())
2890 if(status.bestItem != null)
2891 items.add(status.bestItem);
2892 select(items.toArray(new TreeItem[items.size()]));
2898 public ISelection getWidgetSelection() {
2899 TreeItem[] items = tree.getSelection();
2900 if (items.length == 0)
2901 return StructuredSelection.EMPTY;
2903 List<NodeContext> nodes = new ArrayList<NodeContext>(items.length);
2905 // Caches for resolving node contexts the hard way if necessary.
2906 GENodeQueryManager manager = null;
2907 NodeContext lastParentContext = null;
2908 NodeContext[] lastChildren = null;
2910 for (int i = 0; i < items.length; i++) {
2911 TreeItem item = items[i];
2912 NodeContext ctx = (NodeContext) item.getData();
2913 // It may happen due to the virtual nature of the tree control
2914 // that it contains TreeItems which have not yet been ran through
2919 TreeItem parentItem = item.getParentItem();
2920 NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;
2921 if (parentContext != null) {
2922 NodeContext[] children = lastChildren;
2923 if (parentContext != lastParentContext) {
2924 if (manager == null)
2925 manager = new GENodeQueryManager(this.explorerContext, null, null, null);
2926 lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2927 lastParentContext = parentContext;
2929 int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);
2930 if (index >= 0 && index < children.length) {
2931 NodeContext child = children[index];
2932 if (child != null) {
2934 // Cache NodeContext in TreeItem for faster access
2935 item.setData(child);
2941 //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
2942 ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
2947 public TransientExplorerState getTransientState() {
2948 if (!thread.currentThreadAccess())
2949 throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());
2950 return transientState;
2957 private void select(TreeItem item) {
2958 tree.setSelection(item);
2959 tree.showSelection();
2960 selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
2967 private void select(TreeItem[] items) {
2968 //System.out.println("Select: " + Arrays.toString(items));
2969 tree.setSelection(items);
2970 tree.showSelection();
2971 NodeContext[] data = new NodeContext[items.length];
2972 for (int i = 0; i < data.length; i++) {
2973 data[i] = (NodeContext) items[i].getData();
2975 selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
2978 private void iterateTreeItems(TObjectProcedure<TreeItem> procedure) {
2979 for(TreeItem item : tree.getItems())
2980 if(!iterateTreeItems(item, procedure))
2984 private boolean iterateTreeItems(TreeItem item,
2985 TObjectProcedure<TreeItem> procedure) {
2986 if(!procedure.execute(item))
2988 if(item.getExpanded())
2989 for(TreeItem child : item.getItems())
2990 if(!iterateTreeItems(child, procedure))
3000 private boolean trySelect(TreeItem item, Object input) {
3001 NodeContext itemCtx = (NodeContext) item.getData();
3002 if (itemCtx != null) {
3003 if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {
3008 if (item.getExpanded()) {
3009 for (TreeItem child : item.getItems()) {
3010 if (trySelect(child, input))
3017 private boolean equalsEnough(NodeContext c1, NodeContext c2) {
3019 Object input1 = c1.getConstant(BuiltinKeys.INPUT);
3020 Object input2 = c2.getConstant(BuiltinKeys.INPUT);
3021 if(!ObjectUtils.objectEquals(input1, input2))
3024 Object type1 = c1.getConstant(NodeType.TYPE);
3025 Object type2 = c2.getConstant(NodeType.TYPE);
3026 if(!ObjectUtils.objectEquals(type1, type2))
3033 private NodeContext tryFind(NodeContext context) {
3034 for (TreeItem item : tree.getItems()) {
3035 NodeContext found = tryFind(item, context);
3036 if(found != null) return found;
3041 private NodeContext tryFind(TreeItem item, NodeContext context) {
3042 NodeContext itemCtx = (NodeContext) item.getData();
3043 if (itemCtx != null) {
3044 if (equalsEnough(context, itemCtx)) {
3048 if (item.getExpanded()) {
3049 for (TreeItem child : item.getItems()) {
3050 NodeContext found = tryFind(child, context);
3051 if(found != null) return found;
3058 public boolean select(NodeContext context) {
3060 assertNotDisposed();
3062 if (context == null || context.equals(rootContext)) {
3064 selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
3068 // if (context.equals(rootContext)) {
3069 // tree.deselectAll();
3070 // selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
3074 Object input = context.getConstant(BuiltinKeys.INPUT);
3076 for (TreeItem item : tree.getItems()) {
3077 if (trySelect(item, input))
3085 private NodeContext tryFind2(NodeContext context) {
3086 Set<NodeContext> ctxs = contextToItem.getLeftSet();
3087 for(NodeContext c : ctxs)
3088 if(equalsEnough(c, context))
3093 private boolean waitVisible(NodeContext parent, NodeContext context) {
3094 long start = System.nanoTime();
3096 TreeItem parentItem = contextToItem.getRight(parent);
3098 if(parentItem == null)
3102 NodeContext target = tryFind2(context);
3103 if(target != null) {
3104 TreeItem item = contextToItem.getRight(target);
3105 if (!(item.getParentItem().equals(parentItem)))
3107 tree.setTopItem(item);
3111 Display.getCurrent().readAndDispatch();
3112 long duration = System.nanoTime() - start;
3118 private boolean selectPathInternal(NodeContext[] contexts, int position) {
3119 //System.out.println("NodeContext path : " + contexts);
3121 NodeContext head = tryFind(contexts[position]);
3122 // tryFind may return null for positions, that actually have NodeContext.
3126 if(position == contexts.length-1) {
3127 return select(head);
3131 //setExpanded(head, true);
3133 if(!waitVisible(head, contexts[position+1]))
3136 setExpanded(head, true);
3138 return selectPathInternal(contexts, position+1);
3143 public boolean selectPath(Collection<NodeContext> contexts) {
3145 if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");
3146 if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");
3148 return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
3153 public boolean isVisible(NodeContext context) {
3155 for (TreeItem item : tree.getItems()) {
3156 NodeContext found = tryFind(item, context);
3165 protected ISelection constructSelection(NodeContext... contexts) {
3166 if (contexts == null)
3167 throw new IllegalArgumentException("null contexts");
3168 if (contexts.length == 0)
3169 return StructuredSelection.EMPTY;
3170 if (selectionFilter == null)
3171 return new StructuredSelection(transformSelection(contexts));
3172 return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );
3175 protected Object[] transformSelection(Object[] objects) {
3176 return selectionTransformation.apply(this, objects);
3179 protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
3180 int len = contexts.length;
3181 Object[] objects = new Object[len];
3182 for (int i = 0; i < len; ++i)
3183 objects[i] = filter.filter(contexts[i]);
3188 public void setExpanded(final NodeContext context, final boolean expanded) {
3189 assertNotDisposed();
3190 ThreadUtils.asyncExec(thread, new Runnable() {
3194 doSetExpanded(context, expanded);
3199 private void doSetExpanded(NodeContext context, boolean expanded) {
3200 //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
3201 TreeItem item = contextToItem.getRight(context);
3203 item.setExpanded(expanded);
3205 PrimitiveQueryProcessor<?> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
3206 if (pqp instanceof IsExpandedProcessor) {
3207 IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
3208 iep.replaceExpanded(context, expanded);
3213 public void setColumnsVisible(boolean visible) {
3214 columnsAreVisible = visible;
3215 if(tree != null) tree.setHeaderVisible(columnsAreVisible);
3219 public void setColumns(final Column[] columns) {
3220 setColumns(columns, null);
3224 public void setColumns(final Column[] columns, Consumer<Map<Column, Object>> callback) {
3225 assertNotDisposed();
3226 checkUniqueColumnKeys(columns);
3228 Display d = tree.getDisplay();
3229 if (d.getThread() == Thread.currentThread())
3230 doSetColumns(columns, callback);
3233 if (tree.isDisposed())
3235 doSetColumns(columns, callback);
3239 private void checkUniqueColumnKeys(Column[] cols) {
3240 Set<String> usedColumnKeys = new HashSet<String>();
3241 List<Column> duplicateColumns = new ArrayList<Column>();
3242 for (Column c : cols) {
3243 if (!usedColumnKeys.add(c.getKey()))
3244 duplicateColumns.add(c);
3246 if (!duplicateColumns.isEmpty()) {
3247 throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
3252 * Only meant to be invoked from the SWT UI thread.
3256 private void doSetColumns(Column[] cols, Consumer<Map<Column, Object>> callback) {
3257 // Attempt to keep previous column widths.
3258 Map<String, Integer> prevWidths = new HashMap<>();
3259 for (TreeColumn column : tree.getColumns()) {
3260 Column c = (Column) column.getData();
3262 prevWidths.put(c.getKey(), column.getWidth());
3267 HashMap<String, Integer> keyToIndex = new HashMap<>();
3268 for (int i = 0; i < cols.length; ++i) {
3269 keyToIndex.put(cols[i].getKey(), i);
3272 this.columns = Arrays.copyOf(cols, cols.length);
3273 //this.columns[cols.length] = FILLER_COLUMN;
3274 this.columnKeyToIndex = keyToIndex;
3275 this.columnImageArray = new Image[cols.length];
3276 this.columnDescOrImageArray = new Object[cols.length];
3278 Map<Column, Object> map = new HashMap<>();
3280 tree.setHeaderVisible(columnsAreVisible);
3281 for (Column column : columns) {
3282 TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));
3285 c.setText(column.getLabel());
3286 c.setToolTipText(column.getTooltip());
3288 int cw = column.getWidth();
3290 // Try to keep previous widths
3291 Integer w = prevWidths.get(column.getKey());
3294 else if (cw != Column.DEFAULT_CONTROL_WIDTH)
3297 // Go for some kind of default settings then...
3298 if (ColumnKeys.PROPERTY.equals(column.getKey()))
3304 // if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
3305 // c.addListener(SWT.Resize, resizeListener);
3306 // c.setResizable(true);
3308 // //c.setResizable(false);
3313 if(callback != null) callback.accept(map);
3315 // Make sure the explorer fits the columns properly after initialization.
3316 SWTUtils.asyncExec(tree, () -> {
3317 if (!tree.isDisposed())
3318 refreshColumnSizes();
3322 int toSWT(Align alignment) {
3323 switch (alignment) {
3324 case LEFT: return SWT.LEFT;
3325 case CENTER: return SWT.CENTER;
3326 case RIGHT: return SWT.RIGHT;
3327 default: throw new Error("unhandled alignment: " + alignment);
3332 public Column[] getColumns() {
3333 return Arrays.copyOf(columns, columns.length);
3336 private void detachPrimitiveProcessors() {
3337 for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3338 if (p instanceof ProcessorLifecycle) {
3339 ((ProcessorLifecycle) p).detached(this);
3344 private void clearPrimitiveProcessors() {
3345 for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3346 if (p instanceof ProcessorLifecycle) {
3347 ((ProcessorLifecycle) p).clear();
3352 Listener resizeListener = new Listener() {
3354 public void handleEvent(Event event) {
3355 // Prevent infinite recursion.
3356 if (refreshingColumnSizes)
3358 //TreeColumn column = (TreeColumn) event.widget;
3359 //Column c = (Column) column.getData();
3360 refreshColumnSizes();
3364 Listener itemDisposeListener = new Listener() {
3366 public void handleEvent(Event event) {
3367 if (event.type == SWT.Dispose) {
3368 if (event.widget instanceof TreeItem) {
3369 TreeItem ti = (TreeItem) event.widget;
3370 //NodeContext ctx = (NodeContext) ti.getData();
3371 // System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));
3372 // System.out.println(" map size BEFORE: " + contextToItem.size());
3373 @SuppressWarnings("unused")
3374 NodeContext removed = contextToItem.removeWithRight(ti);
3375 // System.out.println(" REMOVED: " + removed);
3376 // System.out.println(" map size AFTER: " + contextToItem.size());
3385 LabelerListener labelListener = new LabelerListener() {
3387 public boolean columnModified(final NodeContext context, final String key, final String newLabel) {
3388 //System.out.println("column " + key + " modified for " + context + " to " + newLabel);
3389 if (tree.isDisposed())
3392 synchronized (labelRefreshRunnables) {
3393 Runnable refresher = new Runnable() {
3396 // Tree is guaranteed to be non-disposed if this is invoked.
3398 // contextToItem should be accessed only in the SWT thread to keep things thread-safe.
3399 final TreeItem item = contextToItem.getRight(context);
3400 if (item == null || item.isDisposed())
3403 final Integer index = columnKeyToIndex.get(key);
3407 //System.out.println(" found index: " + index);
3408 //System.out.println(" found item: " + item);
3410 GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
3412 // FIXME: indexOf is quadratic
3414 TreeItem parentItem = item.getParentItem();
3415 if (parentItem == null) {
3416 itemIndex = tree.indexOf(item);
3417 //tree.clear(parentIndex, false);
3419 itemIndex = parentItem.indexOf(item);
3420 //item.clear(parentIndex, false);
3422 setTextAndImage(item, manager, context, itemIndex);
3423 } catch (SWTException e) {
3424 ErrorLogger.defaultLogError(e);
3428 //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
3429 labelRefreshRunnables.put(context, refresher);
3431 if (!refreshIsQueued) {
3432 refreshIsQueued = true;
3434 long now = System.currentTimeMillis();
3435 long elapsed = now - lastLabelRefreshScheduled;
3436 if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)
3437 delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;
3438 //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");
3440 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
3443 scheduleImmediateLabelRefresh();
3445 }, delay, TimeUnit.MILLISECONDS);
3447 scheduleImmediateLabelRefresh();
3449 lastLabelRefreshScheduled = now;
3456 public boolean columnsModified(final NodeContext context, final Map<String, String> columns) {
3457 System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
3462 private void scheduleImmediateLabelRefresh() {
3463 Runnable[] runnables = null;
3464 synchronized (labelRefreshRunnables) {
3465 if (labelRefreshRunnables.isEmpty())
3468 runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
3469 labelRefreshRunnables.clear();
3470 refreshIsQueued = false;
3472 final Runnable[] rs = runnables;
3474 if (tree.isDisposed())
3476 tree.getDisplay().asyncExec(new Runnable() {
3479 if (tree.isDisposed())
3481 //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");
3482 tree.setRedraw(false);
3483 for (Runnable r : rs) {
3486 tree.setRedraw(true);
3491 long lastLabelRefreshScheduled = 0;
3492 boolean refreshIsQueued = false;
3493 Map<NodeContext, Runnable> labelRefreshRunnables = new HashMap<NodeContext, Runnable>();
3495 @SuppressWarnings("unchecked")
3497 public <T> T getAdapter(Class<T> adapter) {
3498 if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;
3499 else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;
3503 @SuppressWarnings("unchecked")
3505 public <T> T getControl() {
3510 * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
3513 public void setAutoExpandLevel(int level) {
3514 this.autoExpandLevel = level;
3518 public <T> NodeQueryProcessor<T> getProcessor(QueryKey<T> key) {
3519 return explorerContext.getProcessor(key);
3523 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(PrimitiveQueryKey<T> key) {
3524 return explorerContext.getPrimitiveProcessor(key);
3528 public boolean isEditable() {
3533 public void setEditable(boolean editable) {
3534 if (!thread.currentThreadAccess())
3535 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
3537 this.editable = editable;
3538 Display display = tree.getDisplay();
3539 tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
3543 * For setting a more local service locator for the explorer than the global
3544 * workbench service locator. Sometimes required to give this implementation
3545 * access to local workbench services like IFocusService.
3548 * Must be invoked during right after construction.
3550 * @param serviceLocator
3551 * a specific service locator or <code>null</code> to use the
3552 * workbench global service locator
3554 public void setServiceLocator(IServiceLocator serviceLocator) {
3555 if (serviceLocator == null && PlatformUI.isWorkbenchRunning())
3556 serviceLocator = PlatformUI.getWorkbench();
3557 this.serviceLocator = serviceLocator;
3558 if (serviceLocator != null) {
3559 this.contextService = (IContextService) serviceLocator.getService(IContextService.class);
3560 this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);
3565 public Object getClicked(Object event) {
3566 MouseEvent e = (MouseEvent)event;
3567 final Tree tree = (Tree) e.getSource();
3568 Point point = new Point(e.x, e.y);
3569 TreeItem item = tree.getItem(point);
3571 // No selectable item at point?
3575 Object data = item.getData();