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;
177 import gnu.trove.map.hash.THashMap;
178 import gnu.trove.procedure.TObjectProcedure;
179 import gnu.trove.set.hash.THashSet;
182 * @see #getMaxChildren()
183 * @see #setMaxChildren(int)
184 * @see #getMaxChildren(NodeQueryManager, NodeContext)
186 class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
188 private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
190 private GraphExplorerImpl ge;
192 GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
201 public void setSelection(final ISelection selection) {
202 if(ge == null) return;
203 ge.setSelection(selection, false);
208 public void removeSelectionChangedListener(ISelectionChangedListener listener) {
209 if(ge == null) return;
210 if(ge.isDisposed()) {
211 if (DEBUG_SELECTION_LISTENERS)
212 System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);
215 //assertNotDisposed();
216 //System.out.println("Remove selection changed listener: " + listener);
217 ge.selectionProvider.removeSelectionChangedListener(listener);
221 public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
222 if(ge == null) return;
223 if (!ge.thread.currentThreadAccess())
224 throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
225 if(ge.isDisposed()) {
226 System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);
229 //System.out.println("Add POST selection changed listener: " + listener);
230 ge.selectionProvider.addPostSelectionChangedListener(listener);
234 public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
235 if(ge == null) return;
236 if(ge.isDisposed()) {
237 if (DEBUG_SELECTION_LISTENERS)
238 System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);
241 // assertNotDisposed();
242 //System.out.println("Remove POST selection changed listener: " + listener);
243 ge.selectionProvider.removePostSelectionChangedListener(listener);
248 public void addSelectionChangedListener(ISelectionChangedListener listener) {
249 if(ge == null) return;
250 if (!ge.thread.currentThreadAccess())
251 throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
252 //System.out.println("Add selection changed listener: " + listener);
253 if (ge.tree.isDisposed() || ge.selectionProvider == null) {
254 System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);
258 ge.selectionProvider.addSelectionChangedListener(listener);
263 public ISelection getSelection() {
264 if(ge == null) return StructuredSelection.EMPTY;
265 if (!ge.thread.currentThreadAccess())
266 throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());
267 if (ge.tree.isDisposed() || ge.selectionProvider == null)
268 return StructuredSelection.EMPTY;
269 return ge.selectionProvider.getSelection();
275 * If this explorer is running with an Eclipse workbench open, this
276 * Workbench UI context will be activated whenever inline editing is started
277 * through {@link #startEditing(TreeItem, int)} and deactivated when inline
280 * This context information can be used to for UI handler activity testing.
282 private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
284 private static final String KEY_DRAG_COLUMN = "dragColumn";
286 private static final boolean DEBUG_SELECTION_LISTENERS = false;
288 private static final int DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
290 public static final int DEFAULT_MAX_CHILDREN = 1000;
292 private final IThreadWorkQueue thread;
295 * Local method for checking from whether resources are loaded in
298 private final LocalResourceManager localResourceManager;
301 * Local device resource manager that is safe to use in
302 * {@link ImageLoaderJob} for creating images in a non-UI thread.
304 private final ResourceManager resourceManager;
307 * Package visibility.
308 * TODO: Get rid of these.
312 @SuppressWarnings({ "rawtypes" })
313 final HashMap<CacheKey<?>, NodeQueryProcessor> processors = new HashMap<>();
314 @SuppressWarnings({ "rawtypes" })
315 final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors = new HashMap<>();
316 @SuppressWarnings({ "rawtypes" })
317 final HashMap<Class, DataSource> dataSources = new HashMap<>();
319 class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
320 // This is for query debugging only.
323 GECache cache = new GECache();
324 AtomicBoolean propagating = new AtomicBoolean(false);
325 Object propagateList = new Object();
326 Object propagate = new Object();
327 List<Runnable> scheduleList = new ArrayList<Runnable>();
328 final Deque<Integer> activity = new LinkedList<Integer>();
332 * Stores the currently running query update runnable. If
333 * <code>null</code> there's nothing scheduled yet in which case
334 * scheduling can commence. Otherwise the update should be skipped.
336 AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<>();
339 * Keeps track of nodes that have already been auto-expanded. After
340 * being inserted into this set, nodes will not be forced to stay in an
341 * expanded state after that. This makes it possible for the user to
342 * close auto-expanded nodes.
344 Map<NodeContext, Boolean> autoExpanded = new WeakHashMap<>();
348 protected void doDispose() {
350 autoExpanded.clear();
354 public IGECache getCache() {
359 public int queryIndent() {
364 public int queryIndent(int offset) {
365 queryIndent += offset;
370 @SuppressWarnings("unchecked")
371 public <T> NodeQueryProcessor<T> getProcessor(Object o) {
372 return processors.get(o);
376 @SuppressWarnings("unchecked")
377 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(Object o) {
378 return primitiveProcessors.get(o);
381 @SuppressWarnings("unchecked")
383 public <T> DataSource<T> getDataSource(Class<T> clazz) {
384 return dataSources.get(clazz);
388 public void update(UIElementReference ref) {
389 //System.out.println("GE.update " + ref);
390 TreeItemReference tiref = (TreeItemReference) ref;
391 TreeItem item = tiref.getItem();
392 // NOTE: must be called regardless of the the item value.
393 // A null item is currently used to indicate a tree root update.
394 GraphExplorerImpl.this.update(item);
398 public Object getPropagateLock() {
403 public Object getPropagateListLock() {
404 return propagateList;
408 public boolean isPropagating() {
409 return propagating.get();
413 public void setPropagating(boolean b) {
414 this.propagating.set(b);
418 public List<Runnable> getScheduleList() {
423 public void setScheduleList(List<Runnable> list) {
424 this.scheduleList = list;
428 public Deque<Integer> getActivity() {
433 public void setActivityInt(int i) {
434 this.activityInt = i;
438 public int getActivityInt() {
443 public void scheduleQueryUpdate(Runnable r) {
444 if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
446 //System.out.println("Scheduling query update for runnable " + r);
447 if (currentQueryUpdater.compareAndSet(null, r)) {
448 //System.out.println("Scheduling query update for runnable " + r);
449 queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);
453 Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
456 Runnable r = currentQueryUpdater.getAndSet(null);
458 //System.out.println("Running query update runnable " + r);
465 GraphExplorerContext explorerContext = new GraphExplorerContext();
467 HashSet<TreeItem> pendingItems = new HashSet<>();
468 boolean updating = false;
469 boolean pendingRoot = false;
471 @SuppressWarnings("deprecation")
472 ModificationContext modificationContext = null;
474 NodeContext rootContext;
476 StatePersistor persistor = null;
478 boolean editable = true;
481 * This is a reverse mapping from {@link NodeContext} tree objects back to
482 * their owner TreeItems.
485 * Access this map only in the SWT thread to keep it thread-safe.
488 BijectionMap<NodeContext, TreeItem> contextToItem = new BijectionMap<>();
491 * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
494 Column[] columns = new Column[0];
495 Map<String, Integer> columnKeyToIndex = new HashMap<>();
496 boolean refreshingColumnSizes = false;
497 boolean columnsAreVisible = true;
500 * An array reused for invoking {@link TreeItem#setImage(Image[])} instead
501 * of constantly allocating new arrays for setting each TreeItems images.
502 * This works because {@link TreeItem#setImage(Image[])} does not take hold
503 * of the array itself, only the contents of the array.
505 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
507 Image[] columnImageArray = { null };
510 * Used for collecting Image or ImageDescriptor instances for a single
511 * TreeItem when initially setting images for a TreeItem.
513 * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
515 Object[] columnDescOrImageArray = { null };
517 final ExecutorService queryUpdateScheduler = Threads.getExecutor();
518 final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor();
520 /** Set to true when the Tree widget is disposed. */
521 private boolean disposed = false;
522 private final CopyOnWriteArrayList<FocusListener> focusListeners = new CopyOnWriteArrayList<>();
523 private final CopyOnWriteArrayList<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();
524 private final CopyOnWriteArrayList<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
526 /** Selection provider */
527 private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
528 protected BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider();
529 protected SelectionDataResolver selectionDataResolver;
530 protected SelectionFilter selectionFilter;
531 protected BiFunction<GraphExplorer, Object[], Object[]> selectionTransformation = new BiFunction<GraphExplorer, Object[], Object[]>() {
534 public Object[] apply(GraphExplorer explorer, Object[] objects) {
535 Object[] result = new Object[objects.length];
536 for (int i = 0; i < objects.length; i++) {
537 IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
538 context.setHint(SelectionHints.KEY_MAIN, objects[i]);
545 protected FontDescriptor originalFont;
546 protected ColorDescriptor originalForeground;
547 protected ColorDescriptor originalBackground;
550 * The set of currently selected TreeItem instances. This set is needed
551 * because we need to know in {@link #setData(Event)} whether the updated
552 * item was a part of the current selection in which case the selection must
555 private final Map<TreeItem, NodeContext> selectedItems = new HashMap<>();
558 * TODO: specify what this is for
560 private final Set<NodeContext> selectionRefreshContexts = new HashSet<>();
563 * If this field is non-null, it means that if {@link #setData(Event)}
564 * encounters a NodeContext equal to this one, it must make the TreeItem
565 * assigned to that NodeContext the topmost item of the tree using
566 * {@link Tree#setTopItem(TreeItem)}. After this the field value is
570 * This is related to {@link #initializeState()}, i.e. explorer state
573 // private NodeContext[] topNodePath = NodeContext.NONE;
574 // private int[] topNodePath = {};
575 // private int currentTopNodePathIndex = -1;
578 * See {@link #setAutoExpandLevel(int)}
580 private int autoExpandLevel = 0;
583 * <code>null</code> if not explicitly set through
584 * {@link #setServiceLocator(IServiceLocator)}.
586 private IServiceLocator serviceLocator;
589 * The global workbench context service, if the workbench is available.
590 * Retrieved in the constructor.
592 private IContextService contextService = null;
595 * The global workbench IFocusService, if the workbench is available.
596 * Retrieved in the constructor.
598 private IFocusService focusService = null;
601 * A Workbench UI context activation that is activated when starting inline
602 * editing through {@link #startEditing(TreeItem, int)}.
604 * @see #activateEditingContext()
605 * @see #deactivateEditingContext()
607 private IContextActivation editingContext = null;
609 static class ImageTask {
612 Object[] descsOrImages;
613 public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
616 this.descsOrImages = descsOrImages;
621 * The job that is used for off-loading image loading tasks (see
622 * {@link ImageTask} to a worker thread from the main UI thread.
624 * @see #setPendingImages(IProgressMonitor)
626 ImageLoaderJob imageLoaderJob;
629 * The set of currently gathered up image loading tasks for
630 * {@link #imageLoaderJob} to execute.
632 * @see #setPendingImages(IProgressMonitor)
634 Map<TreeItem, ImageTask> imageTasks = new THashMap<>();
637 * A state flag indicating whether the vertical scroll bar was visible for
638 * {@link #tree} the last time it was checked. Since there is no listener
639 * that can provide this information, we check it in {@link #setData(Event)}
640 * every time any data for a TreeItem is updated. If the visibility changes,
641 * we will force re-layouting of the tree's parent composite.
643 * @see #setData(Event)
645 private boolean verticalBarVisible = false;
647 static class TransientStateImpl implements TransientExplorerState {
649 private Integer activeColumn = null;
652 public synchronized Integer getActiveColumn() {
656 public synchronized void setActiveColumn(Integer column) {
657 activeColumn = column;
662 private TransientStateImpl transientState = new TransientStateImpl();
664 boolean scheduleUpdater() {
666 if (tree.isDisposed())
669 if (pendingRoot == true || !pendingItems.isEmpty()) {
670 assert(!tree.isDisposed());
672 int activity = explorerContext.activityInt;
674 if (activity < 100) {
675 // System.out.println("Scheduling update immediately.");
676 } else if (activity < 1000) {
677 // System.out.println("Scheduling update after 500ms.");
680 // System.out.println("Scheduling update after 3000ms.");
686 //System.out.println("Scheduling UI update after " + delay + " ms.");
687 uiUpdateScheduler.schedule(new Runnable() {
691 if (tree.isDisposed())
694 if (updateCounter > 0) {
696 uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
698 tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this));
702 }, delay, TimeUnit.MILLISECONDS);
711 int updateCounter = 0;
713 void update(TreeItem item) {
715 synchronized(pendingItems) {
717 // System.out.println("update " + item);
721 if(item == null) pendingRoot = true;
722 else pendingItems.add(item);
724 if(updating == true) return;
732 private int maxChildren = DEFAULT_MAX_CHILDREN;
735 public int getMaxChildren() {
740 public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
741 Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
742 //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
743 if (result != null) {
745 throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
752 public void setMaxChildren(int maxChildren) {
753 this.maxChildren = maxChildren;
757 public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
758 this.modificationContext = modificationContext;
762 * @param parent the parent SWT composite
764 public GraphExplorerImpl(Composite parent) {
765 this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
769 * Stores the node context and the modifier that is currently being
770 * modified. These are used internally to prevent duplicate edits from being
771 * initiated which should always be a sensible thing to do.
773 private Set<NodeContext> currentlyModifiedNodes = new THashSet<>();
775 private final TreeEditor editor;
776 private Color invalidModificationColor = null;
779 * @param item the TreeItem to start editing
780 * @param columnIndex the index of the column to edit, starts counting from
782 * @return <code>true</code> if the editing was initiated successfully or
783 * <code>false</code> if editing could not be started due to lack of
784 * {@link Modifier} for the labeler in question.
786 private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
788 return "Rename not supported for selection";
790 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
791 final NodeContext context = (NodeContext) item.getData();
792 Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
794 return "Rename not supported for selection";
796 if(columnKey == null) columnKey = columns[columnIndex].getKey();
798 // columnKey might be prefixed with '#' to indicate
799 // textual editing is preferred. Try to get modifier
800 // for that first and only if it fails, try without
802 Modifier modifier = labeler.getModifier(modificationContext, columnKey);
803 if (modifier == null) {
804 if(columnKey.startsWith("#"))
805 modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
806 if (modifier == null)
807 return "Rename not supported for selection";
809 if (modifier instanceof DeniedModifier) {
810 DeniedModifier dm = (DeniedModifier)modifier;
811 return dm.getMessage();
814 // Prevent editing of a single node context multiple times.
815 if (currentlyModifiedNodes.contains(context)) {
816 //System.out.println("discarding duplicate edit for context " + context);
817 return "Rename not supported for selection";
820 // Clean up any previous editor control
821 Control oldEditor = editor.getEditor();
822 if (oldEditor != null)
825 if (modifier instanceof DialogModifier) {
826 performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
827 } else if (modifier instanceof CustomModifier) {
828 startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
829 } else if (modifier instanceof EnumerationModifier) {
830 startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
832 startTextEditing(item, columnIndex, context, modifier);
844 void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
845 final DialogModifier modifier) {
846 final AtomicBoolean disposed = new AtomicBoolean(false);
847 Consumer<String> callback = result -> {
850 String error = modifier.isValid(result);
852 modifier.modify(result);
853 // Item may be disposed if the tree gets reset after a previous editing.
854 if (!item.isDisposed()) {
855 item.setText(columnIndex, result);
856 queueSelectionRefresh(context);
861 currentlyModifiedNodes.add(context);
863 String status = modifier.query(tree, item, columnIndex, context, callback);
865 ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
867 currentlyModifiedNodes.remove(context);
872 private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
873 Point size = control.computeSize(widthHint, heightHint);
874 editor.horizontalAlignment = SWT.LEFT;
875 Rectangle itemRect = item.getBounds(columnIndex),
876 rect = tree.getClientArea();
877 editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
878 int left = itemRect.x,
879 right = rect.x + rect.width;
880 editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
881 editor.minimumHeight = size.y + insetY * 2;
885 void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
886 GC gc = new GC(control);
887 Point size = gc.textExtent(text);
889 reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
898 void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
899 final CustomModifier modifier) {
900 final Object obj = modifier.createControl(tree, item, columnIndex, context);
901 if (!(obj instanceof Control))
902 throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
903 final Control control = (Control) obj;
905 // final int insetX = 0;
906 // final int insetY = 0;
907 // control.addListener(SWT.Resize, new Listener() {
909 // public void handleEvent(Event e) {
910 // Rectangle rect = control.getBounds();
911 // control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
914 control.addListener(SWT.Dispose, new Listener() {
916 public void handleEvent(Event event) {
917 currentlyModifiedNodes.remove(context);
918 queueSelectionRefresh(context);
919 deactivateEditingContext();
923 if (!(control instanceof Shell)) {
924 editor.setEditor(control, item, columnIndex);
930 GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
932 activateEditingContext(control);
934 // Removed in disposeListener above
935 currentlyModifiedNodes.add(context);
936 //System.out.println("START CUSTOM EDITING: " + item);
945 void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
946 String initialText = modifier.getValue();
947 if (initialText == null)
948 throw new AssertionError("Labeler.Modifier.getValue() returned null");
950 List<String> values = modifier.getValues();
951 String selectedValue = modifier.getValue();
952 int selectedIndex = values.indexOf(selectedValue);
953 if (selectedIndex == -1)
954 throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
956 final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
957 combo.setVisibleItemCount(10);
958 //combo.setEditable(false);
960 for (String value : values) {
963 combo.select(selectedIndex);
965 Listener comboListener = new Listener() {
966 boolean arrowTraverseUsed = false;
968 public void handleEvent(final Event e) {
969 //System.out.println("FOO: " + e);
972 if (e.character == SWT.CR) {
973 // Commit edit directly on ENTER press.
974 String text = combo.getText();
975 modifier.modify(text);
976 // Item may be disposed if the tree gets reset after a previous editing.
977 if (!item.isDisposed()) {
978 item.setText(columnIndex, text);
979 queueSelectionRefresh(context);
983 } else if (e.keyCode == SWT.ESC) {
984 // Cancel editing immediately
991 if (arrowTraverseUsed) {
992 arrowTraverseUsed = false;
996 String text = combo.getText();
997 modifier.modify(text);
999 // Item may be disposed if the tree gets reset after a previous editing.
1000 if (!item.isDisposed()) {
1001 item.setText(columnIndex, text);
1002 queueSelectionRefresh(context);
1007 case SWT.FocusOut: {
1008 String text = combo.getText();
1009 modifier.modify(text);
1011 // Item may be disposed if the tree gets reset after a previous editing.
1012 if (!item.isDisposed()) {
1013 item.setText(columnIndex, text);
1014 queueSelectionRefresh(context);
1019 case SWT.Traverse: {
1021 case SWT.TRAVERSE_RETURN:
1022 String text = combo.getText();
1023 modifier.modify(text);
1024 if (!item.isDisposed()) {
1025 item.setText(columnIndex, text);
1026 queueSelectionRefresh(context);
1028 arrowTraverseUsed = false;
1030 case SWT.TRAVERSE_ESCAPE:
1034 case SWT.TRAVERSE_ARROW_NEXT:
1035 case SWT.TRAVERSE_ARROW_PREVIOUS:
1036 arrowTraverseUsed = true;
1039 //System.out.println("unhandled traversal: " + e.detail);
1045 currentlyModifiedNodes.remove(context);
1046 deactivateEditingContext();
1051 combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
1052 combo.addListener(SWT.KeyDown, comboListener);
1053 combo.addListener(SWT.FocusOut, comboListener);
1054 combo.addListener(SWT.Traverse, comboListener);
1055 combo.addListener(SWT.Selection, comboListener);
1056 combo.addListener(SWT.Dispose, comboListener);
1058 editor.setEditor(combo, item, columnIndex);
1061 combo.setListVisible(true);
1063 GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
1065 activateEditingContext(combo);
1067 // Removed in comboListener
1068 currentlyModifiedNodes.add(context);
1070 //System.out.println("START ENUMERATION EDITING: " + item);
1075 * @param columnIndex
1079 void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {
1080 String initialText = modifier.getValue();
1081 if (initialText == null)
1082 throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);
1084 final Composite composite = new Composite(tree, SWT.NONE);
1085 //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));
1086 final Text text = new Text(composite, SWT.BORDER);
1087 final int insetX = 0;
1088 final int insetY = 0;
1089 composite.addListener(SWT.Resize, new Listener() {
1091 public void handleEvent(Event e) {
1092 Rectangle rect = composite.getClientArea();
1093 text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height
1097 final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
1098 Listener textListener = new Listener() {
1100 boolean modified = false;
1103 public void handleEvent(final Event e) {
1109 //System.out.println("FOCUS OUT " + item);
1110 newText = text.getText();
1111 error = modifier.isValid(newText);
1112 if (error == null) {
1113 modifier.modify(newText);
1115 // Item may be disposed if the tree gets reset after a previous editing.
1116 if (!item.isDisposed()) {
1117 item.setText(columnIndex, newText);
1118 queueSelectionRefresh(context);
1121 // System.out.println("validation error: " + error);
1124 composite.dispose();
1127 newText = text.getText();
1128 error = modifier.isValid(newText);
1129 if (error != null) {
1130 text.setBackground(invalidModificationColor);
1132 //System.out.println("validation error: " + error);
1134 text.setBackground(null);
1141 // Safety check since it seems that this may happen with
1143 if (item.isDisposed())
1146 // Filter input if necessary
1147 e.text = filter != null ? filter.filter(e.text) : e.text;
1149 newText = text.getText();
1150 String leftText = newText.substring(0, e.start);
1151 String rightText = newText.substring(e.end, newText.length());
1152 GraphExplorerImpl.this.reconfigureTreeEditorForText(
1153 item, columnIndex, text, leftText + e.text + rightText,
1154 SWT.DEFAULT, insetX, insetY);
1158 case SWT.TRAVERSE_RETURN:
1160 newText = text.getText();
1161 error = modifier.isValid(newText);
1162 if (error == null) {
1163 modifier.modify(newText);
1164 if (!item.isDisposed()) {
1165 item.setText(columnIndex, newText);
1166 queueSelectionRefresh(context);
1171 case SWT.TRAVERSE_ESCAPE:
1172 composite.dispose();
1176 //System.out.println("unhandled traversal: " + e.detail);
1182 currentlyModifiedNodes.remove(context);
1183 deactivateEditingContext();
1189 // Set the initial text before registering a listener. We do not want immediate modification!
1190 text.setText(initialText);
1191 text.addListener(SWT.FocusOut, textListener);
1192 text.addListener(SWT.Traverse, textListener);
1193 text.addListener(SWT.Verify, textListener);
1194 text.addListener(SWT.Modify, textListener);
1195 text.addListener(SWT.Dispose, textListener);
1196 editor.setEditor(composite, item, columnIndex);
1200 // Initialize TreeEditor properly.
1201 GraphExplorerImpl.this.reconfigureTreeEditorForText(
1202 item, columnIndex, text, initialText,
1203 SWT.DEFAULT, insetX, insetY);
1205 // Removed in textListener
1206 currentlyModifiedNodes.add(context);
1208 activateEditingContext(text);
1210 //System.out.println("START TEXT EDITING: " + item);
1213 protected void errorStatus(String error) {
1214 IStatusLineManager status = getStatusLineManager();
1215 if (status != null) {
1216 status.setErrorMessage(error);
1220 protected IStatusLineManager getStatusLineManager() {
1221 if (serviceLocator instanceof IWorkbenchPart) {
1222 return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);
1223 } else if (serviceLocator instanceof IWorkbenchSite) {
1224 return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);
1229 protected void activateEditingContext(Control control) {
1230 if (contextService != null) {
1231 editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
1233 if (control != null && focusService != null) {
1234 focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);
1235 // No need to remove the control, it will be
1236 // removed automatically when it is disposed.
1240 protected void deactivateEditingContext() {
1241 IContextActivation a = editingContext;
1243 editingContext = null;
1244 contextService.deactivateContext(a);
1251 void queueSelectionRefresh(NodeContext forContext) {
1252 selectionRefreshContexts.add(forContext);
1256 public String startEditing(NodeContext context, String columnKey_) {
1257 assertNotDisposed();
1258 if (!thread.currentThreadAccess())
1259 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
1261 String columnKey = columnKey_;
1262 if(columnKey.startsWith("#")) {
1263 columnKey = columnKey.substring(1);
1266 Integer columnIndex = columnKeyToIndex.get(columnKey);
1267 if (columnIndex == null)
1268 return "Rename not supported for selection";
1270 TreeItem item = contextToItem.getRight(context);
1272 return "Rename not supported for selection";
1274 return startEditing(item, columnIndex, columnKey_);
1279 public String startEditing(String columnKey) {
1281 ISelection selection = postSelectionProvider.getSelection();
1282 if(selection == null) return "Rename not supported for selection";
1283 NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
1284 if(context == null) return "Rename not supported for selection";
1286 return startEditing(context, columnKey);
1291 * @param site <code>null</code> if the explorer is detached from the workbench
1292 * @param parent the parent SWT composite
1293 * @param style the tree style to use, check the see tags for the available flags
1298 * @see SWT#FULL_SELECTION
1299 * @see SWT#NO_SCROLL
1303 public GraphExplorerImpl(Composite parent, int style) {
1305 setServiceLocator(null);
1307 this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
1308 this.resourceManager = new DeviceResourceManager(parent.getDisplay());
1310 this.imageLoaderJob = new ImageLoaderJob(this);
1311 this.imageLoaderJob.setPriority(Job.DECORATE);
1313 invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
1315 this.thread = SWTThread.getThreadAccess(parent);
1317 for(int i=0;i<10;i++) explorerContext.activity.push(0);
1319 tree = new Tree(parent, style);
1320 tree.addListener(SWT.SetData, this);
1321 tree.addListener(SWT.Expand, this);
1322 tree.addListener(SWT.Dispose, this);
1323 tree.addListener(SWT.Activate, this);
1325 tree.setData(KEY_GRAPH_EXPLORER, this);
1327 // These are both required for performing column resizing without flicker.
1328 // See SWT.Resize event handling in #handleEvent() for more explanations.
1329 parent.addListener(SWT.Resize, this);
1330 tree.addListener(SWT.Resize, this);
1332 originalFont = JFaceResources.getDefaultFontDescriptor();
1333 // originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
1334 // originalForeground = tree.getForeground();
1336 tree.setFont((Font) localResourceManager.get(originalFont));
1338 columns = new Column[] { new Column(ColumnKeys.SINGLE) };
1339 columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
1341 editor = new TreeEditor(tree);
1342 editor.horizontalAlignment = SWT.LEFT;
1343 editor.grabHorizontal = true;
1344 editor.minimumWidth = 50;
1346 setBasicListeners();
1347 setDefaultProcessors();
1349 this.toolTip = new GraphExplorerToolTip(explorerContext, tree);
1353 public IThreadWorkQueue getThread() {
1357 TreeItem previousSingleSelection = null;
1358 long focusGainedAt = Long.MIN_VALUE;
1360 protected GraphExplorerToolTip toolTip;
1362 protected void setBasicListeners() {
1363 // Keep track of the previous single selection to help
1364 // decide whether to start editing a tree node on mouse
1366 tree.addListener(SWT.Selection, event -> {
1367 TreeItem[] selection = tree.getSelection();
1368 if (selection.length == 1) {
1369 //for (TreeItem item : selection)
1370 // System.out.println("selection: " + item);
1371 previousSingleSelection = selection[0];
1373 previousSingleSelection = null;
1377 // Try to start editing of tree column when clicked for the second time.
1378 Listener mouseEditListener = new Listener() {
1380 Future<?> startEdit = null;
1383 public void handleEvent(Event event) {
1384 if (event.type == SWT.DragDetect) {
1385 // Needed to prevent editing from being started when in fact
1386 // the user starts dragging an item.
1387 //System.out.println("DRAG DETECT: " + event);
1391 //System.out.println("mouse down: " + event);
1392 if (event.button == 1) {
1393 // Always ignore the first mouse button press that focuses
1394 // the control. Do not let it start in-line editing since
1395 // that is very annoying to users and not how the UI's that
1396 // people are used to behave.
1397 long eventTime = ((long) event.time) & 0xFFFFFFFFL;
1398 if ((eventTime - focusGainedAt) < 250L) {
1399 //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));
1402 //System.out.println("testing whether to start editing");
1404 final Point point = new Point(event.x, event.y);
1405 final TreeItem item = tree.getItem(point);
1408 //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
1410 // Only start editing if the item was already selected.
1411 if (!item.equals(previousSingleSelection)) {
1416 if (tree.getColumnCount() > 1) {
1417 // TODO: reconsider this logic, might not be good in general.
1418 for (int i = 0; i < tree.getColumnCount(); i++) {
1419 if (item.getBounds(i).contains(point)) {
1420 tryScheduleEdit(event, item, point, 100, i);
1425 //System.out.println("clicks: " + event.count);
1426 if (item.getBounds().contains(point)) {
1427 if (event.count == 1) {
1428 tryScheduleEdit(event, item, point, 500, 0);
1437 void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {
1438 //System.out.println("\tCONTAINS: " + item);
1442 //System.out.println("\tScheduling edit: " + item);
1443 startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
1446 ThreadUtils.asyncExec(thread, new Runnable() {
1449 if (item.isDisposed())
1451 startEditing(item, column, null);
1455 }, delayMs, TimeUnit.MILLISECONDS);
1458 boolean cancelEdit() {
1459 Future<?> f = startEdit;
1461 // Try to cancel the start edit task if it's not running yet.
1464 boolean ret = f.cancel(false);
1465 //System.out.println("\tCancelled edit: " + ret);
1469 //System.out.println("\tNo edit in progress to cancel");
1473 tree.addListener(SWT.MouseDown, mouseEditListener);
1474 tree.addListener(SWT.DragDetect, mouseEditListener);
1475 tree.addListener(SWT.DragDetect, event -> {
1476 Point test = new Point(event.x, event.y);
1477 TreeItem item = tree.getItem(test);
1479 for(int i=0;i<tree.getColumnCount();i++) {
1480 Rectangle rect = item.getBounds(i);
1481 if(rect.contains(test)) {
1482 tree.setData(KEY_DRAG_COLUMN, i);
1487 tree.setData(KEY_DRAG_COLUMN, -1);
1489 tree.addListener(SWT.MouseMove, event -> {
1490 Point test = new Point(event.x, event.y);
1491 TreeItem item = tree.getItem(test);
1493 for(int i=0;i<tree.getColumnCount();i++) {
1494 Rectangle rect = item.getBounds(i);
1495 if(rect.contains(test)) {
1496 transientState.setActiveColumn(i);
1501 transientState.setActiveColumn(null);
1504 // Add focus/mouse/key listeners for supporting the respective
1505 // add/remove listener methods in IGraphExplorer.
1506 tree.addFocusListener(new FocusListener() {
1508 public void focusGained(FocusEvent e) {
1509 focusGainedAt = ((long) e.time) & 0xFFFFFFFFL;
1510 for (FocusListener listener : focusListeners)
1511 listener.focusGained(e);
1514 public void focusLost(FocusEvent e) {
1515 for (FocusListener listener : focusListeners)
1516 listener.focusLost(e);
1519 tree.addMouseListener(new MouseListener() {
1521 public void mouseDoubleClick(MouseEvent e) {
1522 for (MouseListener listener : mouseListeners) {
1523 listener.mouseDoubleClick(e);
1527 public void mouseDown(MouseEvent e) {
1528 for (MouseListener listener : mouseListeners) {
1529 listener.mouseDown(e);
1533 public void mouseUp(MouseEvent e) {
1534 for (MouseListener listener : mouseListeners) {
1535 listener.mouseUp(e);
1539 tree.addKeyListener(new KeyListener() {
1541 public void keyPressed(KeyEvent e) {
1542 for (KeyListener listener : keyListeners) {
1543 listener.keyPressed(e);
1547 public void keyReleased(KeyEvent e) {
1548 for (KeyListener listener : keyListeners) {
1549 listener.keyReleased(e);
1554 OpenStrategy os = new OpenStrategy(tree);
1555 os.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
1556 resetSelectionFromWidget();
1558 os.addPostSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
1559 //System.out.println("OPENSTRATEGY: post selection changed: " + e);
1560 resetSelectionFromWidgetAndFirePostSelection(true);
1563 // This listener takes care of updating the set of currently selected
1564 // TreeItem instances. This set is needed because we need to know in
1565 // #setData(Event) whether the updated item was a part of the current
1566 // selection in which case the selection must be updated.
1567 selectionProvider.addSelectionChangedListener(new ISelectionChangedListener() {
1569 public void selectionChanged(SelectionChangedEvent event) {
1570 //System.out.println("selection changed: " + event.getSelection());
1571 Set<NodeContext> set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);
1572 selectedItems.clear();
1573 for (NodeContext nc : set) {
1574 TreeItem item = contextToItem.getRight(nc);
1576 selectedItems.put(item, nc);
1578 //System.out.println("newly selected items: " + selectedItems);
1584 * @return the new selection if it was different from the old selection in
1585 * {@link #selectionProvider}
1587 private ISelection resetSelectionFromWidget() {
1588 ISelection widgetSelection = getWidgetSelection();
1589 // System.out.println("resetSelection()");
1590 // System.out.println(" provider selection: " + selectionProvider.getSelection());
1591 // System.out.println(" widget selection: " + widgetSelection);
1592 boolean equals = selectionProvider.selectionEquals(widgetSelection);
1593 selectionProvider.setSelectionWithoutFiring(widgetSelection);
1594 return equals ? null : widgetSelection;
1598 * @return the new selection if it was different from the old selection in
1599 * {@link #selectionProvider}
1601 private boolean resetSelectionFromWidgetAndFirePostSelection(boolean force) {
1602 ISelection s = resetSelectionFromWidget();
1603 boolean fire = s != null || force;
1605 //System.out.println("FIRING POST-SELECTION: " + selectionProvider.getSelection());
1606 selectionProvider.firePostSelection(selectionProvider.getSelection());
1611 protected void setDefaultProcessors() {
1612 // Add a simple IMAGER query processor that always returns null.
1613 // With this processor no images will ever be shown.
1614 // setPrimitiveProcessor(new StaticImagerProcessor(null));
1616 setProcessor(new DefaultComparableChildrenProcessor());
1617 setProcessor(new DefaultLabelDecoratorsProcessor());
1618 setProcessor(new DefaultImageDecoratorsProcessor());
1619 setProcessor(new DefaultSelectedLabelerProcessor());
1620 setProcessor(new DefaultLabelerFactoriesProcessor());
1621 setProcessor(new DefaultSelectedImagerProcessor());
1622 setProcessor(new DefaultImagerFactoriesProcessor());
1623 setPrimitiveProcessor(new DefaultLabelerProcessor());
1624 setPrimitiveProcessor(new DefaultCheckedStateProcessor());
1625 setPrimitiveProcessor(new DefaultImagerProcessor());
1626 setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());
1627 setPrimitiveProcessor(new DefaultImageDecoratorProcessor());
1628 setPrimitiveProcessor(new NoSelectionRequestProcessor());
1630 setProcessor(new DefaultFinalChildrenProcessor(this));
1632 setProcessor(new DefaultPrunedChildrenProcessor());
1633 setProcessor(new DefaultSelectedViewpointProcessor());
1634 setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());
1635 setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());
1636 setProcessor(new DefaultViewpointContributionsProcessor());
1638 setPrimitiveProcessor(new DefaultViewpointProcessor());
1639 setPrimitiveProcessor(new DefaultViewpointContributionProcessor());
1640 setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());
1641 setPrimitiveProcessor(new DefaultIsExpandedProcessor());
1642 setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());
1646 public <T> void setProcessor(NodeQueryProcessor<T> processor) {
1647 assertNotDisposed();
1648 if (processor == null)
1649 throw new IllegalArgumentException("null processor");
1651 processors.put(processor.getIdentifier(), processor);
1655 public <T> void setPrimitiveProcessor(PrimitiveQueryProcessor<T> processor) {
1656 assertNotDisposed();
1657 if (processor == null)
1658 throw new IllegalArgumentException("null processor");
1660 PrimitiveQueryProcessor<?> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
1662 if (oldProcessor instanceof ProcessorLifecycle)
1663 ((ProcessorLifecycle) oldProcessor).detached(this);
1664 if (processor instanceof ProcessorLifecycle)
1665 ((ProcessorLifecycle) processor).attached(this);
1669 public <T> void setDataSource(DataSource<T> provider) {
1670 assertNotDisposed();
1671 if (provider == null)
1672 throw new IllegalArgumentException("null provider");
1673 dataSources.put(provider.getProvidedClass(), provider);
1676 @SuppressWarnings("unchecked")
1678 public <T> DataSource<T> removeDataSource(Class<T> forProvidedClass) {
1679 assertNotDisposed();
1680 if (forProvidedClass == null)
1681 throw new IllegalArgumentException("null class");
1682 return dataSources.remove(forProvidedClass);
1686 public void setPersistor(StatePersistor persistor) {
1687 this.persistor = persistor;
1691 public SelectionDataResolver getSelectionDataResolver() {
1692 return selectionDataResolver;
1696 public void setSelectionDataResolver(SelectionDataResolver r) {
1697 this.selectionDataResolver = r;
1701 public SelectionFilter getSelectionFilter() {
1702 return selectionFilter;
1706 public void setSelectionFilter(SelectionFilter f) {
1707 this.selectionFilter = f;
1708 // TODO: re-filter current selection?
1712 public void setSelectionTransformation(BiFunction<GraphExplorer, Object[], Object[]> f) {
1713 this.selectionTransformation = f;
1717 public <T> void addListener(T listener) {
1718 if(listener instanceof FocusListener) {
1719 focusListeners.add((FocusListener)listener);
1720 } else if(listener instanceof MouseListener) {
1721 mouseListeners.add((MouseListener)listener);
1722 } else if(listener instanceof KeyListener) {
1723 keyListeners.add((KeyListener)listener);
1728 public <T> void removeListener(T listener) {
1729 if(listener instanceof FocusListener) {
1730 focusListeners.remove(listener);
1731 } else if(listener instanceof MouseListener) {
1732 mouseListeners.remove(listener);
1733 } else if(listener instanceof KeyListener) {
1734 keyListeners.remove(listener);
1738 public void addSelectionListener(SelectionListener listener) {
1739 tree.addSelectionListener(listener);
1742 public void removeSelectionListener(SelectionListener listener) {
1743 tree.removeSelectionListener(listener);
1746 private Set<String> uiContexts;
1749 public void setUIContexts(Set<String> contexts) {
1750 this.uiContexts = contexts;
1754 public void setRoot(final Object root) {
1755 if(uiContexts != null && uiContexts.size() == 1)
1756 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
1758 setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
1762 public void setRootContext(final NodeContext context) {
1763 setRootContext0(context);
1766 private void setRootContext0(final NodeContext context) {
1767 Assert.isNotNull(context, "root must not be null");
1768 if (isDisposed() || tree.isDisposed())
1770 Display display = tree.getDisplay();
1771 if (display.getThread() == Thread.currentThread()) {
1774 display.asyncExec(new Runnable() {
1783 private void initializeState() {
1784 if (persistor == null)
1786 ExplorerStates.scheduleRead(getRoot(), persistor)
1787 .thenAccept(state -> SWTUtils.asyncExec(tree, () -> restoreState(state)));
1790 private void restoreState(ExplorerState state) {
1791 // topNodeToSet will be processed by #setData when it encounters a
1792 // NodeContext that matches this one.
1793 // topNodePath = state.topNodePath;
1794 // topNodePathChildIndex = state.topNodePathChildIndex;
1795 // currentTopNodePathIndex = 0;
1797 Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1798 if (processor instanceof DefaultIsExpandedProcessor) {
1799 DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
1800 for(NodeContext expanded : state.expandedNodes) {
1801 isExpandedProcessor.replaceExpanded(expanded, true);
1806 private void saveState() {
1807 if (persistor == null)
1810 NodeContext[] topNodePath = NodeContext.NONE;
1811 int[] topNodePathChildIndex = {};
1812 Collection<NodeContext> expandedNodes = Collections.emptyList();
1813 Map<String, Integer> columnWidths = Collections.<String, Integer> emptyMap();
1815 // Resolve top node path
1816 TreeItem topItem = tree.getTopItem();
1817 if (topItem != null) {
1818 NodeContext topNode = (NodeContext) topItem.getData();
1819 if (topNode != null) {
1820 topNodePath = getNodeContextPathSegments(topNode);
1821 topNodePathChildIndex = new int[topNodePath.length];
1822 for (int i = 0; i < topNodePath.length; ++i) {
1823 // TODO: get child indexes
1824 topNodePathChildIndex[i] = 0;
1829 // Resolve expanded nodes
1830 Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
1831 if (processor instanceof IsExpandedProcessor) {
1832 IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;
1833 expandedNodes = isExpandedProcessor.getExpanded();
1837 TreeColumn[] columns = tree.getColumns();
1838 if (columns.length > 1) {
1839 columnWidths = new HashMap<String, Integer>();
1840 for (int i = 0; i < columns.length; ++i) {
1841 columnWidths.put(columns[i].getText(), columns[i].getWidth());
1845 persistor.serialize(
1846 ExplorerStates.explorerStateLocation(),
1848 new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
1852 * Invoke only from SWT thread to reset the root of the graph explorer tree.
1856 private void doSetRoot(NodeContext root) {
1857 if (tree.isDisposed())
1859 if (root.getConstant(BuiltinKeys.INPUT) == null) {
1860 ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));
1864 // Empty caches, release queries.
1865 GraphExplorerContext oldContext = explorerContext;
1866 GraphExplorerContext newContext = new GraphExplorerContext();
1867 GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));
1868 this.explorerContext = newContext;
1869 oldContext.safeDispose();
1870 toolTip.setGraphExplorerContext(explorerContext);
1872 // Need to empty these or otherwise they won't be emptied until the
1873 // explorer is disposed which would mean that many unwanted references
1874 // will be held by this map.
1875 clearPrimitiveProcessors();
1877 this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
1878 : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
1880 explorerContext.getCache().incRef(this.rootContext);
1884 NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
1886 tree.setItemCount(contexts.length);
1888 select(rootContext);
1889 refreshColumnSizes();
1893 public NodeContext getRoot() {
1898 public NodeContext getParentContext(NodeContext context) {
1900 throw new IllegalStateException("disposed");
1901 if (!thread.currentThreadAccess())
1902 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
1904 TreeItem item = contextToItem.getRight(context);
1905 if(item == null) return null;
1906 TreeItem parentItem = item.getParentItem();
1907 if(parentItem == null) return null;
1908 return (NodeContext)parentItem.getData();
1911 Point previousTreeSize;
1912 Point previousTreeParentSize;
1913 boolean activatedBefore = false;
1916 public void handleEvent(Event event) {
1917 //System.out.println("EVENT: " + event);
1918 switch(event.type) {
1920 //System.out.println("EXPAND: " + event.item);
1921 if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
1922 expandVirtual(event);
1924 System.out.println("TODO: non-virtual tree item expand");
1928 // Only invoked for SWT.VIRTUAL trees
1930 // Happened for Hannu once during program startup.
1931 // java.lang.AssertionError
1932 // at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)
1933 // at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)
1934 // at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)
1935 // I do not know whether SWT guarantees that SetData events
1936 // don't come after Dispose event has been issued, but I
1937 // think its better to have this check here just incase.
1942 // This ensures that column sizes are refreshed at
1943 // least once when the GE is first shown.
1944 if (!activatedBefore) {
1945 refreshColumnSizes();
1946 activatedBefore = true;
1950 //new Exception().printStackTrace();
1957 if (event.widget == tree) {
1958 // This case is meant for listening to tree width increase.
1959 // The column resizing must be performed only after the tree
1960 // itself as been resized.
1961 Point size = tree.getSize();
1963 if (previousTreeSize != null) {
1964 dx = size.x - previousTreeSize.x;
1966 previousTreeSize = size;
1967 //System.out.println("RESIZE: " + dx + " - size=" + size);
1970 tree.setRedraw(false);
1971 refreshColumnSizes(size);
1972 tree.setRedraw(true);
1974 } else if (event.widget == tree.getParent()) {
1975 // This case is meant for listening to tree width decrease.
1976 // The columns must be resized before the tree widget itself
1977 // is resized to prevent scroll bar flicker. This can be achieved
1978 // by listening to the resize events of the tree parent widget.
1979 Composite parent = tree.getParent();
1980 Point size = parent.getSize();
1982 // We must subtract the parent's border and possible
1983 // scroll bar width from the new target width of the columns.
1984 size.x -= tree.getParent().getBorderWidth() * 2;
1985 ScrollBar vBar = parent.getVerticalBar();
1986 if (vBar != null && vBar.isVisible())
1987 size.x -= vBar.getSize().x;
1990 if (previousTreeParentSize != null) {
1991 dx = size.x - previousTreeParentSize.x;
1993 previousTreeParentSize = size;
1994 //System.out.println("RESIZE: " + dx + " - size=" + size);
1997 tree.setRedraw(false);
1998 refreshColumnSizes(size);
1999 tree.setRedraw(true);
2009 protected void refreshColumnSizes() {
2010 // Composite treeParent = tree.getParent();
2011 // Point size = treeParent.getSize();
2012 // size.x -= treeParent.getBorderWidth() * 2;
2013 Point size = tree.getSize();
2014 refreshColumnSizes(size);
2015 tree.getParent().layout();
2019 * This has been disabled since the logic of handling column widths has been
2020 * externalized to parties creating {@link GraphExplorerImpl} instances.
2022 protected void refreshColumnSizes(Point toSize) {
2024 refreshingColumnSizes = true;
2026 int columnCount = tree.getColumnCount();
2027 if (columnCount > 0) {
2028 Point size = toSize;
2029 int targetWidth = size.x - tree.getBorderWidth() * 2;
2032 // Take the vertical scroll bar existence into to account when
2033 // calculating the overflow column width.
2034 ScrollBar vBar = tree.getVerticalBar();
2035 //if (vBar != null && vBar.isVisible())
2037 targetWidth -= vBar.getSize().x;
2039 List<TreeColumn> resizing = new ArrayList<TreeColumn>();
2041 int resizingWidth = 0;
2042 int totalWeight = 0;
2043 for (int i = 0; i < columnCount - 1; ++i) {
2044 TreeColumn col = tree.getColumn(i);
2045 //System.out.println(" " + col.getText() + ": " + col.getWidth());
2046 int width = col.getWidth();
2048 Column c = (Column) col.getData();
2051 resizingWidth += width;
2052 totalWeight += c.getWeight();
2056 int requiredWidthAdjustment = targetWidth - usedWidth;
2057 if (requiredWidthAdjustment < 0)
2058 requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());
2059 double diff = requiredWidthAdjustment;
2060 //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);
2062 // Decide how much to give space to / take space from each grabbing column
2063 double wrel = 1.0 / resizing.size();
2065 double[] weightedShares = new double[resizing.size()];
2066 for (int i = 0; i < resizing.size(); ++i) {
2067 TreeColumn col = resizing.get(i);
2068 Column c = (Column) col.getData();
2069 if (totalWeight == 0) {
2070 weightedShares[i] = wrel;
2072 weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
2075 //System.out.println("grabbing columns:" + resizing);
2076 //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
2078 // Always shrink the columns if necessary, but don't enlarge before
2079 // there is sufficient space to at least give all resizable columns
2081 if (diff < 0 || (diff > 0 && diff > resizing.size())) {
2082 // Need to either shrink or enlarge the resizable columns if possible.
2083 for (int i = 0; i < resizing.size(); ++i) {
2084 TreeColumn col = resizing.get(i);
2085 Column c = (Column) col.getData();
2086 int cw = col.getWidth();
2087 //double wrel = (double) cw / (double) resizingWidth;
2088 //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);
2089 double ddelta = weightedShares[i] * diff;
2092 delta = (int) Math.floor(ddelta);
2094 delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
2096 //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);
2097 //System.out.println("argh(" + col.getText() + "): " + c.getWidth() + " vs. " + col.getWidth() + " vs. " + (cw+delta));
2098 int newWidth = Math.max(c.getWidth(), cw + delta);
2099 requiredWidthAdjustment -= (newWidth - cw);
2100 col.setWidth(newWidth);
2104 //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
2106 TreeColumn last = tree.getColumn(columnCount - 1);
2107 // HACK: see #setColumns for why this is here.
2108 if (FILLER.equals(last.getText())) {
2109 last.setWidth(Math.max(0, requiredWidthAdjustment));
2113 refreshingColumnSizes = false;
2118 private void doDispose() {
2119 explorerContext.dispose();
2121 // No longer necessary, the used executors are shared.
2122 //scheduler.shutdown();
2123 //scheduler2.shutdown();
2126 detachPrimitiveProcessors();
2127 primitiveProcessors.clear();
2128 dataSources.clear();
2130 pendingItems.clear();
2134 contextToItem.clear();
2136 mouseListeners.clear();
2138 selectionProvider.clearListeners();
2139 selectionProvider = null;
2140 selectionDataResolver = null;
2141 selectionRefreshContexts.clear();
2142 selectedItems.clear();
2143 originalFont = null;
2145 localResourceManager.dispose();
2147 // Must shutdown image loader job before disposing its ResourceManager
2148 imageLoaderJob.dispose();
2149 imageLoaderJob.cancel();
2151 imageLoaderJob.join();
2152 } catch (InterruptedException e) {
2153 ErrorLogger.defaultLogError(e);
2155 resourceManager.dispose();
2157 postSelectionProvider.dispose();
2161 private void expandVirtual(final Event event) {
2162 TreeItem item = (TreeItem) event.item;
2163 assert (item != null);
2164 NodeContext context = (NodeContext) item.getData();
2165 assert (context != null);
2167 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));
2168 NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);
2169 int maxChildren = getMaxChildren(manager, context);
2170 item.setItemCount(children.length < maxChildren ? children.length : maxChildren);
2173 private NodeContext getNodeContext(TreeItem item) {
2174 assert(item != null);
2176 NodeContext context = (NodeContext)item.getData();
2177 assert(context != null);
2182 private NodeContext getParentContext(TreeItem item) {
2183 TreeItem parentItem = item.getParentItem();
2184 if(parentItem != null) {
2185 return getNodeContext(parentItem);
2191 private static final String LISTENER_SET_INDICATOR = "LSI";
2192 private static final String PENDING = "PENDING";
2193 private int contextSelectionChangeModCount = 0;
2196 * Only invoked for SWT.VIRTUAL widgets.
2200 private void setData(final Event event) {
2201 assert (event != null);
2202 TreeItem item = (TreeItem) event.item;
2203 assert (item != null);
2205 // Based on experience it seems to be possible that
2206 // SetData events are sent for disposed TreeItems.
2207 if (item.isDisposed() || item.getData(PENDING) != null)
2210 //System.out.println("GE.SetData " + item);
2212 GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
2214 NodeContext parentContext = getParentContext(item);
2215 assert (parentContext != null);
2217 NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2219 // Some children have disappeared since counting
2220 if (event.index < 0) {
2221 ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());
2224 if (event.index >= parentChildren.length)
2227 NodeContext context = parentChildren[event.index];
2228 assert (context != null);
2229 item.setData(context);
2231 // Manage NodeContext -> TreeItem mappings
2232 contextToItem.map(context, item);
2233 if (item.getData(LISTENER_SET_INDICATOR) == null) {
2234 // This "if" exists because setData will get called many
2235 // times for the same (NodeContext, TreeItem) pairs.
2236 // Each TreeItem only needs one listener, but this
2237 // is needed to tell whether it already has a listener
2239 item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
2240 item.addListener(SWT.Dispose, itemDisposeListener);
2243 boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
2245 PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);
2246 int maxChildren = getMaxChildren(manager, context);
2247 //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);
2249 NodeContext[] pruned = children.getPrunedChildren();
2250 int count = Math.min(pruned.length, maxChildren);
2252 if (isExpanded || item.getItemCount() > 1) {
2253 item.setItemCount(count);
2254 TreeItem[] childItems = item.getItems();
2255 for(int i=0;i<count;i++)
2256 contextToItem.map(pruned[i], childItems[i]);
2258 if (children.getPrunedChildren().length == 0) {
2259 item.setItemCount(0);
2261 // item.setItemCount(1);
2262 item.setItemCount(count);
2263 TreeItem[] childItems = item.getItems();
2264 for(int i=0;i<count;i++)
2265 contextToItem.map(pruned[i], childItems[i]);
2266 // item.getItem(0).setData(PENDING, PENDING);
2267 // item.getItem(0).setItemCount(o);
2271 setTextAndImage(item, manager, context, event.index);
2273 // Check if the node should be auto-expanded?
2274 if ((autoExpandLevel == ALL_LEVELS || autoExpandLevel > 1) && !isExpanded) {
2275 //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");
2276 int level = getTreeItemLevel(item);
2277 if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)
2278 && !explorerContext.autoExpanded.containsKey(context))
2280 //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
2281 explorerContext.autoExpanded.put(context, Boolean.TRUE);
2282 setExpanded(context, true);
2286 item.setExpanded(isExpanded);
2288 if ((tree.getStyle() & SWT.CHECK) != 0) {
2289 CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);
2290 item.setChecked(CheckedState.CHECKED_STATES.contains(checked));
2291 item.setGrayed(CheckedState.GRAYED == checked);
2294 //System.out.println("GE.SetData completed " + item);
2296 // This test makes sure that selectionProvider holds the correct
2297 // selection with respect to the actual selection stored by the virtual
2299 // The data items shown below the items occupied by the selected and now removed data
2300 // will be squeezed to use the tree items previously used for the now
2301 // removed data. When this happens, the NodeContext items stored by the
2302 // tree items will be different from what the GraphExplorer's
2303 // ISelectionProvider thinks the selection currently is. To compensate,
2304 // 1. Recognize the situation
2305 // 2. ASAP set the selection provider selection to what is actually
2306 // offered by the tree widget.
2307 NodeContext selectedContext = selectedItems.get(item);
2308 // System.out.println("selectedContext(" + item + "): " + selectedContext);
2309 if (selectedContext != null && !selectedContext.equals(context)) {
2310 final int modCount = ++contextSelectionChangeModCount;
2311 // System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);
2312 // System.out.println(" old context: " + selectedContext);
2313 // System.out.println(" new context: " + context);
2314 // System.out.println(" provider selection: " + selectionProvider.getSelection());
2315 // System.out.println(" widget selection: " + getWidgetSelection());
2316 ThreadUtils.asyncExec(thread, new Runnable() {
2321 int count = contextSelectionChangeModCount;
2322 // System.out.println("MODCOUNT: " + modCount + " vs. " + count);
2323 if (modCount != count)
2325 resetSelectionFromWidgetAndFirePostSelection(false);
2330 // This must be done to keep the visible tree selection properly
2331 // in sync with the selectionProvider JFace proxy of this class in
2332 // cases where an in-line editor was previously active for the node
2334 if (selectionRefreshContexts.remove(context)) {
2335 final ISelection currentSelection = selectionProvider.getSelection();
2336 // asyncExec is here to prevent ui glitches that
2337 // seem to occur if the selection setting is done
2338 // directly here in between setData invocations.
2339 ThreadUtils.asyncExec(thread, new Runnable() {
2344 // System.out.println("REFRESHING SELECTION: " + currentSelection);
2345 // System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));
2346 // System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());
2347 setSelection(currentSelection, true);
2348 // System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));
2349 // System.out.println("AFTER setSelection: " + selectionProvider.getSelection());
2354 // TODO: doesn't work if any part of the node path that should be
2355 // revealed is out of view.
2356 // Disabled until a better solution is devised.
2357 // Suggestion: include item indexes into the stored node context path
2358 // to make it possible for this method to know whether the current
2359 // node path segment is currently out of view based on event.index.
2360 // If out of view, this code needs to scroll the view programmatically
2362 // if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {
2363 // NodeContext topNode = topNodePath[currentTopNodePathIndex];
2364 // if (topNode.equals(context)) {
2365 // final TreeItem topItem = item;
2366 // ++currentTopNodePathIndex;
2367 // if (currentTopNodePathIndex >= topNodePath.length) {
2368 // // Mission accomplished. End search for top node here.
2369 // topNodePath = NodeContext.NONE;
2370 // currentTopNodePathIndex = -1;
2372 // ThreadUtils.asyncExec(thread, new Runnable() {
2374 // public void run() {
2375 // if (isDisposed())
2377 // tree.setTopItem(topItem);
2383 // Check if vertical scroll bar has become visible and refresh layout.
2384 ScrollBar verticalBar = tree.getVerticalBar();
2385 if(verticalBar != null) {
2386 boolean currentlyVerticalBarVisible = verticalBar.isVisible();
2387 if (verticalBarVisible != currentlyVerticalBarVisible) {
2388 verticalBarVisible = currentlyVerticalBarVisible;
2389 Composite parent = tree.getParent();
2397 * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the
2398 * return value is calculated. Items without parents have level=2,
2399 * their children level=3, etc. Returns 0 for invalid items
2401 private int getTreeItemLevel(TreeItem item) {
2405 for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);
2406 //System.out.println("\tgetTreeItemLevel(" + parent + ")");
2407 //System.out.println("level(" + item + "): " + level);
2415 private NodeContext[] getNodeContextPathSegments(NodeContext node) {
2416 TreeItem item = contextToItem.getRight(node);
2418 return NodeContext.NONE;
2419 int level = getTreeItemLevel(item);
2421 return NodeContext.NONE;
2422 // Exclude root from the saved node path.
2424 NodeContext[] segments = new NodeContext[level];
2425 for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {
2426 NodeContext ctx = (NodeContext) item.getData();
2428 return NodeContext.NONE;
2429 segments[level-1] = ctx;
2438 @SuppressWarnings("unused")
2439 private NodeContextPath getNodeContextPath(NodeContext node) {
2440 NodeContext[] path = getNodeContextPathSegments(node);
2441 return new NodeContextPath(path);
2444 void setImage(NodeContext node, TreeItem item, Imager imager, Collection<ImageDecorator> decorators, int itemIndex) {
2445 Image[] images = columnImageArray;
2446 Arrays.fill(images, null);
2447 if (imager == null) {
2448 item.setImage(images);
2452 Object[] descOrImage = columnDescOrImageArray;
2453 Arrays.fill(descOrImage, null);
2454 boolean finishLoadingInJob = false;
2456 for (Column column : columns) {
2457 String key = column.getKey();
2458 ImageDescriptor desc = imager.getImage(key);
2460 // Attempt to decorate the label
2461 if (!decorators.isEmpty()) {
2462 for (ImageDecorator id : decorators) {
2463 ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);
2469 // Try resolving only cached images here and now
2470 Object img = localResourceManager.find(desc);
2472 img = resourceManager.find(desc);
2474 images[index] = img != null ? (Image) img : null;
2475 descOrImage[index] = img == null ? desc : img;
2476 finishLoadingInJob |= img == null;
2481 // Finish loading the final image in the image loader job if necessary.
2482 if (finishLoadingInJob) {
2483 // Prevent UI from flashing unnecessarily by reusing the old image
2484 // in the item if it exists.
2485 for (int c = 0; c < columns.length; ++c) {
2486 Image img = item.getImage(c);
2490 item.setImage(images);
2492 // Schedule loading to another thread to refrain from blocking
2493 // the UI with database operations.
2494 queueImageTask(item, new ImageTask(
2497 Arrays.copyOf(descOrImage, descOrImage.length)));
2499 // Set any images that were resolved.
2500 item.setImage(images);
2504 private void queueImageTask(TreeItem item, ImageTask task) {
2505 synchronized (imageTasks) {
2506 imageTasks.put(item, task);
2508 imageLoaderJob.scheduleIfNecessary(100);
2512 * Invoked in a job worker thread.
2515 * @see ImageLoaderJob
2518 protected IStatus setPendingImages(IProgressMonitor monitor) {
2519 ImageTask[] tasks = null;
2520 synchronized (imageTasks) {
2521 tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);
2524 if (tasks.length == 0)
2525 return Status.OK_STATUS;
2527 MultiStatus status = null;
2529 // Load missing images
2530 for (ImageTask task : tasks) {
2531 Object[] descs = task.descsOrImages;
2532 for (int i = 0; i < descs.length; ++i) {
2533 Object obj = descs[i];
2534 if (obj instanceof ImageDescriptor) {
2535 ImageDescriptor desc = (ImageDescriptor) obj;
2537 descs[i] = resourceManager.get((ImageDescriptor) desc);
2538 } catch (DeviceResourceException e) {
2540 status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);
2541 status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));
2547 // Perform final UI updates in the UI thread.
2548 final ImageTask[] _tasks = tasks;
2549 thread.asyncExec(new Runnable() {
2552 if (!tree.isDisposed()) {
2553 tree.setRedraw(false);
2555 tree.setRedraw(true);
2560 return status != null ? status : Status.OK_STATUS;
2564 * Invoked in the UI thread only.
2568 void setImages(ImageTask[] tasks) {
2569 for (ImageTask task : tasks)
2575 * Invoked in the UI thread only.
2579 void setImage(ImageTask task) {
2580 // Be sure not to process disposed items.
2581 if (task.item.isDisposed())
2583 // Discard this task if the TreeItem has switched owning NodeContext.
2584 if (!contextToItem.contains(task.node, task.item))
2587 Object[] descs = task.descsOrImages;
2588 Image[] images = columnImageArray;
2589 Arrays.fill(images, null);
2590 for (int i = 0; i < descs.length; ++i) {
2591 Object desc = descs[i];
2592 if (desc instanceof Image) {
2593 images[i] = (Image) desc;
2596 task.item.setImage(images);
2599 void setText(TreeItem item, Labeler labeler, Collection<LabelDecorator> decorators, int itemIndex) {
2600 if (labeler != null) {
2601 String[] texts = new String[columns.length];
2603 Map<String, String> labels = labeler.getLabels();
2604 Map<String, String> runtimeLabels = labeler.getRuntimeLabels();
2605 for (Column column : columns) {
2606 String key = column.getKey();
2608 if (runtimeLabels != null) s = runtimeLabels.get(key);
2609 if (s == null) s = labels.get(key);
2611 FontDescriptor font = originalFont;
2612 ColorDescriptor bg = originalBackground;
2613 ColorDescriptor fg = originalForeground;
2615 // Attempt to decorate the label
2616 if (!decorators.isEmpty()) {
2617 for (LabelDecorator ld : decorators) {
2618 String ds = ld.decorateLabel(s, key, itemIndex);
2622 FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
2626 ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
2630 ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
2636 if (font != originalFont) {
2637 //System.out.println("set font: " + index + ": " + font);
2638 item.setFont(index, (Font) localResourceManager.get(font));
2640 if (bg != originalBackground)
2641 item.setBackground(index, (Color) localResourceManager.get(bg));
2642 if (fg != originalForeground)
2643 item.setForeground(index, (Color) localResourceManager.get(fg));
2649 item.setText(texts);
2651 item.setText(Labeler.NO_LABEL);
2655 void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {
2656 Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
2657 if (labeler != null) {
2658 labeler.setListener(labelListener);
2660 Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);
2661 Collection<LabelDecorator> labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);
2662 Collection<ImageDecorator> imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);
2664 setText(item, labeler, labelDecorators, itemIndex);
2665 setImage(context, item, imager, imageDecorators, itemIndex);
2669 public void setFocus() {
2674 public <T> T query(NodeContext context, CacheKey<T> key) {
2675 return this.explorerContext.cache.get(context, key);
2679 public boolean isDisposed() {
2683 protected void assertNotDisposed() {
2685 throw new IllegalStateException("disposed");
2692 * @param forceControlUpdate
2695 public void setSelection(final ISelection selection, boolean forceControlUpdate) {
2696 assertNotDisposed();
2697 boolean equalsOld = selectionProvider.selectionEquals(selection);
2698 if (equalsOld && !forceControlUpdate) {
2699 // Just set the selection object instance, fire no events nor update
2700 // the viewer selection.
2701 selectionProvider.setSelection(selection);
2703 // Schedule viewer and selection update if necessary.
2704 if (tree.isDisposed())
2706 Display d = tree.getDisplay();
2707 if (d.getThread() == Thread.currentThread()) {
2708 updateSelectionToControl(selection);
2710 d.asyncExec(new Runnable() {
2713 if (tree.isDisposed())
2715 updateSelectionToControl(selection);
2723 /* Contains the best currently found tree item and its priority
2725 private static class SelectionResolutionStatus {
2726 int bestPriority = Integer.MAX_VALUE;
2734 private void updateSelectionToControl(ISelection selection) {
2735 if (selectionDataResolver == null)
2737 if (!(selection instanceof IStructuredSelection))
2740 // Initialize selection resolution status map
2741 IStructuredSelection iss = (IStructuredSelection) selection;
2742 final THashMap<Object,SelectionResolutionStatus> statusMap =
2743 new THashMap<Object,SelectionResolutionStatus>(iss.size());
2744 for(Iterator<?> it = iss.iterator(); it.hasNext();) {
2745 Object selectionElement = it.next();
2746 Object resolvedElement = selectionDataResolver.resolve(selectionElement);
2749 new SelectionResolutionStatus());
2752 // Iterate all tree items and try to match them to the selection
2753 iterateTreeItems(new TObjectProcedure<TreeItem>() {
2755 public boolean execute(TreeItem treeItem) {
2756 NodeContext nodeContext = (NodeContext)treeItem.getData();
2757 if(nodeContext == null)
2759 SelectionResolutionStatus status = statusMap.get(nodeContext);
2760 if(status != null) {
2761 status.bestPriority = 0; // best possible match
2762 status.bestItem = treeItem;
2766 Object input = nodeContext.getConstant(BuiltinKeys.INPUT);
2767 status = statusMap.get(input);
2768 if(status != null) {
2769 NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);
2770 int curPriority = nodeType instanceof EntityNodeType
2771 ? 1 // Prefer EntityNodeType matches to other node types
2773 if(curPriority < status.bestPriority) {
2774 status.bestPriority = curPriority;
2775 status.bestItem = treeItem;
2783 ArrayList<TreeItem> items = new ArrayList<TreeItem>(statusMap.size());
2784 for(SelectionResolutionStatus status : statusMap.values())
2785 if(status.bestItem != null)
2786 items.add(status.bestItem);
2787 select(items.toArray(new TreeItem[items.size()]));
2793 public ISelection getWidgetSelection() {
2794 TreeItem[] items = tree.getSelection();
2795 if (items.length == 0)
2796 return StructuredSelection.EMPTY;
2798 List<NodeContext> nodes = new ArrayList<NodeContext>(items.length);
2800 // Caches for resolving node contexts the hard way if necessary.
2801 GENodeQueryManager manager = null;
2802 NodeContext lastParentContext = null;
2803 NodeContext[] lastChildren = null;
2805 for (int i = 0; i < items.length; i++) {
2806 TreeItem item = items[i];
2807 NodeContext ctx = (NodeContext) item.getData();
2808 // It may happen due to the virtual nature of the tree control
2809 // that it contains TreeItems which have not yet been ran through
2814 TreeItem parentItem = item.getParentItem();
2815 NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;
2816 if (parentContext != null) {
2817 NodeContext[] children = lastChildren;
2818 if (parentContext != lastParentContext) {
2819 if (manager == null)
2820 manager = new GENodeQueryManager(this.explorerContext, null, null, null);
2821 lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
2822 lastParentContext = parentContext;
2824 int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);
2825 if (index >= 0 && index < children.length) {
2826 NodeContext child = children[index];
2827 if (child != null) {
2829 // Cache NodeContext in TreeItem for faster access
2830 item.setData(child);
2836 //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
2837 ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
2842 public TransientExplorerState getTransientState() {
2843 if (!thread.currentThreadAccess())
2844 throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());
2845 return transientState;
2852 private void select(TreeItem item) {
2853 tree.setSelection(item);
2854 tree.showSelection();
2855 selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
2862 private void select(TreeItem[] items) {
2863 //System.out.println("Select: " + Arrays.toString(items));
2864 tree.setSelection(items);
2865 tree.showSelection();
2866 NodeContext[] data = new NodeContext[items.length];
2867 for (int i = 0; i < data.length; i++) {
2868 data[i] = (NodeContext) items[i].getData();
2870 selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
2873 private void iterateTreeItems(TObjectProcedure<TreeItem> procedure) {
2874 for(TreeItem item : tree.getItems())
2875 if(!iterateTreeItems(item, procedure))
2879 private boolean iterateTreeItems(TreeItem item,
2880 TObjectProcedure<TreeItem> procedure) {
2881 if(!procedure.execute(item))
2883 if(item.getExpanded())
2884 for(TreeItem child : item.getItems())
2885 if(!iterateTreeItems(child, procedure))
2895 private boolean trySelect(TreeItem item, Object input) {
2896 NodeContext itemCtx = (NodeContext) item.getData();
2897 if (itemCtx != null) {
2898 if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {
2903 if (item.getExpanded()) {
2904 for (TreeItem child : item.getItems()) {
2905 if (trySelect(child, input))
2912 private boolean equalsEnough(NodeContext c1, NodeContext c2) {
2914 Object input1 = c1.getConstant(BuiltinKeys.INPUT);
2915 Object input2 = c2.getConstant(BuiltinKeys.INPUT);
2916 if(!ObjectUtils.objectEquals(input1, input2))
2919 Object type1 = c1.getConstant(NodeType.TYPE);
2920 Object type2 = c2.getConstant(NodeType.TYPE);
2921 if(!ObjectUtils.objectEquals(type1, type2))
2928 private NodeContext tryFind(NodeContext context) {
2929 for (TreeItem item : tree.getItems()) {
2930 NodeContext found = tryFind(item, context);
2931 if(found != null) return found;
2936 private NodeContext tryFind(TreeItem item, NodeContext context) {
2937 NodeContext itemCtx = (NodeContext) item.getData();
2938 if (itemCtx != null) {
2939 if (equalsEnough(context, itemCtx)) {
2943 if (item.getExpanded()) {
2944 for (TreeItem child : item.getItems()) {
2945 NodeContext found = tryFind(child, context);
2946 if(found != null) return found;
2953 public boolean select(NodeContext context) {
2955 assertNotDisposed();
2957 if (context == null || context.equals(rootContext)) {
2959 selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
2963 // if (context.equals(rootContext)) {
2964 // tree.deselectAll();
2965 // selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
2969 Object input = context.getConstant(BuiltinKeys.INPUT);
2971 for (TreeItem item : tree.getItems()) {
2972 if (trySelect(item, input))
2980 private NodeContext tryFind2(NodeContext context) {
2981 Set<NodeContext> ctxs = contextToItem.getLeftSet();
2982 for(NodeContext c : ctxs)
2983 if(equalsEnough(c, context))
2988 private boolean waitVisible(NodeContext parent, NodeContext context) {
2989 long start = System.nanoTime();
2991 TreeItem parentItem = contextToItem.getRight(parent);
2993 if(parentItem == null)
2997 NodeContext target = tryFind2(context);
2998 if(target != null) {
2999 TreeItem item = contextToItem.getRight(target);
3000 if (!(item.getParentItem().equals(parentItem)))
3002 tree.setTopItem(item);
3006 Display.getCurrent().readAndDispatch();
3007 long duration = System.nanoTime() - start;
3013 private boolean selectPathInternal(NodeContext[] contexts, int position) {
3014 //System.out.println("NodeContext path : " + contexts);
3016 NodeContext head = tryFind(contexts[position]);
3018 if(position == contexts.length-1) {
3019 return select(head);
3023 //setExpanded(head, true);
3025 if(!waitVisible(head, contexts[position+1]))
3028 setExpanded(head, true);
3030 return selectPathInternal(contexts, position+1);
3035 public boolean selectPath(Collection<NodeContext> contexts) {
3037 if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");
3038 if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");
3040 return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
3045 public boolean isVisible(NodeContext context) {
3047 for (TreeItem item : tree.getItems()) {
3048 NodeContext found = tryFind(item, context);
3057 protected ISelection constructSelection(NodeContext... contexts) {
3058 if (contexts == null)
3059 throw new IllegalArgumentException("null contexts");
3060 if (contexts.length == 0)
3061 return StructuredSelection.EMPTY;
3062 if (selectionFilter == null)
3063 return new StructuredSelection(transformSelection(contexts));
3064 return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );
3067 protected Object[] transformSelection(Object[] objects) {
3068 return selectionTransformation.apply(this, objects);
3071 protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
3072 int len = contexts.length;
3073 Object[] objects = new Object[len];
3074 for (int i = 0; i < len; ++i)
3075 objects[i] = filter.filter(contexts[i]);
3080 public void setExpanded(final NodeContext context, final boolean expanded) {
3081 assertNotDisposed();
3082 ThreadUtils.asyncExec(thread, new Runnable() {
3086 doSetExpanded(context, expanded);
3091 private void doSetExpanded(NodeContext context, boolean expanded) {
3092 //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
3093 TreeItem item = contextToItem.getRight(context);
3095 item.setExpanded(expanded);
3097 PrimitiveQueryProcessor<?> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
3098 if (pqp instanceof IsExpandedProcessor) {
3099 IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
3100 iep.replaceExpanded(context, expanded);
3105 public void setColumnsVisible(boolean visible) {
3106 columnsAreVisible = visible;
3107 if(tree != null) tree.setHeaderVisible(columnsAreVisible);
3111 public void setColumns(final Column[] columns) {
3112 setColumns(columns, null);
3116 public void setColumns(final Column[] columns, Consumer<Map<Column, Object>> callback) {
3117 assertNotDisposed();
3118 checkUniqueColumnKeys(columns);
3120 Display d = tree.getDisplay();
3121 if (d.getThread() == Thread.currentThread())
3122 doSetColumns(columns, callback);
3125 if (tree.isDisposed())
3127 doSetColumns(columns, callback);
3131 private void checkUniqueColumnKeys(Column[] cols) {
3132 Set<String> usedColumnKeys = new HashSet<String>();
3133 List<Column> duplicateColumns = new ArrayList<Column>();
3134 for (Column c : cols) {
3135 if (!usedColumnKeys.add(c.getKey()))
3136 duplicateColumns.add(c);
3138 if (!duplicateColumns.isEmpty()) {
3139 throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
3144 * Only meant to be invoked from the SWT UI thread.
3148 private void doSetColumns(Column[] cols, Consumer<Map<Column, Object>> callback) {
3149 // Attempt to keep previous column widths.
3150 Map<String, Integer> prevWidths = new HashMap<>();
3151 for (TreeColumn column : tree.getColumns()) {
3152 Column c = (Column) column.getData();
3154 prevWidths.put(c.getKey(), column.getWidth());
3159 HashMap<String, Integer> keyToIndex = new HashMap<>();
3160 for (int i = 0; i < cols.length; ++i) {
3161 keyToIndex.put(cols[i].getKey(), i);
3164 this.columns = Arrays.copyOf(cols, cols.length);
3165 //this.columns[cols.length] = FILLER_COLUMN;
3166 this.columnKeyToIndex = keyToIndex;
3167 this.columnImageArray = new Image[cols.length];
3168 this.columnDescOrImageArray = new Object[cols.length];
3170 Map<Column, Object> map = new HashMap<>();
3172 tree.setHeaderVisible(columnsAreVisible);
3173 for (Column column : columns) {
3174 TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));
3177 c.setText(column.getLabel());
3178 c.setToolTipText(column.getTooltip());
3180 int cw = column.getWidth();
3182 // Try to keep previous widths
3183 Integer w = prevWidths.get(column.getKey());
3186 else if (cw != Column.DEFAULT_CONTROL_WIDTH)
3189 // Go for some kind of default settings then...
3190 if (ColumnKeys.PROPERTY.equals(column.getKey()))
3196 // if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
3197 // c.addListener(SWT.Resize, resizeListener);
3198 // c.setResizable(true);
3200 // //c.setResizable(false);
3205 if(callback != null) callback.accept(map);
3207 // Make sure the explorer fits the columns properly after initialization.
3208 SWTUtils.asyncExec(tree, () -> {
3209 if (!tree.isDisposed())
3210 refreshColumnSizes();
3214 int toSWT(Align alignment) {
3215 switch (alignment) {
3216 case LEFT: return SWT.LEFT;
3217 case CENTER: return SWT.CENTER;
3218 case RIGHT: return SWT.RIGHT;
3219 default: throw new Error("unhandled alignment: " + alignment);
3224 public Column[] getColumns() {
3225 return Arrays.copyOf(columns, columns.length);
3228 private void detachPrimitiveProcessors() {
3229 for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3230 if (p instanceof ProcessorLifecycle) {
3231 ((ProcessorLifecycle) p).detached(this);
3236 private void clearPrimitiveProcessors() {
3237 for (PrimitiveQueryProcessor<?> p : primitiveProcessors.values()) {
3238 if (p instanceof ProcessorLifecycle) {
3239 ((ProcessorLifecycle) p).clear();
3244 Listener resizeListener = new Listener() {
3246 public void handleEvent(Event event) {
3247 // Prevent infinite recursion.
3248 if (refreshingColumnSizes)
3250 //TreeColumn column = (TreeColumn) event.widget;
3251 //Column c = (Column) column.getData();
3252 refreshColumnSizes();
3256 Listener itemDisposeListener = new Listener() {
3258 public void handleEvent(Event event) {
3259 if (event.type == SWT.Dispose) {
3260 if (event.widget instanceof TreeItem) {
3261 TreeItem ti = (TreeItem) event.widget;
3262 //NodeContext ctx = (NodeContext) ti.getData();
3263 // System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));
3264 // System.out.println(" map size BEFORE: " + contextToItem.size());
3265 @SuppressWarnings("unused")
3266 NodeContext removed = contextToItem.removeWithRight(ti);
3267 // System.out.println(" REMOVED: " + removed);
3268 // System.out.println(" map size AFTER: " + contextToItem.size());
3277 LabelerListener labelListener = new LabelerListener() {
3279 public boolean columnModified(final NodeContext context, final String key, final String newLabel) {
3280 //System.out.println("column " + key + " modified for " + context + " to " + newLabel);
3281 if (tree.isDisposed())
3284 synchronized (labelRefreshRunnables) {
3285 Runnable refresher = new Runnable() {
3288 // Tree is guaranteed to be non-disposed if this is invoked.
3290 // contextToItem should be accessed only in the SWT thread to keep things thread-safe.
3291 final TreeItem item = contextToItem.getRight(context);
3292 if (item == null || item.isDisposed())
3295 final Integer index = columnKeyToIndex.get(key);
3299 //System.out.println(" found index: " + index);
3300 //System.out.println(" found item: " + item);
3302 GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
3304 // FIXME: indexOf is quadratic
3306 TreeItem parentItem = item.getParentItem();
3307 if (parentItem == null) {
3308 itemIndex = tree.indexOf(item);
3309 //tree.clear(parentIndex, false);
3311 itemIndex = parentItem.indexOf(item);
3312 //item.clear(parentIndex, false);
3314 setTextAndImage(item, manager, context, itemIndex);
3315 } catch (SWTException e) {
3316 ErrorLogger.defaultLogError(e);
3320 //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
3321 labelRefreshRunnables.put(context, refresher);
3323 if (!refreshIsQueued) {
3324 refreshIsQueued = true;
3326 long now = System.currentTimeMillis();
3327 long elapsed = now - lastLabelRefreshScheduled;
3328 if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)
3329 delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;
3330 //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");
3332 ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
3335 scheduleImmediateLabelRefresh();
3337 }, delay, TimeUnit.MILLISECONDS);
3339 scheduleImmediateLabelRefresh();
3341 lastLabelRefreshScheduled = now;
3348 public boolean columnsModified(final NodeContext context, final Map<String, String> columns) {
3349 System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
3354 private void scheduleImmediateLabelRefresh() {
3355 Runnable[] runnables = null;
3356 synchronized (labelRefreshRunnables) {
3357 if (labelRefreshRunnables.isEmpty())
3360 runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
3361 labelRefreshRunnables.clear();
3362 refreshIsQueued = false;
3364 final Runnable[] rs = runnables;
3366 if (tree.isDisposed())
3368 tree.getDisplay().asyncExec(new Runnable() {
3371 if (tree.isDisposed())
3373 //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");
3374 tree.setRedraw(false);
3375 for (Runnable r : rs) {
3378 tree.setRedraw(true);
3383 long lastLabelRefreshScheduled = 0;
3384 boolean refreshIsQueued = false;
3385 Map<NodeContext, Runnable> labelRefreshRunnables = new HashMap<NodeContext, Runnable>();
3387 @SuppressWarnings("unchecked")
3389 public <T> T getAdapter(Class<T> adapter) {
3390 if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;
3391 else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;
3395 @SuppressWarnings("unchecked")
3397 public <T> T getControl() {
3402 * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
3405 public void setAutoExpandLevel(int level) {
3406 this.autoExpandLevel = level;
3410 public <T> NodeQueryProcessor<T> getProcessor(QueryKey<T> key) {
3411 return explorerContext.getProcessor(key);
3415 public <T> PrimitiveQueryProcessor<T> getPrimitiveProcessor(PrimitiveQueryKey<T> key) {
3416 return explorerContext.getPrimitiveProcessor(key);
3420 public boolean isEditable() {
3425 public void setEditable(boolean editable) {
3426 if (!thread.currentThreadAccess())
3427 throw new IllegalStateException("not in SWT display thread " + thread.getThread());
3429 this.editable = editable;
3430 Display display = tree.getDisplay();
3431 tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
3435 * For setting a more local service locator for the explorer than the global
3436 * workbench service locator. Sometimes required to give this implementation
3437 * access to local workbench services like IFocusService.
3440 * Must be invoked during right after construction.
3442 * @param serviceLocator
3443 * a specific service locator or <code>null</code> to use the
3444 * workbench global service locator
3446 public void setServiceLocator(IServiceLocator serviceLocator) {
3447 if (serviceLocator == null && PlatformUI.isWorkbenchRunning())
3448 serviceLocator = PlatformUI.getWorkbench();
3449 this.serviceLocator = serviceLocator;
3450 if (serviceLocator != null) {
3451 this.contextService = (IContextService) serviceLocator.getService(IContextService.class);
3452 this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);
3457 public Object getClicked(Object event) {
3458 MouseEvent e = (MouseEvent)event;
3459 final Tree tree = (Tree) e.getSource();
3460 Point point = new Point(e.x, e.y);
3461 TreeItem item = tree.getItem(point);
3463 // No selectable item at point?
3467 Object data = item.getData();