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.util.OpenStrategy;
54 import org.eclipse.jface.viewers.IPostSelectionProvider;
55 import org.eclipse.jface.viewers.ISelection;
56 import org.eclipse.jface.viewers.ISelectionChangedListener;
57 import org.eclipse.jface.viewers.ISelectionProvider;
58 import org.eclipse.jface.viewers.IStructuredSelection;
59 import org.eclipse.jface.viewers.SelectionChangedEvent;
60 import org.eclipse.jface.viewers.StructuredSelection;
61 import org.eclipse.jface.viewers.TreeSelection;
62 import org.eclipse.swt.SWT;
63 import org.eclipse.swt.SWTException;
64 import org.eclipse.swt.custom.CCombo;
65 import org.eclipse.swt.custom.TreeEditor;
66 import org.eclipse.swt.events.FocusEvent;
67 import org.eclipse.swt.events.FocusListener;
68 import org.eclipse.swt.events.KeyEvent;
69 import org.eclipse.swt.events.KeyListener;
70 import org.eclipse.swt.events.MouseEvent;
71 import org.eclipse.swt.events.MouseListener;
72 import org.eclipse.swt.events.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;
176 import org.slf4j.Logger;
177 import org.slf4j.LoggerFactory;
179 import gnu.trove.map.hash.THashMap;
180 import gnu.trove.procedure.TObjectProcedure;
181 import gnu.trove.set.hash.THashSet;
184 * @see #getMaxChildren()
185 * @see #setMaxChildren(int)
186 * @see #getMaxChildren(NodeQueryManager, NodeContext)
188 class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
190 private static final Logger LOGGER = LoggerFactory.getLogger(GraphExplorerImpl.class);
192 private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
194 private GraphExplorerImpl ge;
196 GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
205 public void setSelection(final ISelection selection) {
206 if(ge == null) return;
207 ge.setSelection(selection, false);
212 public void removeSelectionChangedListener(ISelectionChangedListener listener) {
213 if(ge == null) return;
214 if(ge.isDisposed()) {
215 if (DEBUG_SELECTION_LISTENERS)
216 System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);
219 //assertNotDisposed();
220 //System.out.println("Remove selection changed listener: " + listener);
221 ge.selectionProvider.removeSelectionChangedListener(listener);
225 public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
226 if(ge == null) return;
227 if (!ge.thread.currentThreadAccess())
228 throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
229 if(ge.isDisposed()) {
230 System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);
233 //System.out.println("Add POST selection changed listener: " + listener);
234 ge.selectionProvider.addPostSelectionChangedListener(listener);
238 public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
239 if(ge == null) return;
240 if(ge.isDisposed()) {
241 if (DEBUG_SELECTION_LISTENERS)
242 System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);
245 // assertNotDisposed();
246 //System.out.println("Remove POST selection changed listener: " + listener);
247 ge.selectionProvider.removePostSelectionChangedListener(listener);
252 public void addSelectionChangedListener(ISelectionChangedListener listener) {
253 if(ge == null) return;
254 if (!ge.thread.currentThreadAccess())
255 throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
256 //System.out.println("Add selection changed listener: " + listener);
257 if (ge.tree.isDisposed() || ge.selectionProvider == null) {
258 System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);
262 ge.selectionProvider.addSelectionChangedListener(listener);
267 public ISelection getSelection() {
268 if(ge == null) return StructuredSelection.EMPTY;
269 if (!ge.thread.currentThreadAccess())
270 throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());
271 if (ge.tree.isDisposed() || ge.selectionProvider == null)
272 return StructuredSelection.EMPTY;
273 return ge.selectionProvider.getSelection();
279 * If this explorer is running with an Eclipse workbench open, this
280 * Workbench UI context will be activated whenever inline editing is started
281 * through {@link #startEditing(TreeItem, int)} and deactivated when inline
284 * This context information can be used to for UI handler activity testing.
286 private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
288 private static final String KEY_DRAG_COLUMN = "dragColumn";
290 private static final boolean DEBUG_SELECTION_LISTENERS = false;
292 private static final int DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
294 public static final int DEFAULT_MAX_CHILDREN = 1000;
296 private final IThreadWorkQueue thread;
299 * Local method for checking from whether resources are loaded in
302 private final LocalResourceManager localResourceManager;
305 * Local device resource manager that is safe to use in
306 * {@link ImageLoaderJob} for creating images in a non-UI thread.
308 private final ResourceManager resourceManager;
311 * Package visibility.
312 * TODO: Get rid of these.
316 @SuppressWarnings({ "rawtypes" })
317 final HashMap<CacheKey<?>, NodeQueryProcessor> processors = new HashMap<>();
318 @SuppressWarnings({ "rawtypes" })
319 final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors = new HashMap<>();
320 @SuppressWarnings({ "rawtypes" })
321 final HashMap<Class, DataSource> dataSources = new HashMap<>();
323 class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
324 // This is for query debugging only.
327 GECache cache = new GECache();
328 AtomicBoolean propagating = new AtomicBoolean(false);
329 Object propagateList = new Object();
330 Object propagate = new Object();
331 List<Runnable> scheduleList = new ArrayList<Runnable>();
332 final Deque<Integer> activity = new LinkedList<Integer>();
336 * Stores the currently running query update runnable. If
337 * <code>null</code> there's nothing scheduled yet in which case
338 * scheduling can commence. Otherwise the update should be skipped.
340 AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<>();
343 * Keeps track of nodes that have already been auto-expanded. After
344 * being inserted into this set, nodes will not be forced to stay in an
345 * expanded state after that. This makes it possible for the user to
346 * close auto-expanded nodes.
348 Map<NodeContext, Boolean> autoExpanded = new WeakHashMap<>();
352 protected void doDispose() {
354 autoExpanded.clear();
358 public IGECache getCache() {
363 public int queryIndent() {
368 public int queryIndent(int offset) {
369 queryIndent += offset;
374 @SuppressWarnings("unchecked")
375 public <T> NodeQueryProcessor<T> getProcessor(Object o) {
376 return processors.get(o);
380 @SuppressWarnings("unchecked")
381 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(Object o) {
382 return primitiveProcessors.get(o);
385 @SuppressWarnings("unchecked")
387 public <T> DataSource<T> getDataSource(Class<T> clazz) {
388 return dataSources.get(clazz);
392 public void update(UIElementReference ref) {
393 //System.out.println("GE.update " + ref);
394 TreeItemReference tiref = (TreeItemReference) ref;
395 TreeItem item = tiref.getItem();
396 // NOTE: must be called regardless of the the item value.
397 // A null item is currently used to indicate a tree root update.
398 GraphExplorerImpl.this.update(item);
402 public Object getPropagateLock() {
407 public Object getPropagateListLock() {
408 return propagateList;
412 public boolean isPropagating() {
413 return propagating.get();
417 public void setPropagating(boolean b) {
418 this.propagating.set(b);
422 public List<Runnable> getScheduleList() {
427 public void setScheduleList(List<Runnable> list) {
428 this.scheduleList = list;
432 public Deque<Integer> getActivity() {
437 public void setActivityInt(int i) {
438 this.activityInt = i;
442 public int getActivityInt() {
447 public void scheduleQueryUpdate(Runnable r) {
448 if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
450 //System.out.println("Scheduling query update for runnable " + r);
451 if (currentQueryUpdater.compareAndSet(null, r)) {
452 //System.out.println("Scheduling query update for runnable " + r);
453 queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);
457 Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
460 Runnable r = currentQueryUpdater.getAndSet(null);
462 //System.out.println("Running query update runnable " + r);
469 GraphExplorerContext explorerContext = new GraphExplorerContext();
471 HashSet<TreeItem> pendingItems = new HashSet<>();
472 boolean updating = false;
473 boolean pendingRoot = false;
475 @SuppressWarnings("deprecation")
476 ModificationContext modificationContext = null;
478 NodeContext rootContext;
480 StatePersistor persistor = null;
482 boolean editable = true;
485 * This is a reverse mapping from {@link NodeContext} tree objects back to
486 * their owner TreeItems.
489 * Access this map only in the SWT thread to keep it thread-safe.
492 BijectionMap<NodeContext, TreeItem> contextToItem = new BijectionMap<>();
495 * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
498 Column[] columns = new Column[0];
499 Map<String, Integer> columnKeyToIndex = new HashMap<>();
500 boolean refreshingColumnSizes = false;
501 boolean columnsAreVisible = true;
504 * An array reused for invoking {@link TreeItem#setImage(Image[])} instead
505 * of constantly allocating new arrays for setting each TreeItems images.
506 * This works because {@link TreeItem#setImage(Image[])} does not take hold
507 * of the array itself, only the contents of the array.
509 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
511 Image[] columnImageArray = { null };
514 * Used for collecting Image or ImageDescriptor instances for a single
515 * TreeItem when initially setting images for a TreeItem.
517 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
519 Object[] columnDescOrImageArray = { null };
521 final ExecutorService queryUpdateScheduler = Threads.getExecutor();
522 final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor();
524 /** Set to true when the Tree widget is disposed. */
525 private boolean disposed = false;
526 private final CopyOnWriteArrayList<FocusListener> focusListeners = new CopyOnWriteArrayList<>();
527 private final CopyOnWriteArrayList<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();
528 private final CopyOnWriteArrayList<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
530 /** Selection provider */
531 private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
532 protected BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider();
533 protected SelectionDataResolver selectionDataResolver;
534 protected SelectionFilter selectionFilter;
535 protected BiFunction<GraphExplorer, Object[], Object[]> selectionTransformation = new BiFunction<GraphExplorer, Object[], Object[]>() {
538 public Object[] apply(GraphExplorer explorer, Object[] objects) {
539 Object[] result = new Object[objects.length];
540 for (int i = 0; i < objects.length; i++) {
541 IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
542 context.setHint(SelectionHints.KEY_MAIN, objects[i]);
549 protected FontDescriptor originalFont;
550 protected ColorDescriptor originalForeground;
551 protected ColorDescriptor originalBackground;
554 * The set of currently selected TreeItem instances. This set is needed
555 * because we need to know in {@link #setData(Event)} whether the updated
556 * item was a part of the current selection in which case the selection must
559 private final Map<TreeItem, NodeContext> selectedItems = new HashMap<>();
562 * TODO: specify what this is for
564 private final Set<NodeContext> selectionRefreshContexts = new HashSet<>();
567 * If this field is non-null, it means that if {@link #setData(Event)}
568 * encounters a NodeContext equal to this one, it must make the TreeItem
569 * assigned to that NodeContext the topmost item of the tree using
570 * {@link Tree#setTopItem(TreeItem)}. After this the field value is
574 * This is related to {@link #initializeState()}, i.e. explorer state
577 // private NodeContext[] topNodePath = NodeContext.NONE;
578 // private int[] topNodePath = {};
579 // private int currentTopNodePathIndex = -1;
582 * See {@link #setAutoExpandLevel(int)}
584 private int autoExpandLevel = 0;
587 * <code>null</code> if not explicitly set through
588 * {@link #setServiceLocator(IServiceLocator)}.
590 private IServiceLocator serviceLocator;
593 * The global workbench context service, if the workbench is available.
594 * Retrieved in the constructor.
596 private IContextService contextService = null;
599 * The global workbench IFocusService, if the workbench is available.
600 * Retrieved in the constructor.
602 private IFocusService focusService = null;
605 * A Workbench UI context activation that is activated when starting inline
606 * editing through {@link #startEditing(TreeItem, int)}.
608 * @see #activateEditingContext()
609 * @see #deactivateEditingContext()
611 private IContextActivation editingContext = null;
613 static class ImageTask {
616 Object[] descsOrImages;
617 public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
620 this.descsOrImages = descsOrImages;
625 * The job that is used for off-loading image loading tasks (see
626 * {@link ImageTask} to a worker thread from the main UI thread.
628 * @see #setPendingImages(IProgressMonitor)
630 ImageLoaderJob imageLoaderJob;
633 * The set of currently gathered up image loading tasks for
634 * {@link #imageLoaderJob} to execute.
636 * @see #setPendingImages(IProgressMonitor)
638 Map<TreeItem, ImageTask> imageTasks = new THashMap<>();
641 * A state flag indicating whether the vertical scroll bar was visible for
642 * {@link #tree} the last time it was checked. Since there is no listener
643 * that can provide this information, we check it in {@link #setData(Event)}
644 * every time any data for a TreeItem is updated. If the visibility changes,
645 * we will force re-layouting of the tree's parent composite.
647 * @see #setData(Event)
649 private boolean verticalBarVisible = false;
651 static class TransientStateImpl implements TransientExplorerState {
653 private Integer activeColumn = null;
656 public synchronized Integer getActiveColumn() {
660 public synchronized void setActiveColumn(Integer column) {
661 activeColumn = column;
666 private TransientStateImpl transientState = new TransientStateImpl();
668 boolean scheduleUpdater() {
670 if (tree.isDisposed())
673 if (pendingRoot == true || !pendingItems.isEmpty()) {
674 assert(!tree.isDisposed());
676 int activity = explorerContext.activityInt;
678 if (activity < 100) {
679 // System.out.println("Scheduling update immediately.");
680 } else if (activity < 1000) {
681 // System.out.println("Scheduling update after 500ms.");
684 // System.out.println("Scheduling update after 3000ms.");
690 //System.out.println("Scheduling UI update after " + delay + " ms.");
691 uiUpdateScheduler.schedule(new Runnable() {
695 if (tree.isDisposed())
698 if (updateCounter > 0) {
700 uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
702 tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this));
706 }, delay, TimeUnit.MILLISECONDS);
715 int updateCounter = 0;
717 void update(TreeItem item) {
719 synchronized(pendingItems) {
721 // System.out.println("update " + item);
725 if(item == null) pendingRoot = true;
726 else pendingItems.add(item);
728 if(updating == true) return;
736 private int maxChildren = DEFAULT_MAX_CHILDREN;
739 public int getMaxChildren() {
744 public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
745 Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
746 //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
747 if (result != null) {
749 throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
756 public void setMaxChildren(int maxChildren) {
757 this.maxChildren = maxChildren;
761 public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
762 this.modificationContext = modificationContext;
766 * @param parent the parent SWT composite
768 public GraphExplorerImpl(Composite parent) {
769 this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
773 * Stores the node context and the modifier that is currently being
774 * modified. These are used internally to prevent duplicate edits from being
775 * initiated which should always be a sensible thing to do.
777 private Set<NodeContext> currentlyModifiedNodes = new THashSet<>();
779 private final TreeEditor editor;
780 private Color invalidModificationColor = null;
783 * @param item the TreeItem to start editing
784 * @param columnIndex the index of the column to edit, starts counting from
786 * @return <code>true</code> if the editing was initiated successfully or
787 * <code>false</code> if editing could not be started due to lack of
788 * {@link Modifier} for the labeler in question.
790 private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
792 return "Rename not supported for selection";
794 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
795 final NodeContext context = (NodeContext) item.getData();
796 Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
798 return "Rename not supported for selection";
800 if(columnKey == null) columnKey = columns[columnIndex].getKey();
802 // columnKey might be prefixed with '#' to indicate
803 // textual editing is preferred. Try to get modifier
804 // for that first and only if it fails, try without
806 Modifier modifier = labeler.getModifier(modificationContext, columnKey);
807 if (modifier == null) {
808 if(columnKey.startsWith("#"))
809 modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
810 if (modifier == null)
811 return "Rename not supported for selection";
813 if (modifier instanceof DeniedModifier) {
814 DeniedModifier dm = (DeniedModifier)modifier;
815 return dm.getMessage();
818 // Prevent editing of a single node context multiple times.
819 if (currentlyModifiedNodes.contains(context)) {
820 //System.out.println("discarding duplicate edit for context " + context);
821 return "Rename not supported for selection";
824 // Clean up any previous editor control
825 Control oldEditor = editor.getEditor();
826 if (oldEditor != null)
829 if (modifier instanceof DialogModifier) {
830 performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
831 } else if (modifier instanceof CustomModifier) {
832 startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
833 } else if (modifier instanceof EnumerationModifier) {
834 startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
836 startTextEditing(item, columnIndex, context, modifier);
848 void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
849 final DialogModifier modifier) {
850 final AtomicBoolean disposed = new AtomicBoolean(false);
851 Consumer<String> callback = result -> {
854 String error = modifier.isValid(result);
856 modifier.modify(result);
857 // Item may be disposed if the tree gets reset after a previous editing.
858 if (!item.isDisposed()) {
859 item.setText(columnIndex, result);
860 queueSelectionRefresh(context);
865 currentlyModifiedNodes.add(context);
867 String status = modifier.query(tree, item, columnIndex, context, callback);
869 ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
871 currentlyModifiedNodes.remove(context);
876 private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
877 Point size = control.computeSize(widthHint, heightHint);
878 editor.horizontalAlignment = SWT.LEFT;
879 Rectangle itemRect = item.getBounds(columnIndex),
880 rect = tree.getClientArea();
881 editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
882 int left = itemRect.x,
883 right = rect.x + rect.width;
884 editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
885 editor.minimumHeight = size.y + insetY * 2;
889 void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
890 GC gc = new GC(control);
891 Point size = gc.textExtent(text);
893 reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
902 void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
903 final CustomModifier modifier) {
904 final Object obj = modifier.createControl(tree, item, columnIndex, context);
905 if (!(obj instanceof Control))
906 throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
907 final Control control = (Control) obj;
909 // final int insetX = 0;
910 // final int insetY = 0;
911 // control.addListener(SWT.Resize, new Listener() {
913 // public void handleEvent(Event e) {
914 // Rectangle rect = control.getBounds();
915 // control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
918 control.addListener(SWT.Dispose, new Listener() {
920 public void handleEvent(Event event) {
921 currentlyModifiedNodes.remove(context);
922 queueSelectionRefresh(context);
923 deactivateEditingContext();
927 if (!(control instanceof Shell)) {
928 editor.setEditor(control, item, columnIndex);
934 GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
936 activateEditingContext(control);
938 // Removed in disposeListener above
939 currentlyModifiedNodes.add(context);
940 //System.out.println("START CUSTOM EDITING: " + item);
949 void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
950 String initialText = modifier.getValue();
951 if (initialText == null)
952 throw new AssertionError("Labeler.Modifier.getValue() returned null");
954 List<String> values = modifier.getValues();
955 String selectedValue = modifier.getValue();
956 int selectedIndex = values.indexOf(selectedValue);
957 if (selectedIndex == -1)
958 throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
960 final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
961 combo.setVisibleItemCount(10);
962 //combo.setEditable(false);
964 for (String value : values) {
967 combo.select(selectedIndex);
969 Listener comboListener = new Listener() {
970 boolean arrowTraverseUsed = false;
972 public void handleEvent(final Event e) {
973 //System.out.println("FOO: " + e);
976 if (e.character == SWT.CR) {
977 // Commit edit directly on ENTER press.
978 String text = combo.getText();
979 modifier.modify(text);
980 // Item may be disposed if the tree gets reset after a previous editing.
981 if (!item.isDisposed()) {
982 item.setText(columnIndex, text);
983 queueSelectionRefresh(context);
987 } else if (e.keyCode == SWT.ESC) {
988 // Cancel editing immediately
995 if (arrowTraverseUsed) {
996 arrowTraverseUsed = false;
1000 String text = combo.getText();
1001 modifier.modify(text);
1003 // Item may be disposed if the tree gets reset after a previous editing.
1004 if (!item.isDisposed()) {
1005 item.setText(columnIndex, text);
1006 queueSelectionRefresh(context);
1011 case SWT.FocusOut: {
1012 String text = combo.getText();
1013 modifier.modify(text);
1015 // Item may be disposed if the tree gets reset after a previous editing.
1016 if (!item.isDisposed()) {
1017 item.setText(columnIndex, text);
1018 queueSelectionRefresh(context);
1023 case SWT.Traverse: {
1025 case SWT.TRAVERSE_RETURN:
1026 String text = combo.getText();
1027 modifier.modify(text);
1028 if (!item.isDisposed()) {
1029 item.setText(columnIndex, text);
1030 queueSelectionRefresh(context);
1032 arrowTraverseUsed = false;
1034 case SWT.TRAVERSE_ESCAPE:
1038 case SWT.TRAVERSE_ARROW_NEXT:
1039 case SWT.TRAVERSE_ARROW_PREVIOUS:
1040 arrowTraverseUsed = true;
1043 //System.out.println("unhandled traversal: " + e.detail);
1049 currentlyModifiedNodes.remove(context);
1050 deactivateEditingContext();
1055 combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
1056 combo.addListener(SWT.KeyDown, comboListener);
1057 combo.addListener(SWT.FocusOut, comboListener);
1058 combo.addListener(SWT.Traverse, comboListener);
1059 combo.addListener(SWT.Selection, comboListener);
1060 combo.addListener(SWT.Dispose, comboListener);
1062 editor.setEditor(combo, item, columnIndex);
1065 combo.setListVisible(true);
1067 GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
1069 activateEditingContext(combo);
1071 // Removed in comboListener
1072 currentlyModifiedNodes.add(context);
1074 //System.out.println("START ENUMERATION EDITING: " + item);
1079 * @param columnIndex
1083 void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {
1084 String initialText = modifier.getValue();
1085 if (initialText == null)
1086 throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);
1088 final Composite composite = new Composite(tree, SWT.NONE);
1089 //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));
1090 final Text text = new Text(composite, SWT.BORDER);
1091 final int insetX = 0;
1092 final int insetY = 0;
1093 composite.addListener(SWT.Resize, new Listener() {
1095 public void handleEvent(Event e) {
1096 Rectangle rect = composite.getClientArea();
1097 text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height
1101 final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
1102 Listener textListener = new Listener() {
1104 boolean modified = false;
1107 public void handleEvent(final Event e) {
1113 //System.out.println("FOCUS OUT " + item);
1114 newText = text.getText();
1115 error = modifier.isValid(newText);
1116 if (error == null) {
1117 modifier.modify(newText);
1119 // Item may be disposed if the tree gets reset after a previous editing.
1120 if (!item.isDisposed()) {
1121 item.setText(columnIndex, newText);
1122 queueSelectionRefresh(context);
1125 // System.out.println("validation error: " + error);
1128 composite.dispose();
1131 newText = text.getText();
1132 error = modifier.isValid(newText);
1133 if (error != null) {
1134 text.setBackground(invalidModificationColor);
1136 //System.out.println("validation error: " + error);
1138 text.setBackground(null);
1145 // Safety check since it seems that this may happen with
1147 if (item.isDisposed())
1150 // Filter input if necessary
1151 e.text = filter != null ? filter.filter(e.text) : e.text;
1153 newText = text.getText();
1154 String leftText = newText.substring(0, e.start);
1155 String rightText = newText.substring(e.end, newText.length());
1156 GraphExplorerImpl.this.reconfigureTreeEditorForText(
1157 item, columnIndex, text, leftText + e.text + rightText,
1158 SWT.DEFAULT, insetX, insetY);
1162 case SWT.TRAVERSE_RETURN:
1164 newText = text.getText();
1165 error = modifier.isValid(newText);
1166 if (error == null) {
1167 modifier.modify(newText);
1168 if (!item.isDisposed()) {
1169 item.setText(columnIndex, newText);
1170 queueSelectionRefresh(context);
1175 case SWT.TRAVERSE_ESCAPE:
1176 composite.dispose();
1180 //System.out.println("unhandled traversal: " + e.detail);
1186 currentlyModifiedNodes.remove(context);
1187 deactivateEditingContext();
1193 // Set the initial text before registering a listener. We do not want immediate modification!
1194 text.setText(initialText);
1195 text.addListener(SWT.FocusOut, textListener);
1196 text.addListener(SWT.Traverse, textListener);
1197 text.addListener(SWT.Verify, textListener);
1198 text.addListener(SWT.Modify, textListener);
1199 text.addListener(SWT.Dispose, textListener);
1200 editor.setEditor(composite, item, columnIndex);
1204 // Initialize TreeEditor properly.
1205 GraphExplorerImpl.this.reconfigureTreeEditorForText(
1206 item, columnIndex, text, initialText,
1207 SWT.DEFAULT, insetX, insetY);
1209 // Removed in textListener
1210 currentlyModifiedNodes.add(context);
1212 activateEditingContext(text);
1214 //System.out.println("START TEXT EDITING: " + item);
1217 protected void errorStatus(String error) {
1218 IStatusLineManager status = getStatusLineManager();
1219 if (status != null) {
1220 status.setErrorMessage(error);
1224 protected IStatusLineManager getStatusLineManager() {
1225 if (serviceLocator instanceof IWorkbenchPart) {
1226 return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);
1227 } else if (serviceLocator instanceof IWorkbenchSite) {
1228 return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);
1233 protected void activateEditingContext(Control control) {
1234 if (contextService != null) {
1235 editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
1237 if (control != null && focusService != null) {
1238 focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);
1239 // No need to remove the control, it will be
1240 // removed automatically when it is disposed.
1244 protected void deactivateEditingContext() {
1245 IContextActivation a = editingContext;
1247 editingContext = null;
1248 contextService.deactivateContext(a);
1255 void queueSelectionRefresh(NodeContext forContext) {
1256 selectionRefreshContexts.add(forContext);
1260 public String startEditing(NodeContext context, String columnKey_) {
1261 assertNotDisposed();
1262 if (!thread.currentThreadAccess())
1263 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
1265 String columnKey = columnKey_;
1266 if(columnKey.startsWith("#")) {
1267 columnKey = columnKey.substring(1);
1270 Integer columnIndex = columnKeyToIndex.get(columnKey);
1271 if (columnIndex == null)
1272 return "Rename not supported for selection";
1274 TreeItem item = contextToItem.getRight(context);
1276 return "Rename not supported for selection";
1278 return startEditing(item, columnIndex, columnKey_);
1283 public String startEditing(String columnKey) {
1285 ISelection selection = postSelectionProvider.getSelection();
1286 if(selection == null) return "Rename not supported for selection";
1287 NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
1288 if(context == null) return "Rename not supported for selection";
1290 return startEditing(context, columnKey);
1295 * @param site <code>null</code> if the explorer is detached from the workbench
1296 * @param parent the parent SWT composite
1297 * @param style the tree style to use, check the see tags for the available flags
1302 * @see SWT#FULL_SELECTION
1303 * @see SWT#NO_SCROLL
1307 public GraphExplorerImpl(Composite parent, int style) {
1309 setServiceLocator(null);
1311 this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
1312 this.resourceManager = new DeviceResourceManager(parent.getDisplay());
1314 this.imageLoaderJob = new ImageLoaderJob(this);
1315 this.imageLoaderJob.setPriority(Job.DECORATE);
1317 invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
1319 this.thread = SWTThread.getThreadAccess(parent);
1321 for(int i=0;i<10;i++) explorerContext.activity.push(0);
1323 tree = new Tree(parent, style);
1324 tree.addListener(SWT.SetData, this);
1325 tree.addListener(SWT.Expand, this);
1326 tree.addListener(SWT.Dispose, this);
1327 tree.addListener(SWT.Activate, this);
1329 tree.setData(KEY_GRAPH_EXPLORER, this);
1331 // These are both required for performing column resizing without flicker.
1332 // See SWT.Resize event handling in #handleEvent() for more explanations.
1333 parent.addListener(SWT.Resize, this);
1334 tree.addListener(SWT.Resize, this);
1336 originalFont = JFaceResources.getDefaultFontDescriptor();
1337 // originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
1338 // originalForeground = tree.getForeground();
1340 tree.setFont((Font) localResourceManager.get(originalFont));
1342 columns = new Column[] { new Column(ColumnKeys.SINGLE) };
1343 columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
1345 editor = new TreeEditor(tree);
1346 editor.horizontalAlignment = SWT.LEFT;
1347 editor.grabHorizontal = true;
1348 editor.minimumWidth = 50;
1350 setBasicListeners();
1351 setDefaultProcessors();
1353 this.toolTip = new GraphExplorerToolTip(explorerContext, tree);
1357 public IThreadWorkQueue getThread() {
1361 TreeItem previousSingleSelection = null;
1362 long focusGainedAt = Long.MIN_VALUE;
1364 protected GraphExplorerToolTip toolTip;
1366 protected void setBasicListeners() {
1367 // Keep track of the previous single selection to help
1368 // decide whether to start editing a tree node on mouse
1370 tree.addListener(SWT.Selection, event -> {
1371 TreeItem[] selection = tree.getSelection();
1372 if (selection.length == 1) {
1373 //for (TreeItem item : selection)
1374 // System.out.println("selection: " + item);
1375 previousSingleSelection = selection[0];
1377 previousSingleSelection = null;
1381 // Try to start editing of tree column when clicked for the second time.
1382 Listener mouseEditListener = new Listener() {
1384 Future<?> startEdit = null;
1387 public void handleEvent(Event event) {
1388 if (event.type == SWT.DragDetect) {
1389 // Needed to prevent editing from being started when in fact
1390 // the user starts dragging an item.
1391 //System.out.println("DRAG DETECT: " + event);
1395 //System.out.println("mouse down: " + event);
1396 if (event.button == 1) {
1397 // Always ignore the first mouse button press that focuses
1398 // the control. Do not let it start in-line editing since
1399 // that is very annoying to users and not how the UI's that
1400 // people are used to behave.
1401 long eventTime = ((long) event.time) & 0xFFFFFFFFL;
1402 if ((eventTime - focusGainedAt) < 250L) {
1403 //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));
1406 //System.out.println("testing whether to start editing");
1408 final Point point = new Point(event.x, event.y);
1409 final TreeItem item = tree.getItem(point);
1412 //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
1414 // Only start editing if the item was already selected.
1415 if (!item.equals(previousSingleSelection)) {
1420 if (tree.getColumnCount() > 1) {
1421 // TODO: reconsider this logic, might not be good in general.
1422 for (int i = 0; i < tree.getColumnCount(); i++) {
1423 if (item.getBounds(i).contains(point)) {
1424 tryScheduleEdit(event, item, point, 100, i);
1429 //System.out.println("clicks: " + event.count);
1430 if (item.getBounds().contains(point)) {
1431 if (event.count == 1) {
1432 tryScheduleEdit(event, item, point, 500, 0);
1441 void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {
1442 //System.out.println("\tCONTAINS: " + item);
1446 //System.out.println("\tScheduling edit: " + item);
1447 startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
1450 ThreadUtils.asyncExec(thread, new Runnable() {
1453 if (item.isDisposed())
1455 startEditing(item, column, null);
1459 }, delayMs, TimeUnit.MILLISECONDS);
1462 boolean cancelEdit() {
1463 Future<?> f = startEdit;
1465 // Try to cancel the start edit task if it's not running yet.
1468 boolean ret = f.cancel(false);
1469 //System.out.println("\tCancelled edit: " + ret);
1473 //System.out.println("\tNo edit in progress to cancel");
1477 tree.addListener(SWT.MouseDown, mouseEditListener);
1478 tree.addListener(SWT.DragDetect, mouseEditListener);
1479 tree.addListener(SWT.DragDetect, event -> {
1480 Point test = new Point(event.x, event.y);
1481 TreeItem item = tree.getItem(test);
1483 for(int i=0;i<tree.getColumnCount();i++) {
1484 Rectangle rect = item.getBounds(i);
1485 if(rect.contains(test)) {
1486 tree.setData(KEY_DRAG_COLUMN, i);
1491 tree.setData(KEY_DRAG_COLUMN, -1);
1493 tree.addListener(SWT.MouseMove, event -> {
1494 Point test = new Point(event.x, event.y);
1495 TreeItem item = tree.getItem(test);
1497 for(int i=0;i<tree.getColumnCount();i++) {
1498 Rectangle rect = item.getBounds(i);
1499 if(rect.contains(test)) {
1500 transientState.setActiveColumn(i);
1505 transientState.setActiveColumn(null);
1508 // Add focus/mouse/key listeners for supporting the respective
1509 // add/remove listener methods in IGraphExplorer.
1510 tree.addFocusListener(new FocusListener() {
1512 public void focusGained(FocusEvent e) {
1513 focusGainedAt = ((long) e.time) & 0xFFFFFFFFL;
1514 for (FocusListener listener : focusListeners)
1515 listener.focusGained(e);
1518 public void focusLost(FocusEvent e) {
1519 for (FocusListener listener : focusListeners)
1520 listener.focusLost(e);
1523 tree.addMouseListener(new MouseListener() {
1525 public void mouseDoubleClick(MouseEvent e) {
1526 for (MouseListener listener : mouseListeners) {
1527 listener.mouseDoubleClick(e);
1531 public void mouseDown(MouseEvent e) {
1532 for (MouseListener listener : mouseListeners) {
1533 listener.mouseDown(e);
1537 public void mouseUp(MouseEvent e) {
1538 for (MouseListener listener : mouseListeners) {
1539 listener.mouseUp(e);
1543 tree.addKeyListener(new KeyListener() {
1545 public void keyPressed(KeyEvent e) {
1546 for (KeyListener listener : keyListeners) {
1547 listener.keyPressed(e);
1551 public void keyReleased(KeyEvent e) {
1552 for (KeyListener listener : keyListeners) {
1553 listener.keyReleased(e);
1558 OpenStrategy os = new OpenStrategy(tree);
1559 os.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
1560 ISelection s = resetSelectionFromWidget();
1562 LOGGER.trace("Fire selection change: {}", s);
1563 selectionProvider.fireSelection(s);
1566 os.addPostSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
1567 LOGGER.trace("Fire post-selection change: {}", selectionProvider.getSelection());
1568 resetSelectionFromWidgetAndFirePostSelection(true);
1571 // This listener takes care of updating the set of currently selected
1572 // TreeItem instances. This set is needed because we need to know in
1573 // #setData(Event) whether the updated item was a part of the current
1574 // selection in which case the selection must be updated.
1575 selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
1577 public void selectionChanged(SelectionChangedEvent event) {
1578 //System.out.println("selection changed: " + event.getSelection());
1579 Set<NodeContext> set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);
1580 selectedItems.clear();
1581 for (NodeContext nc : set) {
1582 TreeItem item = contextToItem.getRight(nc);
1584 selectedItems.put(item, nc);
1586 //System.out.println("newly selected items: " + selectedItems);
1592 * @return the new selection if it was different from the old selection in
1593 * {@link #selectionProvider}
1595 private ISelection resetSelectionFromWidget() {
1596 ISelection widgetSelection = getWidgetSelection();
1597 // System.out.println("resetSelection()");
1598 // System.out.println(" provider selection: " + selectionProvider.getSelection());
1599 // System.out.println(" widget selection: " + widgetSelection);
1600 boolean equals = selectionProvider.selectionEquals(widgetSelection);
1601 selectionProvider.setSelectionWithoutFiring(widgetSelection);
1602 return equals ? null : widgetSelection;
1606 * @return the new selection if it was different from the old selection in
1607 * {@link #selectionProvider}
1609 private boolean resetSelectionFromWidgetAndFirePostSelection(boolean force) {
1610 ISelection s = resetSelectionFromWidget();
1611 boolean fire = s != null || force;
1613 //System.out.println("FIRING POST-SELECTION: " + selectionProvider.getSelection());
1614 selectionProvider.firePostSelection(selectionProvider.getSelection());
1619 protected void setDefaultProcessors() {
1620 // Add a simple IMAGER query processor that always returns null.
1621 // With this processor no images will ever be shown.
1622 // setPrimitiveProcessor(new StaticImagerProcessor(null));
1624 setProcessor(new DefaultComparableChildrenProcessor());
1625 setProcessor(new DefaultLabelDecoratorsProcessor());
1626 setProcessor(new DefaultImageDecoratorsProcessor());
1627 setProcessor(new DefaultSelectedLabelerProcessor());
1628 setProcessor(new DefaultLabelerFactoriesProcessor());
1629 setProcessor(new DefaultSelectedImagerProcessor());
1630 setProcessor(new DefaultImagerFactoriesProcessor());
1631 setPrimitiveProcessor(new DefaultLabelerProcessor());
1632 setPrimitiveProcessor(new DefaultCheckedStateProcessor());
1633 setPrimitiveProcessor(new DefaultImagerProcessor());
1634 setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());
1635 setPrimitiveProcessor(new DefaultImageDecoratorProcessor());
1636 setPrimitiveProcessor(new NoSelectionRequestProcessor());
1638 setProcessor(new DefaultFinalChildrenProcessor(this));
1640 setProcessor(new DefaultPrunedChildrenProcessor());
1641 setProcessor(new DefaultSelectedViewpointProcessor());
1642 setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());
1643 setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());
1644 setProcessor(new DefaultViewpointContributionsProcessor());
1646 setPrimitiveProcessor(new DefaultViewpointProcessor());
1647 setPrimitiveProcessor(new DefaultViewpointContributionProcessor());
1648 setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());
1649 setPrimitiveProcessor(new DefaultIsExpandedProcessor());
1650 setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());
1654 public <T> void setProcessor(NodeQueryProcessor<T> processor) {
1655 assertNotDisposed();
1656 if (processor == null)
1657 throw new IllegalArgumentException("null processor");
1659 processors.put(processor.getIdentifier(), processor);
1663 public <T> void setPrimitiveProcessor(PrimitiveQueryProcessor<T> processor) {
1664 assertNotDisposed();
1665 if (processor == null)
1666 throw new IllegalArgumentException("null processor");
1668 PrimitiveQueryProcessor<?> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
1670 if (oldProcessor instanceof ProcessorLifecycle)
1671 ((ProcessorLifecycle) oldProcessor).detached(this);
1672 if (processor instanceof ProcessorLifecycle)
1673 ((ProcessorLifecycle) processor).attached(this);
1677 public <T> void setDataSource(DataSource<T> provider) {
1678 assertNotDisposed();
1679 if (provider == null)
1680 throw new IllegalArgumentException("null provider");
1681 dataSources.put(provider.getProvidedClass(), provider);
1684 @SuppressWarnings("unchecked")
1686 public <T> DataSource<T> removeDataSource(Class<T> forProvidedClass) {
1687 assertNotDisposed();
1688 if (forProvidedClass == null)
1689 throw new IllegalArgumentException("null class");
1690 return dataSources.remove(forProvidedClass);
1694 public void setPersistor(StatePersistor persistor) {
1695 this.persistor = persistor;
1699 public SelectionDataResolver getSelectionDataResolver() {
1700 return selectionDataResolver;
1704 public void setSelectionDataResolver(SelectionDataResolver r) {
1705 this.selectionDataResolver = r;
1709 public SelectionFilter getSelectionFilter() {
1710 return selectionFilter;
1714 public void setSelectionFilter(SelectionFilter f) {
1715 this.selectionFilter = f;
1716 // TODO: re-filter current selection?
1720 public void setSelectionTransformation(BiFunction<GraphExplorer, Object[], Object[]> f) {
1721 this.selectionTransformation = f;
1725 public <T> void addListener(T listener) {
1726 if(listener instanceof FocusListener) {
1727 focusListeners.add((FocusListener)listener);
1728 } else if(listener instanceof MouseListener) {
1729 mouseListeners.add((MouseListener)listener);
1730 } else if(listener instanceof KeyListener) {
1731 keyListeners.add((KeyListener)listener);
1736 public <T> void removeListener(T listener) {
1737 if(listener instanceof FocusListener) {
1738 focusListeners.remove(listener);
1739 } else if(listener instanceof MouseListener) {
1740 mouseListeners.remove(listener);
1741 } else if(listener instanceof KeyListener) {
1742 keyListeners.remove(listener);
1746 public void addSelectionListener(SelectionListener listener) {
1747 tree.addSelectionListener(listener);
1750 public void removeSelectionListener(SelectionListener listener) {
1751 tree.removeSelectionListener(listener);
1754 private Set<String> uiContexts;
1757 public void setUIContexts(Set<String> contexts) {
1758 this.uiContexts = contexts;
1762 public void setRoot(final Object root) {
1763 if(uiContexts != null && uiContexts.size() == 1)
1764 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
1766 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
1770 public void setRootContext(final NodeContext context) {
1771 setRootContext0(context);
1774 private void setRootContext0(final NodeContext context) {
1775 Assert.isNotNull(context, "root must not be null");
1776 if (isDisposed() || tree.isDisposed())
1778 Display display = tree.getDisplay();
1779 if (display.getThread() == Thread.currentThread()) {
1782 display.asyncExec(new Runnable() {
1791 private void initializeState() {
1792 if (persistor == null)
1794 ExplorerStates.scheduleRead(getRoot(), persistor)
1795 .thenAccept(state -> SWTUtils.asyncExec(tree, () -> restoreState(state)));
1798 private void restoreState(ExplorerState state) {
1799 // topNodeToSet will be processed by #setData when it encounters a
1800 // NodeContext that matches this one.
1801 // topNodePath = state.topNodePath;
1802 // topNodePathChildIndex = state.topNodePathChildIndex;
1803 // currentTopNodePathIndex = 0;
1805 Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1806 if (processor instanceof DefaultIsExpandedProcessor) {
1807 DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
1808 for(NodeContext expanded : state.expandedNodes) {
1809 isExpandedProcessor.replaceExpanded(expanded, true);
1814 private void saveState() {
1815 if (persistor == null)
1818 NodeContext[] topNodePath = NodeContext.NONE;
1819 int[] topNodePathChildIndex = {};
1820 Collection<NodeContext> expandedNodes = Collections.emptyList();
1821 Map<String, Integer> columnWidths = Collections.<String, Integer> emptyMap();
1823 // Resolve top node path
1824 TreeItem topItem = tree.getTopItem();
1825 if (topItem != null) {
1826 NodeContext topNode = (NodeContext) topItem.getData();
1827 if (topNode != null) {
1828 topNodePath = getNodeContextPathSegments(topNode);
1829 topNodePathChildIndex = new int[topNodePath.length];
1830 for (int i = 0; i < topNodePath.length; ++i) {
1831 // TODO: get child indexes
1832 topNodePathChildIndex[i] = 0;
1837 // Resolve expanded nodes
1838 Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1839 if (processor instanceof IsExpandedProcessor) {
1840 IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;
1841 expandedNodes = isExpandedProcessor.getExpanded();
1845 TreeColumn[] columns = tree.getColumns();
1846 if (columns.length > 1) {
1847 columnWidths = new HashMap<String, Integer>();
1848 for (int i = 0; i < columns.length; ++i) {
1849 columnWidths.put(columns[i].getText(), columns[i].getWidth());
1853 persistor.serialize(
1854 ExplorerStates.explorerStateLocation(),
1856 new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
1860 * Invoke only from SWT thread to reset the root of the graph explorer tree.
1864 private void doSetRoot(NodeContext root) {
1865 if (tree.isDisposed())
1867 if (root.getConstant(BuiltinKeys.INPUT) == null) {
1868 ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));
1872 // Empty caches, release queries.
1873 GraphExplorerContext oldContext = explorerContext;
1874 GraphExplorerContext newContext = new GraphExplorerContext();
1875 GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));
1876 this.explorerContext = newContext;
1877 oldContext.safeDispose();
1878 toolTip.setGraphExplorerContext(explorerContext);
1880 // Need to empty these or otherwise they won't be emptied until the
1881 // explorer is disposed which would mean that many unwanted references
1882 // will be held by this map.
1883 clearPrimitiveProcessors();
1885 this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
1886 : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
1888 explorerContext.getCache().incRef(this.rootContext);
1892 NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
1894 tree.setItemCount(contexts.length);
1896 select(rootContext);
1897 refreshColumnSizes();
1901 public NodeContext getRoot() {
1906 public NodeContext getParentContext(NodeContext context) {
1908 throw new IllegalStateException("disposed");
1909 if (!thread.currentThreadAccess())
1910 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
1912 TreeItem item = contextToItem.getRight(context);
1913 if(item == null) return null;
1914 TreeItem parentItem = item.getParentItem();
1915 if(parentItem == null) return null;
1916 return (NodeContext)parentItem.getData();
1919 Point previousTreeSize;
1920 Point previousTreeParentSize;
1921 boolean activatedBefore = false;
1924 public void handleEvent(Event event) {
1925 //System.out.println("EVENT: " + event);
1926 switch(event.type) {
1928 //System.out.println("EXPAND: " + event.item);
1929 if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
1930 expandVirtual(event);
1932 System.out.println("TODO: non-virtual tree item expand");
1936 // Only invoked for SWT.VIRTUAL trees
1938 // Happened for Hannu once during program startup.
1939 // java.lang.AssertionError
1940 // at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)
1941 // at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)
1942 // at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)
1943 // I do not know whether SWT guarantees that SetData events
1944 // don't come after Dispose event has been issued, but I
1945 // think its better to have this check here just incase.
1950 // This ensures that column sizes are refreshed at
1951 // least once when the GE is first shown.
1952 if (!activatedBefore) {
1953 refreshColumnSizes();
1954 activatedBefore = true;
1958 //new Exception().printStackTrace();
1965 if (event.widget == tree) {
1966 // This case is meant for listening to tree width increase.
1967 // The column resizing must be performed only after the tree
1968 // itself as been resized.
1969 Point size = tree.getSize();
1971 if (previousTreeSize != null) {
1972 dx = size.x - previousTreeSize.x;
1974 previousTreeSize = size;
1975 //System.out.println("RESIZE: " + dx + " - size=" + size);
1978 tree.setRedraw(false);
1979 refreshColumnSizes(size);
1980 tree.setRedraw(true);
1982 } else if (event.widget == tree.getParent()) {
1983 // This case is meant for listening to tree width decrease.
1984 // The columns must be resized before the tree widget itself
1985 // is resized to prevent scroll bar flicker. This can be achieved
1986 // by listening to the resize events of the tree parent widget.
1987 Composite parent = tree.getParent();
1988 Point size = parent.getSize();
1990 // We must subtract the parent's border and possible
1991 // scroll bar width from the new target width of the columns.
1992 size.x -= tree.getParent().getBorderWidth() * 2;
1993 ScrollBar vBar = parent.getVerticalBar();
1994 if (vBar != null && vBar.isVisible())
1995 size.x -= vBar.getSize().x;
1998 if (previousTreeParentSize != null) {
1999 dx = size.x - previousTreeParentSize.x;
2001 previousTreeParentSize = size;
2002 //System.out.println("RESIZE: " + dx + " - size=" + size);
2005 tree.setRedraw(false);
2006 refreshColumnSizes(size);
2007 tree.setRedraw(true);
2017 protected void refreshColumnSizes() {
2018 // Composite treeParent = tree.getParent();
2019 // Point size = treeParent.getSize();
2020 // size.x -= treeParent.getBorderWidth() * 2;
2021 Point size = tree.getSize();
2022 refreshColumnSizes(size);
2023 tree.getParent().layout();
2027 * This has been disabled since the logic of handling column widths has been
2028 * externalized to parties creating {@link GraphExplorerImpl} instances.
2030 protected void refreshColumnSizes(Point toSize) {
2032 refreshingColumnSizes = true;
2034 int columnCount = tree.getColumnCount();
2035 if (columnCount > 0) {
2036 Point size = toSize;
2037 int targetWidth = size.x - tree.getBorderWidth() * 2;
2040 // Take the vertical scroll bar existence into to account when
2041 // calculating the overflow column width.
2042 ScrollBar vBar = tree.getVerticalBar();
2043 //if (vBar != null && vBar.isVisible())
2045 targetWidth -= vBar.getSize().x;
2047 List<TreeColumn> resizing = new ArrayList<TreeColumn>();
2049 int resizingWidth = 0;
2050 int totalWeight = 0;
2051 for (int i = 0; i < columnCount - 1; ++i) {
2052 TreeColumn col = tree.getColumn(i);
2053 //System.out.println(" " + col.getText() + ": " + col.getWidth());
2054 int width = col.getWidth();
2056 Column c = (Column) col.getData();
2059 resizingWidth += width;
2060 totalWeight += c.getWeight();
2064 int requiredWidthAdjustment = targetWidth - usedWidth;
2065 if (requiredWidthAdjustment < 0)
2066 requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());
2067 double diff = requiredWidthAdjustment;
2068 //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);
2070 // Decide how much to give space to / take space from each grabbing column
2071 double wrel = 1.0 / resizing.size();
2073 double[] weightedShares = new double[resizing.size()];
2074 for (int i = 0; i < resizing.size(); ++i) {
2075 TreeColumn col = resizing.get(i);
2076 Column c = (Column) col.getData();
2077 if (totalWeight == 0) {
2078 weightedShares[i] = wrel;
2080 weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
2083 //System.out.println("grabbing columns:" + resizing);
2084 //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
2086 // Always shrink the columns if necessary, but don't enlarge before
2087 // there is sufficient space to at least give all resizable columns
2089 if (diff < 0 || (diff > 0 && diff > resizing.size())) {
2090 // Need to either shrink or enlarge the resizable columns if possible.
2091 for (int i = 0; i < resizing.size(); ++i) {
2092 TreeColumn col = resizing.get(i);
2093 Column c = (Column) col.getData();
2094 int cw = col.getWidth();
2095 //double wrel = (double) cw / (double) resizingWidth;
2096 //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);
2097 double ddelta = weightedShares[i] * diff;
2100 delta = (int) Math.floor(ddelta);
2102 delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
2104 //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);
2105 //System.out.println("argh(" + col.getText() + "): " + c.getWidth() + " vs. " + col.getWidth() + " vs. " + (cw+delta));
2106 int newWidth = Math.max(c.getWidth(), cw + delta);
2107 requiredWidthAdjustment -= (newWidth - cw);
2108 col.setWidth(newWidth);
2112 //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
2114 TreeColumn last = tree.getColumn(columnCount - 1);
2115 // HACK: see #setColumns for why this is here.
2116 if (FILLER.equals(last.getText())) {
2117 last.setWidth(Math.max(0, requiredWidthAdjustment));
2121 refreshingColumnSizes = false;
2126 private void doDispose() {
2127 explorerContext.dispose();
2129 // No longer necessary, the used executors are shared.
2130 //scheduler.shutdown();
2131 //scheduler2.shutdown();
2134 detachPrimitiveProcessors();
2135 primitiveProcessors.clear();
2136 dataSources.clear();
2138 pendingItems.clear();
2142 contextToItem.clear();
2144 mouseListeners.clear();
2146 selectionProvider.clearListeners();
2147 selectionProvider = null;
2148 selectionDataResolver = null;
2149 selectionRefreshContexts.clear();
2150 selectedItems.clear();
2151 originalFont = null;
2153 localResourceManager.dispose();
2155 // Must shutdown image loader job before disposing its ResourceManager
2156 imageLoaderJob.dispose();
2157 imageLoaderJob.cancel();
2159 imageLoaderJob.join();
2160 } catch (InterruptedException e) {
2161 ErrorLogger.defaultLogError(e);
2163 resourceManager.dispose();
2165 postSelectionProvider.dispose();
2169 private void expandVirtual(final Event event) {
2170 TreeItem item = (TreeItem) event.item;
2171 assert (item != null);
2172 NodeContext context = (NodeContext) item.getData();
2173 assert (context != null);
2175 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));
2176 NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);
2177 int maxChildren = getMaxChildren(manager, context);
2178 item.setItemCount(children.length < maxChildren ? children.length : maxChildren);
2181 private NodeContext getNodeContext(TreeItem item) {
2182 assert(item != null);
2184 NodeContext context = (NodeContext)item.getData();
2185 assert(context != null);
2190 private NodeContext getParentContext(TreeItem item) {
2191 TreeItem parentItem = item.getParentItem();
2192 if(parentItem != null) {
2193 return getNodeContext(parentItem);
2199 private static final String LISTENER_SET_INDICATOR = "LSI";
2200 private static final String PENDING = "PENDING";
2201 private int contextSelectionChangeModCount = 0;
2204 * Only invoked for SWT.VIRTUAL widgets.
2208 private void setData(final Event event) {
2209 assert (event != null);
2210 TreeItem item = (TreeItem) event.item;
2211 assert (item != null);
2213 // Based on experience it seems to be possible that
2214 // SetData events are sent for disposed TreeItems.
2215 if (item.isDisposed() || item.getData(PENDING) != null)
2218 //System.out.println("GE.SetData " + item);
2220 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
2222 NodeContext parentContext = getParentContext(item);
2223 assert (parentContext != null);
2225 NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2227 // Some children have disappeared since counting
2228 if (event.index < 0) {
2229 ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());
2232 if (event.index >= parentChildren.length)
2235 NodeContext context = parentChildren[event.index];
2236 assert (context != null);
2237 item.setData(context);
2239 // Manage NodeContext -> TreeItem mappings
2240 contextToItem.map(context, item);
2241 if (item.getData(LISTENER_SET_INDICATOR) == null) {
2242 // This "if" exists because setData will get called many
2243 // times for the same (NodeContext, TreeItem) pairs.
2244 // Each TreeItem only needs one listener, but this
2245 // is needed to tell whether it already has a listener
2247 item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
2248 item.addListener(SWT.Dispose, itemDisposeListener);
2251 boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
2253 PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);
2254 int maxChildren = getMaxChildren(manager, context);
2255 //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);
2257 NodeContext[] pruned = children.getPrunedChildren();
2258 int count = Math.min(pruned.length, maxChildren);
2260 if (isExpanded || item.getItemCount() > 1) {
2261 item.setItemCount(count);
2262 TreeItem[] childItems = item.getItems();
2263 for(int i=0;i<count;i++)
2264 contextToItem.map(pruned[i], childItems[i]);
2266 if (children.getPrunedChildren().length == 0) {
2267 item.setItemCount(0);
2269 // item.setItemCount(1);
2270 item.setItemCount(count);
2271 TreeItem[] childItems = item.getItems();
2272 for(int i=0;i<count;i++)
2273 contextToItem.map(pruned[i], childItems[i]);
2274 // item.getItem(0).setData(PENDING, PENDING);
2275 // item.getItem(0).setItemCount(o);
2279 setTextAndImage(item, manager, context, event.index);
2281 // Check if the node should be auto-expanded?
2282 if ((autoExpandLevel == ALL_LEVELS || autoExpandLevel > 1) && !isExpanded) {
2283 //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");
2284 int level = getTreeItemLevel(item);
2285 if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)
2286 && !explorerContext.autoExpanded.containsKey(context))
2288 //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
2289 explorerContext.autoExpanded.put(context, Boolean.TRUE);
2290 setExpanded(context, true);
2294 item.setExpanded(isExpanded);
2296 if ((tree.getStyle() & SWT.CHECK) != 0) {
2297 CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);
2298 item.setChecked(CheckedState.CHECKED_STATES.contains(checked));
2299 item.setGrayed(CheckedState.GRAYED == checked);
2302 //System.out.println("GE.SetData completed " + item);
2304 // This test makes sure that selectionProvider holds the correct
2305 // selection with respect to the actual selection stored by the virtual
2307 // The data items shown below the items occupied by the selected and now removed data
2308 // will be squeezed to use the tree items previously used for the now
2309 // removed data. When this happens, the NodeContext items stored by the
2310 // tree items will be different from what the GraphExplorer's
2311 // ISelectionProvider thinks the selection currently is. To compensate,
2312 // 1. Recognize the situation
2313 // 2. ASAP set the selection provider selection to what is actually
2314 // offered by the tree widget.
2315 NodeContext selectedContext = selectedItems.get(item);
2316 // System.out.println("selectedContext(" + item + "): " + selectedContext);
2317 if (selectedContext != null && !selectedContext.equals(context)) {
2318 final int modCount = ++contextSelectionChangeModCount;
2319 // System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);
2320 // System.out.println(" old context: " + selectedContext);
2321 // System.out.println(" new context: " + context);
2322 // System.out.println(" provider selection: " + selectionProvider.getSelection());
2323 // System.out.println(" widget selection: " + getWidgetSelection());
2324 ThreadUtils.asyncExec(thread, new Runnable() {
2329 int count = contextSelectionChangeModCount;
2330 // System.out.println("MODCOUNT: " + modCount + " vs. " + count);
2331 if (modCount != count)
2333 resetSelectionFromWidgetAndFirePostSelection(false);
2338 // This must be done to keep the visible tree selection properly
2339 // in sync with the selectionProvider JFace proxy of this class in
2340 // cases where an in-line editor was previously active for the node
2342 if (selectionRefreshContexts.remove(context)) {
2343 final ISelection currentSelection = selectionProvider.getSelection();
2344 // asyncExec is here to prevent ui glitches that
2345 // seem to occur if the selection setting is done
2346 // directly here in between setData invocations.
2347 ThreadUtils.asyncExec(thread, new Runnable() {
2352 // System.out.println("REFRESHING SELECTION: " + currentSelection);
2353 // System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));
2354 // System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());
2355 setSelection(currentSelection, true);
2356 // System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));
2357 // System.out.println("AFTER setSelection: " + selectionProvider.getSelection());
2362 // TODO: doesn't work if any part of the node path that should be
2363 // revealed is out of view.
2364 // Disabled until a better solution is devised.
2365 // Suggestion: include item indexes into the stored node context path
2366 // to make it possible for this method to know whether the current
2367 // node path segment is currently out of view based on event.index.
2368 // If out of view, this code needs to scroll the view programmatically
2370 // if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {
2371 // NodeContext topNode = topNodePath[currentTopNodePathIndex];
2372 // if (topNode.equals(context)) {
2373 // final TreeItem topItem = item;
2374 // ++currentTopNodePathIndex;
2375 // if (currentTopNodePathIndex >= topNodePath.length) {
2376 // // Mission accomplished. End search for top node here.
2377 // topNodePath = NodeContext.NONE;
2378 // currentTopNodePathIndex = -1;
2380 // ThreadUtils.asyncExec(thread, new Runnable() {
2382 // public void run() {
2383 // if (isDisposed())
2385 // tree.setTopItem(topItem);
2391 // Check if vertical scroll bar has become visible and refresh layout.
2392 ScrollBar verticalBar = tree.getVerticalBar();
2393 if(verticalBar != null) {
2394 boolean currentlyVerticalBarVisible = verticalBar.isVisible();
2395 if (verticalBarVisible != currentlyVerticalBarVisible) {
2396 verticalBarVisible = currentlyVerticalBarVisible;
2397 Composite parent = tree.getParent();
2405 * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the
2406 * return value is calculated. Items without parents have level=2,
2407 * their children level=3, etc. Returns 0 for invalid items
2409 private int getTreeItemLevel(TreeItem item) {
2413 for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);
2414 //System.out.println("\tgetTreeItemLevel(" + parent + ")");
2415 //System.out.println("level(" + item + "): " + level);
2423 private NodeContext[] getNodeContextPathSegments(NodeContext node) {
2424 TreeItem item = contextToItem.getRight(node);
2426 return NodeContext.NONE;
2427 int level = getTreeItemLevel(item);
2429 return NodeContext.NONE;
2430 // Exclude root from the saved node path.
2432 NodeContext[] segments = new NodeContext[level];
2433 for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {
2434 NodeContext ctx = (NodeContext) item.getData();
2436 return NodeContext.NONE;
2437 segments[level-1] = ctx;
2446 @SuppressWarnings("unused")
2447 private NodeContextPath getNodeContextPath(NodeContext node) {
2448 NodeContext[] path = getNodeContextPathSegments(node);
2449 return new NodeContextPath(path);
2452 void setImage(NodeContext node, TreeItem item, Imager imager, Collection<ImageDecorator> decorators, int itemIndex) {
2453 Image[] images = columnImageArray;
2454 Arrays.fill(images, null);
2455 if (imager == null) {
2456 item.setImage(images);
2460 Object[] descOrImage = columnDescOrImageArray;
2461 Arrays.fill(descOrImage, null);
2462 boolean finishLoadingInJob = false;
2464 for (Column column : columns) {
2465 String key = column.getKey();
2466 ImageDescriptor desc = imager.getImage(key);
2468 // Attempt to decorate the label
2469 if (!decorators.isEmpty()) {
2470 for (ImageDecorator id : decorators) {
2471 ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);
2477 // Try resolving only cached images here and now
2478 Object img = localResourceManager.find(desc);
2480 img = resourceManager.find(desc);
2482 images[index] = img != null ? (Image) img : null;
2483 descOrImage[index] = img == null ? desc : img;
2484 finishLoadingInJob |= img == null;
2489 // Finish loading the final image in the image loader job if necessary.
2490 if (finishLoadingInJob) {
2491 // Prevent UI from flashing unnecessarily by reusing the old image
2492 // in the item if it exists.
2493 for (int c = 0; c < columns.length; ++c) {
2494 Image img = item.getImage(c);
2498 item.setImage(images);
2500 // Schedule loading to another thread to refrain from blocking
2501 // the UI with database operations.
2502 queueImageTask(item, new ImageTask(
2505 Arrays.copyOf(descOrImage, descOrImage.length)));
2507 // Set any images that were resolved.
2508 item.setImage(images);
2512 private void queueImageTask(TreeItem item, ImageTask task) {
2513 synchronized (imageTasks) {
2514 imageTasks.put(item, task);
2516 imageLoaderJob.scheduleIfNecessary(100);
2520 * Invoked in a job worker thread.
2523 * @see ImageLoaderJob
2526 protected IStatus setPendingImages(IProgressMonitor monitor) {
2527 ImageTask[] tasks = null;
2528 synchronized (imageTasks) {
2529 tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);
2532 if (tasks.length == 0)
2533 return Status.OK_STATUS;
2535 MultiStatus status = null;
2537 // Load missing images
2538 for (ImageTask task : tasks) {
2539 Object[] descs = task.descsOrImages;
2540 for (int i = 0; i < descs.length; ++i) {
2541 Object obj = descs[i];
2542 if (obj instanceof ImageDescriptor) {
2543 ImageDescriptor desc = (ImageDescriptor) obj;
2545 descs[i] = resourceManager.get((ImageDescriptor) desc);
2546 } catch (DeviceResourceException e) {
2548 status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);
2549 status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));
2555 // Perform final UI updates in the UI thread.
2556 final ImageTask[] _tasks = tasks;
2557 thread.asyncExec(new Runnable() {
2560 if (!tree.isDisposed()) {
2561 tree.setRedraw(false);
2563 tree.setRedraw(true);
2568 return status != null ? status : Status.OK_STATUS;
2572 * Invoked in the UI thread only.
2576 void setImages(ImageTask[] tasks) {
2577 for (ImageTask task : tasks)
2583 * Invoked in the UI thread only.
2587 void setImage(ImageTask task) {
2588 // Be sure not to process disposed items.
2589 if (task.item.isDisposed())
2591 // Discard this task if the TreeItem has switched owning NodeContext.
2592 if (!contextToItem.contains(task.node, task.item))
2595 Object[] descs = task.descsOrImages;
2596 Image[] images = columnImageArray;
2597 Arrays.fill(images, null);
2598 for (int i = 0; i < descs.length; ++i) {
2599 Object desc = descs[i];
2600 if (desc instanceof Image) {
2601 images[i] = (Image) desc;
2604 task.item.setImage(images);
2607 void setText(TreeItem item, Labeler labeler, Collection<LabelDecorator> decorators, int itemIndex) {
2608 if (labeler != null) {
2609 String[] texts = new String[columns.length];
2611 Map<String, String> labels = labeler.getLabels();
2612 Map<String, String> runtimeLabels = labeler.getRuntimeLabels();
2613 for (Column column : columns) {
2614 String key = column.getKey();
2616 if (runtimeLabels != null) s = runtimeLabels.get(key);
2617 if (s == null) s = labels.get(key);
2619 FontDescriptor font = originalFont;
2620 ColorDescriptor bg = originalBackground;
2621 ColorDescriptor fg = originalForeground;
2623 // Attempt to decorate the label
2624 if (!decorators.isEmpty()) {
2625 for (LabelDecorator ld : decorators) {
2626 String ds = ld.decorateLabel(s, key, itemIndex);
2630 FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
2634 ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
2638 ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
2644 if (font != originalFont) {
2645 //System.out.println("set font: " + index + ": " + font);
2646 item.setFont(index, (Font) localResourceManager.get(font));
2648 if (bg != originalBackground)
2649 item.setBackground(index, (Color) localResourceManager.get(bg));
2650 if (fg != originalForeground)
2651 item.setForeground(index, (Color) localResourceManager.get(fg));
2657 item.setText(texts);
2659 item.setText(Labeler.NO_LABEL);
2663 void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {
2664 Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
2665 if (labeler != null) {
2666 labeler.setListener(labelListener);
2668 Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);
2669 Collection<LabelDecorator> labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);
2670 Collection<ImageDecorator> imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);
2672 setText(item, labeler, labelDecorators, itemIndex);
2673 setImage(context, item, imager, imageDecorators, itemIndex);
2677 public void setFocus() {
2682 public <T> T query(NodeContext context, CacheKey<T> key) {
2683 return this.explorerContext.cache.get(context, key);
2687 public boolean isDisposed() {
2691 protected void assertNotDisposed() {
2693 throw new IllegalStateException("disposed");
2700 * @param forceControlUpdate
2703 public void setSelection(final ISelection selection, boolean forceControlUpdate) {
2704 assertNotDisposed();
2705 boolean equalsOld = selectionProvider.selectionEquals(selection);
2706 if (equalsOld && !forceControlUpdate) {
2707 // Just set the selection object instance, fire no events nor update
2708 // the viewer selection.
2709 selectionProvider.setSelection(selection);
2711 // Schedule viewer and selection update if necessary.
2712 if (tree.isDisposed())
2714 Display d = tree.getDisplay();
2715 if (d.getThread() == Thread.currentThread()) {
2716 updateSelectionToControl(selection);
2718 d.asyncExec(new Runnable() {
2721 if (tree.isDisposed())
2723 updateSelectionToControl(selection);
2731 /* Contains the best currently found tree item and its priority
2733 private static class SelectionResolutionStatus {
2734 int bestPriority = Integer.MAX_VALUE;
2742 private void updateSelectionToControl(ISelection selection) {
2743 if (selectionDataResolver == null)
2745 if (!(selection instanceof IStructuredSelection))
2748 // Initialize selection resolution status map
2749 IStructuredSelection iss = (IStructuredSelection) selection;
2750 final THashMap<Object,SelectionResolutionStatus> statusMap =
2751 new THashMap<Object,SelectionResolutionStatus>(iss.size());
2752 for(Iterator<?> it = iss.iterator(); it.hasNext();) {
2753 Object selectionElement = it.next();
2754 Object resolvedElement = selectionDataResolver.resolve(selectionElement);
2757 new SelectionResolutionStatus());
2760 // Iterate all tree items and try to match them to the selection
2761 iterateTreeItems(new TObjectProcedure<TreeItem>() {
2763 public boolean execute(TreeItem treeItem) {
2764 NodeContext nodeContext = (NodeContext)treeItem.getData();
2765 if(nodeContext == null)
2767 SelectionResolutionStatus status = statusMap.get(nodeContext);
2768 if(status != null) {
2769 status.bestPriority = 0; // best possible match
2770 status.bestItem = treeItem;
2774 Object input = nodeContext.getConstant(BuiltinKeys.INPUT);
2775 status = statusMap.get(input);
2776 if(status != null) {
2777 NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);
2778 int curPriority = nodeType instanceof EntityNodeType
2779 ? 1 // Prefer EntityNodeType matches to other node types
2781 if(curPriority < status.bestPriority) {
2782 status.bestPriority = curPriority;
2783 status.bestItem = treeItem;
2791 ArrayList<TreeItem> items = new ArrayList<TreeItem>(statusMap.size());
2792 for(SelectionResolutionStatus status : statusMap.values())
2793 if(status.bestItem != null)
2794 items.add(status.bestItem);
2795 select(items.toArray(new TreeItem[items.size()]));
2801 public ISelection getWidgetSelection() {
2802 TreeItem[] items = tree.getSelection();
2803 if (items.length == 0)
2804 return StructuredSelection.EMPTY;
2806 List<NodeContext> nodes = new ArrayList<NodeContext>(items.length);
2808 // Caches for resolving node contexts the hard way if necessary.
2809 GENodeQueryManager manager = null;
2810 NodeContext lastParentContext = null;
2811 NodeContext[] lastChildren = null;
2813 for (int i = 0; i < items.length; i++) {
2814 TreeItem item = items[i];
2815 NodeContext ctx = (NodeContext) item.getData();
2816 // It may happen due to the virtual nature of the tree control
2817 // that it contains TreeItems which have not yet been ran through
2822 TreeItem parentItem = item.getParentItem();
2823 NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;
2824 if (parentContext != null) {
2825 NodeContext[] children = lastChildren;
2826 if (parentContext != lastParentContext) {
2827 if (manager == null)
2828 manager = new GENodeQueryManager(this.explorerContext, null, null, null);
2829 lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2830 lastParentContext = parentContext;
2832 int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);
2833 if (index >= 0 && index < children.length) {
2834 NodeContext child = children[index];
2835 if (child != null) {
2837 // Cache NodeContext in TreeItem for faster access
2838 item.setData(child);
2844 //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
2845 ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
2850 public TransientExplorerState getTransientState() {
2851 if (!thread.currentThreadAccess())
2852 throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());
2853 return transientState;
2860 private void select(TreeItem item) {
2861 tree.setSelection(item);
2862 tree.showSelection();
2863 selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
2870 private void select(TreeItem[] items) {
2871 //System.out.println("Select: " + Arrays.toString(items));
2872 tree.setSelection(items);
2873 tree.showSelection();
2874 NodeContext[] data = new NodeContext[items.length];
2875 for (int i = 0; i < data.length; i++) {
2876 data[i] = (NodeContext) items[i].getData();
2878 selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
2881 private void iterateTreeItems(TObjectProcedure<TreeItem> procedure) {
2882 for(TreeItem item : tree.getItems())
2883 if(!iterateTreeItems(item, procedure))
2887 private boolean iterateTreeItems(TreeItem item,
2888 TObjectProcedure<TreeItem> procedure) {
2889 if(!procedure.execute(item))
2891 if(item.getExpanded())
2892 for(TreeItem child : item.getItems())
2893 if(!iterateTreeItems(child, procedure))
2903 private boolean trySelect(TreeItem item, Object input) {
2904 NodeContext itemCtx = (NodeContext) item.getData();
2905 if (itemCtx != null) {
2906 if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {
2911 if (item.getExpanded()) {
2912 for (TreeItem child : item.getItems()) {
2913 if (trySelect(child, input))
2920 private boolean equalsEnough(NodeContext c1, NodeContext c2) {
2922 Object input1 = c1.getConstant(BuiltinKeys.INPUT);
2923 Object input2 = c2.getConstant(BuiltinKeys.INPUT);
2924 if(!ObjectUtils.objectEquals(input1, input2))
2927 Object type1 = c1.getConstant(NodeType.TYPE);
2928 Object type2 = c2.getConstant(NodeType.TYPE);
2929 if(!ObjectUtils.objectEquals(type1, type2))
2936 private NodeContext tryFind(NodeContext context) {
2937 for (TreeItem item : tree.getItems()) {
2938 NodeContext found = tryFind(item, context);
2939 if(found != null) return found;
2944 private NodeContext tryFind(TreeItem item, NodeContext context) {
2945 NodeContext itemCtx = (NodeContext) item.getData();
2946 if (itemCtx != null) {
2947 if (equalsEnough(context, itemCtx)) {
2951 if (item.getExpanded()) {
2952 for (TreeItem child : item.getItems()) {
2953 NodeContext found = tryFind(child, context);
2954 if(found != null) return found;
2961 public boolean select(NodeContext context) {
2963 assertNotDisposed();
2965 if (context == null || context.equals(rootContext)) {
2967 selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
2971 // if (context.equals(rootContext)) {
2972 // tree.deselectAll();
2973 // selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
2977 Object input = context.getConstant(BuiltinKeys.INPUT);
2979 for (TreeItem item : tree.getItems()) {
2980 if (trySelect(item, input))
2988 private NodeContext tryFind2(NodeContext context) {
2989 Set<NodeContext> ctxs = contextToItem.getLeftSet();
2990 for(NodeContext c : ctxs)
2991 if(equalsEnough(c, context))
2996 private boolean waitVisible(NodeContext parent, NodeContext context) {
2997 long start = System.nanoTime();
2999 TreeItem parentItem = contextToItem.getRight(parent);
3001 if(parentItem == null)
3005 NodeContext target = tryFind2(context);
3006 if(target != null) {
3007 TreeItem item = contextToItem.getRight(target);
3008 if (!(item.getParentItem().equals(parentItem)))
3010 tree.setTopItem(item);
3014 Display.getCurrent().readAndDispatch();
3015 long duration = System.nanoTime() - start;
3021 private boolean selectPathInternal(NodeContext[] contexts, int position) {
3022 //System.out.println("NodeContext path : " + contexts);
3024 NodeContext head = tryFind(contexts[position]);
3026 if(position == contexts.length-1) {
3027 return select(head);
3031 //setExpanded(head, true);
3033 if(!waitVisible(head, contexts[position+1]))
3036 setExpanded(head, true);
3038 return selectPathInternal(contexts, position+1);
3043 public boolean selectPath(Collection<NodeContext> contexts) {
3045 if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");
3046 if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");
3048 return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
3053 public boolean isVisible(NodeContext context) {
3055 for (TreeItem item : tree.getItems()) {
3056 NodeContext found = tryFind(item, context);
3065 protected ISelection constructSelection(NodeContext... contexts) {
3066 if (contexts == null)
3067 throw new IllegalArgumentException("null contexts");
3068 if (contexts.length == 0)
3069 return StructuredSelection.EMPTY;
3070 if (selectionFilter == null)
3071 return new StructuredSelection(transformSelection(contexts));
3072 return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );
3075 protected Object[] transformSelection(Object[] objects) {
3076 return selectionTransformation.apply(this, objects);
3079 protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
3080 int len = contexts.length;
3081 Object[] objects = new Object[len];
3082 for (int i = 0; i < len; ++i)
3083 objects[i] = filter.filter(contexts[i]);
3088 public void setExpanded(final NodeContext context, final boolean expanded) {
3089 assertNotDisposed();
3090 ThreadUtils.asyncExec(thread, new Runnable() {
3094 doSetExpanded(context, expanded);
3099 private void doSetExpanded(NodeContext context, boolean expanded) {
3100 //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
3101 TreeItem item = contextToItem.getRight(context);
3103 item.setExpanded(expanded);
3105 PrimitiveQueryProcessor<?> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
3106 if (pqp instanceof IsExpandedProcessor) {
3107 IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
3108 iep.replaceExpanded(context, expanded);
3113 public void setColumnsVisible(boolean visible) {
3114 columnsAreVisible = visible;
3115 if(tree != null) tree.setHeaderVisible(columnsAreVisible);
3119 public void setColumns(final Column[] columns) {
3120 setColumns(columns, null);
3124 public void setColumns(final Column[] columns, Consumer<Map<Column, Object>> callback) {
3125 assertNotDisposed();
3126 checkUniqueColumnKeys(columns);
3128 Display d = tree.getDisplay();
3129 if (d.getThread() == Thread.currentThread())
3130 doSetColumns(columns, callback);
3133 if (tree.isDisposed())
3135 doSetColumns(columns, callback);
3139 private void checkUniqueColumnKeys(Column[] cols) {
3140 Set<String> usedColumnKeys = new HashSet<String>();
3141 List<Column> duplicateColumns = new ArrayList<Column>();
3142 for (Column c : cols) {
3143 if (!usedColumnKeys.add(c.getKey()))
3144 duplicateColumns.add(c);
3146 if (!duplicateColumns.isEmpty()) {
3147 throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
3152 * Only meant to be invoked from the SWT UI thread.
3156 private void doSetColumns(Column[] cols, Consumer<Map<Column, Object>> callback) {
3157 // Attempt to keep previous column widths.
3158 Map<String, Integer> prevWidths = new HashMap<>();
3159 for (TreeColumn column : tree.getColumns()) {
3160 Column c = (Column) column.getData();
3162 prevWidths.put(c.getKey(), column.getWidth());
3167 HashMap<String, Integer> keyToIndex = new HashMap<>();
3168 for (int i = 0; i < cols.length; ++i) {
3169 keyToIndex.put(cols[i].getKey(), i);
3172 this.columns = Arrays.copyOf(cols, cols.length);
3173 //this.columns[cols.length] = FILLER_COLUMN;
3174 this.columnKeyToIndex = keyToIndex;
3175 this.columnImageArray = new Image[cols.length];
3176 this.columnDescOrImageArray = new Object[cols.length];
3178 Map<Column, Object> map = new HashMap<>();
3180 tree.setHeaderVisible(columnsAreVisible);
3181 for (Column column : columns) {
3182 TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));
3185 c.setText(column.getLabel());
3186 c.setToolTipText(column.getTooltip());
3188 int cw = column.getWidth();
3190 // Try to keep previous widths
3191 Integer w = prevWidths.get(column.getKey());
3194 else if (cw != Column.DEFAULT_CONTROL_WIDTH)
3197 // Go for some kind of default settings then...
3198 if (ColumnKeys.PROPERTY.equals(column.getKey()))
3204 // if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
3205 // c.addListener(SWT.Resize, resizeListener);
3206 // c.setResizable(true);
3208 // //c.setResizable(false);
3213 if(callback != null) callback.accept(map);
3215 // Make sure the explorer fits the columns properly after initialization.
3216 SWTUtils.asyncExec(tree, () -> {
3217 if (!tree.isDisposed())
3218 refreshColumnSizes();
3222 int toSWT(Align alignment) {
3223 switch (alignment) {
3224 case LEFT: return SWT.LEFT;
3225 case CENTER: return SWT.CENTER;
3226 case RIGHT: return SWT.RIGHT;
3227 default: throw new Error("unhandled alignment: " + alignment);
3232 public Column[] getColumns() {
3233 return Arrays.copyOf(columns, columns.length);
3236 private void detachPrimitiveProcessors() {
3237 for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3238 if (p instanceof ProcessorLifecycle) {
3239 ((ProcessorLifecycle) p).detached(this);
3244 private void clearPrimitiveProcessors() {
3245 for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3246 if (p instanceof ProcessorLifecycle) {
3247 ((ProcessorLifecycle) p).clear();
3252 Listener resizeListener = new Listener() {
3254 public void handleEvent(Event event) {
3255 // Prevent infinite recursion.
3256 if (refreshingColumnSizes)
3258 //TreeColumn column = (TreeColumn) event.widget;
3259 //Column c = (Column) column.getData();
3260 refreshColumnSizes();
3264 Listener itemDisposeListener = new Listener() {
3266 public void handleEvent(Event event) {
3267 if (event.type == SWT.Dispose) {
3268 if (event.widget instanceof TreeItem) {
3269 TreeItem ti = (TreeItem) event.widget;
3270 //NodeContext ctx = (NodeContext) ti.getData();
3271 // System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));
3272 // System.out.println(" map size BEFORE: " + contextToItem.size());
3273 @SuppressWarnings("unused")
3274 NodeContext removed = contextToItem.removeWithRight(ti);
3275 // System.out.println(" REMOVED: " + removed);
3276 // System.out.println(" map size AFTER: " + contextToItem.size());
3285 LabelerListener labelListener = new LabelerListener() {
3287 public boolean columnModified(final NodeContext context, final String key, final String newLabel) {
3288 //System.out.println("column " + key + " modified for " + context + " to " + newLabel);
3289 if (tree.isDisposed())
3292 synchronized (labelRefreshRunnables) {
3293 Runnable refresher = new Runnable() {
3296 // Tree is guaranteed to be non-disposed if this is invoked.
3298 // contextToItem should be accessed only in the SWT thread to keep things thread-safe.
3299 final TreeItem item = contextToItem.getRight(context);
3300 if (item == null || item.isDisposed())
3303 final Integer index = columnKeyToIndex.get(key);
3307 //System.out.println(" found index: " + index);
3308 //System.out.println(" found item: " + item);
3310 GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
3312 // FIXME: indexOf is quadratic
3314 TreeItem parentItem = item.getParentItem();
3315 if (parentItem == null) {
3316 itemIndex = tree.indexOf(item);
3317 //tree.clear(parentIndex, false);
3319 itemIndex = parentItem.indexOf(item);
3320 //item.clear(parentIndex, false);
3322 setTextAndImage(item, manager, context, itemIndex);
3323 } catch (SWTException e) {
3324 ErrorLogger.defaultLogError(e);
3328 //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
3329 labelRefreshRunnables.put(context, refresher);
3331 if (!refreshIsQueued) {
3332 refreshIsQueued = true;
3334 long now = System.currentTimeMillis();
3335 long elapsed = now - lastLabelRefreshScheduled;
3336 if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)
3337 delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;
3338 //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");
3340 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
3343 scheduleImmediateLabelRefresh();
3345 }, delay, TimeUnit.MILLISECONDS);
3347 scheduleImmediateLabelRefresh();
3349 lastLabelRefreshScheduled = now;
3356 public boolean columnsModified(final NodeContext context, final Map<String, String> columns) {
3357 System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
3362 private void scheduleImmediateLabelRefresh() {
3363 Runnable[] runnables = null;
3364 synchronized (labelRefreshRunnables) {
3365 if (labelRefreshRunnables.isEmpty())
3368 runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
3369 labelRefreshRunnables.clear();
3370 refreshIsQueued = false;
3372 final Runnable[] rs = runnables;
3374 if (tree.isDisposed())
3376 tree.getDisplay().asyncExec(new Runnable() {
3379 if (tree.isDisposed())
3381 //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");
3382 tree.setRedraw(false);
3383 for (Runnable r : rs) {
3386 tree.setRedraw(true);
3391 long lastLabelRefreshScheduled = 0;
3392 boolean refreshIsQueued = false;
3393 Map<NodeContext, Runnable> labelRefreshRunnables = new HashMap<NodeContext, Runnable>();
3395 @SuppressWarnings("unchecked")
3397 public <T> T getAdapter(Class<T> adapter) {
3398 if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;
3399 else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;
3403 @SuppressWarnings("unchecked")
3405 public <T> T getControl() {
3410 * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
3413 public void setAutoExpandLevel(int level) {
3414 this.autoExpandLevel = level;
3418 public <T> NodeQueryProcessor<T> getProcessor(QueryKey<T> key) {
3419 return explorerContext.getProcessor(key);
3423 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(PrimitiveQueryKey<T> key) {
3424 return explorerContext.getPrimitiveProcessor(key);
3428 public boolean isEditable() {
3433 public void setEditable(boolean editable) {
3434 if (!thread.currentThreadAccess())
3435 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
3437 this.editable = editable;
3438 Display display = tree.getDisplay();
3439 tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
3443 * For setting a more local service locator for the explorer than the global
3444 * workbench service locator. Sometimes required to give this implementation
3445 * access to local workbench services like IFocusService.
3448 * Must be invoked during right after construction.
3450 * @param serviceLocator
3451 * a specific service locator or <code>null</code> to use the
3452 * workbench global service locator
3454 public void setServiceLocator(IServiceLocator serviceLocator) {
3455 if (serviceLocator == null && PlatformUI.isWorkbenchRunning())
3456 serviceLocator = PlatformUI.getWorkbench();
3457 this.serviceLocator = serviceLocator;
3458 if (serviceLocator != null) {
3459 this.contextService = (IContextService) serviceLocator.getService(IContextService.class);
3460 this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);
3465 public Object getClicked(Object event) {
3466 MouseEvent e = (MouseEvent)event;
3467 final Tree tree = (Tree) e.getSource();
3468 Point point = new Point(e.x, e.y);
3469 TreeItem item = tree.getItem(point);
3471 // No selectable item at point?
3475 Object data = item.getData();