X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;ds=sidebyside;f=bundles%2Forg.simantics.browsing.ui.swt%2Fsrc%2Forg%2Fsimantics%2Fbrowsing%2Fui%2Fswt%2FGraphExplorerImpl.java;h=6361401b48723f58aa62516387421f5f473ea8cc;hb=2d2faa8fa89c174e70dc5bd63fb5d56acc1925c1;hp=0978ddb70f35585ffc70cee347f0dcc33bb5aa45;hpb=969bd23cab98a79ca9101af33334000879fb60c5;p=simantics%2Fplatform.git
diff --git a/bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl.java b/bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl.java
index 0978ddb70..6361401b4 100644
--- a/bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl.java
+++ b/bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl.java
@@ -1,3550 +1,3579 @@
-/*******************************************************************************
- * Copyright (c) 2007, 2012 Association for Decentralized Information Management
- * in Industry THTH ry.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- * VTT Technical Research Centre of Finland - initial API and implementation
- *******************************************************************************/
-package org.simantics.browsing.ui.swt;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.WeakHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-import org.eclipse.core.runtime.Assert;
-import org.eclipse.core.runtime.AssertionFailedException;
-import org.eclipse.core.runtime.IProgressMonitor;
-import org.eclipse.core.runtime.IStatus;
-import org.eclipse.core.runtime.MultiStatus;
-import org.eclipse.core.runtime.Platform;
-import org.eclipse.core.runtime.Status;
-import org.eclipse.core.runtime.jobs.Job;
-import org.eclipse.jface.action.IStatusLineManager;
-import org.eclipse.jface.resource.ColorDescriptor;
-import org.eclipse.jface.resource.DeviceResourceException;
-import org.eclipse.jface.resource.DeviceResourceManager;
-import org.eclipse.jface.resource.FontDescriptor;
-import org.eclipse.jface.resource.ImageDescriptor;
-import org.eclipse.jface.resource.JFaceResources;
-import org.eclipse.jface.resource.LocalResourceManager;
-import org.eclipse.jface.resource.ResourceManager;
-import org.eclipse.jface.viewers.IPostSelectionProvider;
-import org.eclipse.jface.viewers.ISelection;
-import org.eclipse.jface.viewers.ISelectionChangedListener;
-import org.eclipse.jface.viewers.ISelectionProvider;
-import org.eclipse.jface.viewers.IStructuredSelection;
-import org.eclipse.jface.viewers.SelectionChangedEvent;
-import org.eclipse.jface.viewers.StructuredSelection;
-import org.eclipse.jface.viewers.TreeSelection;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.SWTException;
-import org.eclipse.swt.custom.CCombo;
-import org.eclipse.swt.custom.TreeEditor;
-import org.eclipse.swt.events.FocusEvent;
-import org.eclipse.swt.events.FocusListener;
-import org.eclipse.swt.events.KeyEvent;
-import org.eclipse.swt.events.KeyListener;
-import org.eclipse.swt.events.MouseEvent;
-import org.eclipse.swt.events.MouseListener;
-import org.eclipse.swt.events.SelectionEvent;
-import org.eclipse.swt.events.SelectionListener;
-import org.eclipse.swt.graphics.Color;
-import org.eclipse.swt.graphics.Font;
-import org.eclipse.swt.graphics.GC;
-import org.eclipse.swt.graphics.Image;
-import org.eclipse.swt.graphics.Point;
-import org.eclipse.swt.graphics.RGB;
-import org.eclipse.swt.graphics.Rectangle;
-import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Control;
-import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Event;
-import org.eclipse.swt.widgets.Listener;
-import org.eclipse.swt.widgets.ScrollBar;
-import org.eclipse.swt.widgets.Text;
-import org.eclipse.swt.widgets.Tree;
-import org.eclipse.swt.widgets.TreeColumn;
-import org.eclipse.swt.widgets.TreeItem;
-import org.eclipse.ui.IWorkbenchPart;
-import org.eclipse.ui.IWorkbenchSite;
-import org.eclipse.ui.PlatformUI;
-import org.eclipse.ui.contexts.IContextActivation;
-import org.eclipse.ui.contexts.IContextService;
-import org.eclipse.ui.services.IServiceLocator;
-import org.eclipse.ui.swt.IFocusService;
-import org.simantics.browsing.ui.BuiltinKeys;
-import org.simantics.browsing.ui.CheckedState;
-import org.simantics.browsing.ui.Column;
-import org.simantics.browsing.ui.Column.Align;
-import org.simantics.browsing.ui.DataSource;
-import org.simantics.browsing.ui.ExplorerState;
-import org.simantics.browsing.ui.GraphExplorer;
-import org.simantics.browsing.ui.NodeContext;
-import org.simantics.browsing.ui.NodeContext.CacheKey;
-import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey;
-import org.simantics.browsing.ui.NodeContext.QueryKey;
-import org.simantics.browsing.ui.NodeContextPath;
-import org.simantics.browsing.ui.NodeQueryManager;
-import org.simantics.browsing.ui.NodeQueryProcessor;
-import org.simantics.browsing.ui.PrimitiveQueryProcessor;
-import org.simantics.browsing.ui.SelectionDataResolver;
-import org.simantics.browsing.ui.SelectionFilter;
-import org.simantics.browsing.ui.StatePersistor;
-import org.simantics.browsing.ui.common.ColumnKeys;
-import org.simantics.browsing.ui.common.ErrorLogger;
-import org.simantics.browsing.ui.common.NodeContextBuilder;
-import org.simantics.browsing.ui.common.NodeContextUtil;
-import org.simantics.browsing.ui.common.internal.GECache;
-import org.simantics.browsing.ui.common.internal.GENodeQueryManager;
-import org.simantics.browsing.ui.common.internal.IGECache;
-import org.simantics.browsing.ui.common.internal.IGraphExplorerContext;
-import org.simantics.browsing.ui.common.internal.UIElementReference;
-import org.simantics.browsing.ui.common.processors.DefaultCheckedStateProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultComparableChildrenProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultFinalChildrenProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultImageDecoratorProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultImagerFactoriesProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultImagerProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultLabelDecoratorProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultLabelerFactoriesProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultLabelerProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultPrunedChildrenProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultSelectedImageDecoratorFactoriesProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelDecoratorFactoriesProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelerProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointFactoryProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionsProcessor;
-import org.simantics.browsing.ui.common.processors.DefaultViewpointProcessor;
-import org.simantics.browsing.ui.common.processors.IsExpandedProcessor;
-import org.simantics.browsing.ui.common.processors.NoSelectionRequestProcessor;
-import org.simantics.browsing.ui.common.processors.ProcessorLifecycle;
-import org.simantics.browsing.ui.content.ImageDecorator;
-import org.simantics.browsing.ui.content.Imager;
-import org.simantics.browsing.ui.content.LabelDecorator;
-import org.simantics.browsing.ui.content.Labeler;
-import org.simantics.browsing.ui.content.Labeler.CustomModifier;
-import org.simantics.browsing.ui.content.Labeler.DeniedModifier;
-import org.simantics.browsing.ui.content.Labeler.DialogModifier;
-import org.simantics.browsing.ui.content.Labeler.EnumerationModifier;
-import org.simantics.browsing.ui.content.Labeler.FilteringModifier;
-import org.simantics.browsing.ui.content.Labeler.LabelerListener;
-import org.simantics.browsing.ui.content.Labeler.Modifier;
-import org.simantics.browsing.ui.content.PrunedChildrenResult;
-import org.simantics.browsing.ui.model.nodetypes.EntityNodeType;
-import org.simantics.browsing.ui.model.nodetypes.NodeType;
-import org.simantics.browsing.ui.swt.internal.Threads;
-import org.simantics.db.layer0.SelectionHints;
-import org.simantics.utils.ObjectUtils;
-import org.simantics.utils.datastructures.BijectionMap;
-import org.simantics.utils.datastructures.BinaryFunction;
-import org.simantics.utils.datastructures.disposable.AbstractDisposable;
-import org.simantics.utils.datastructures.hints.IHintContext;
-import org.simantics.utils.threads.IThreadWorkQueue;
-import org.simantics.utils.threads.SWTThread;
-import org.simantics.utils.threads.ThreadUtils;
-import org.simantics.utils.ui.ISelectionUtils;
-import org.simantics.utils.ui.jface.BasePostSelectionProvider;
-import org.simantics.utils.ui.widgets.VetoingEventHandler;
-import org.simantics.utils.ui.workbench.WorkbenchUtils;
-
-import gnu.trove.map.hash.THashMap;
-import gnu.trove.procedure.TObjectProcedure;
-import gnu.trove.set.hash.THashSet;
-
-/**
- * @see #getMaxChildren()
- * @see #setMaxChildren(int)
- * @see #getMaxChildren(NodeQueryManager, NodeContext)
- */
-class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
-
- private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
-
- private GraphExplorerImpl ge;
-
- GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
- this.ge = ge;
- }
-
- void dispose() {
- ge = null;
- }
-
- @Override
- public void setSelection(final ISelection selection) {
- if(ge == null) return;
- ge.setSelection(selection, false);
- }
-
-
- @Override
- public void removeSelectionChangedListener(ISelectionChangedListener listener) {
- if(ge == null) return;
- if(ge.isDisposed()) {
- if (DEBUG_SELECTION_LISTENERS)
- System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);
- return;
- }
- //assertNotDisposed();
- //System.out.println("Remove selection changed listener: " + listener);
- ge.selectionProvider.removeSelectionChangedListener(listener);
- }
-
- @Override
- public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
- if(ge == null) return;
- if (!ge.thread.currentThreadAccess())
- throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
- if(ge.isDisposed()) {
- System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);
- return;
- }
- //System.out.println("Add POST selection changed listener: " + listener);
- ge.selectionProvider.addPostSelectionChangedListener(listener);
- }
-
- @Override
- public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
- if(ge == null) return;
- if(ge.isDisposed()) {
- if (DEBUG_SELECTION_LISTENERS)
- System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);
- return;
- }
-// assertNotDisposed();
- //System.out.println("Remove POST selection changed listener: " + listener);
- ge.selectionProvider.removePostSelectionChangedListener(listener);
- }
-
-
- @Override
- public void addSelectionChangedListener(ISelectionChangedListener listener) {
- if(ge == null) return;
- if (!ge.thread.currentThreadAccess())
- throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
- //System.out.println("Add selection changed listener: " + listener);
- if (ge.tree.isDisposed() || ge.selectionProvider == null) {
- System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);
- return;
- }
-
- ge.selectionProvider.addSelectionChangedListener(listener);
- }
-
-
- @Override
- public ISelection getSelection() {
- if(ge == null) return StructuredSelection.EMPTY;
- if (!ge.thread.currentThreadAccess())
- throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());
- if (ge.tree.isDisposed() || ge.selectionProvider == null)
- return StructuredSelection.EMPTY;
- return ge.selectionProvider.getSelection();
- }
-
- }
-
- /**
- * If this explorer is running with an Eclipse workbench open, this
- * Workbench UI context will be activated whenever inline editing is started
- * through {@link #startEditing(TreeItem, int)} and deactivated when inline
- * editing finishes.
- *
- * This context information can be used to for UI handler activity testing.
- */
- private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
-
- private static final String KEY_DRAG_COLUMN = "dragColumn";
-
- private static final boolean DEBUG_SELECTION_LISTENERS = false;
-
- private static final int DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
-
- public static final int DEFAULT_MAX_CHILDREN = 1000;
-
- private static final long POST_SELECTION_DELAY = 300;
-
- /**
- * The time in milliseconds that must elapse between consecutive
- * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)}
- * invocations in order for this class to construct a new selection.
- *
- *
- * This is done because selection construction can be very expensive as the
- * selected set grows larger when the user is pressing shift+arrow keys.
- * GraphExplorerImpl will naturally listen to all changes in the tree
- * selection, but as an optimization will not construct new
- * StructuredSelection instances for every selection change event. A new
- * selection will be constructed and set only if the selection hasn't
- * changed for the amount of milliseconds specified by this constant.
- */
- private static final long SELECTION_CHANGE_QUIET_TIME = 150;
-
- private final IThreadWorkQueue thread;
-
- /**
- * Local method for checking from whether resources are loaded in
- * JFaceResources.
- */
- private final LocalResourceManager localResourceManager;
-
- /**
- * Local device resource manager that is safe to use in
- * {@link ImageLoaderJob} for creating images in a non-UI thread.
- */
- private final ResourceManager resourceManager;
-
- /*
- * Package visibility.
- * TODO: Get rid of these.
- */
- Tree tree;
-
- @SuppressWarnings({ "rawtypes" })
- final HashMap, NodeQueryProcessor> processors = new HashMap, NodeQueryProcessor>();
- @SuppressWarnings({ "rawtypes" })
- final HashMap primitiveProcessors = new HashMap();
- @SuppressWarnings({ "rawtypes" })
- final HashMap dataSources = new HashMap();
-
- class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
- // This is for query debugging only.
- int queryIndent = 0;
-
- GECache cache = new GECache();
- AtomicBoolean propagating = new AtomicBoolean(false);
- Object propagateList = new Object();
- Object propagate = new Object();
- List scheduleList = new ArrayList();
- final Deque activity = new LinkedList();
- int activityInt = 0;
-
- /**
- * Stores the currently running query update runnable. If
- * null
there's nothing scheduled yet in which case
- * scheduling can commence. Otherwise the update should be skipped.
- */
- AtomicReference currentQueryUpdater = new AtomicReference();
-
- /**
- * Keeps track of nodes that have already been auto-expanded. After
- * being inserted into this set, nodes will not be forced to stay in an
- * expanded state after that. This makes it possible for the user to
- * close auto-expanded nodes.
- */
- Map autoExpanded = new WeakHashMap();
-
-
- @Override
- protected void doDispose() {
- saveState();
- autoExpanded.clear();
- }
-
- @Override
- public IGECache getCache() {
- return cache;
- }
-
- @Override
- public int queryIndent() {
- return queryIndent;
- }
-
- @Override
- public int queryIndent(int offset) {
- queryIndent += offset;
- return queryIndent;
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public NodeQueryProcessor getProcessor(Object o) {
- return processors.get(o);
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public PrimitiveQueryProcessor getPrimitiveProcessor(Object o) {
- return primitiveProcessors.get(o);
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public DataSource getDataSource(Class clazz) {
- return dataSources.get(clazz);
- }
-
- @Override
- public void update(UIElementReference ref) {
- //System.out.println("GE.update " + ref);
- TreeItemReference tiref = (TreeItemReference) ref;
- TreeItem item = tiref.getItem();
- // NOTE: must be called regardless of the the item value.
- // A null item is currently used to indicate a tree root update.
- GraphExplorerImpl.this.update(item);
- }
-
- @Override
- public Object getPropagateLock() {
- return propagate;
- }
-
- @Override
- public Object getPropagateListLock() {
- return propagateList;
- }
-
- @Override
- public boolean isPropagating() {
- return propagating.get();
- }
-
- @Override
- public void setPropagating(boolean b) {
- this.propagating.set(b);
- }
-
- @Override
- public List getScheduleList() {
- return scheduleList;
- }
-
- @Override
- public void setScheduleList(List list) {
- this.scheduleList = list;
- }
-
- @Override
- public Deque getActivity() {
- return activity;
- }
-
- @Override
- public void setActivityInt(int i) {
- this.activityInt = i;
- }
-
- @Override
- public int getActivityInt() {
- return activityInt;
- }
-
- @Override
- public void scheduleQueryUpdate(Runnable r) {
- if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
- return;
- //System.out.println("Scheduling query update for runnable " + r);
- if (currentQueryUpdater.compareAndSet(null, r)) {
- //System.out.println("Scheduling query update for runnable " + r);
- queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);
- }
- }
-
- Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
- @Override
- public void run() {
- Runnable r = currentQueryUpdater.getAndSet(null);
- if (r != null) {
- //System.out.println("Running query update runnable " + r);
- r.run();
- }
- }
- };
- }
-
- GraphExplorerContext explorerContext = new GraphExplorerContext();
-
- HashSet pendingItems = new HashSet();
- boolean updating = false;
- boolean pendingRoot = false;
-
- @SuppressWarnings("deprecation")
- ModificationContext modificationContext = null;
-
- NodeContext rootContext;
-
- StatePersistor persistor = null;
-
- boolean editable = true;
-
- /**
- * This is a reverse mapping from {@link NodeContext} tree objects back to
- * their owner TreeItems.
- *
- *
- * Access this map only in the SWT thread to keep it thread-safe.
- *
- */
- BijectionMap contextToItem = new BijectionMap();
-
- /**
- * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
- * initialize.
- */
- Column[] columns = new Column[0];
- Map columnKeyToIndex = new HashMap();
- boolean refreshingColumnSizes = false;
- boolean columnsAreVisible = true;
-
- /**
- * An array reused for invoking {@link TreeItem#setImage(Image[])} instead
- * of constantly allocating new arrays for setting each TreeItems images.
- * This works because {@link TreeItem#setImage(Image[])} does not take hold
- * of the array itself, only the contents of the array.
- *
- * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
- */
- Image[] columnImageArray = { null };
-
- /**
- * Used for collecting Image or ImageDescriptor instances for a single
- * TreeItem when initially setting images for a TreeItem.
- *
- * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
- */
- Object[] columnDescOrImageArray = { null };
-
- final ExecutorService queryUpdateScheduler = Threads.getExecutor();
- final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor();
-
- /** Set to true when the Tree widget is disposed. */
- private boolean disposed = false;
- private final CopyOnWriteArrayList focusListeners = new CopyOnWriteArrayList();
- private final CopyOnWriteArrayList mouseListeners = new CopyOnWriteArrayList();
- private final CopyOnWriteArrayList keyListeners = new CopyOnWriteArrayList();
-
- /** Selection provider */
- private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
- protected BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider();
- protected SelectionDataResolver selectionDataResolver;
- protected SelectionFilter selectionFilter;
- protected BinaryFunction selectionTransformation = new BinaryFunction() {
-
- @Override
- public Object[] call(GraphExplorer explorer, Object[] objects) {
- Object[] result = new Object[objects.length];
- for (int i = 0; i < objects.length; i++) {
- IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
- context.setHint(SelectionHints.KEY_MAIN, objects[i]);
- result[i] = context;
- }
- return result;
- }
-
- };
- protected FontDescriptor originalFont;
- protected ColorDescriptor originalForeground;
- protected ColorDescriptor originalBackground;
-
- /**
- * The set of currently selected TreeItem instances. This set is needed
- * because we need to know in {@link #setData(Event)} whether the updated
- * item was a part of the current selection in which case the selection must
- * be updated.
- */
- private final Map selectedItems = new HashMap();
-
- /**
- * TODO: specify what this is for
- */
- private final Set selectionRefreshContexts = new HashSet();
-
- /**
- * If this field is non-null, it means that if {@link #setData(Event)}
- * encounters a NodeContext equal to this one, it must make the TreeItem
- * assigned to that NodeContext the topmost item of the tree using
- * {@link Tree#setTopItem(TreeItem)}. After this the field value is
- * nullified.
- *
- *
- * This is related to {@link #initializeState()}, i.e. explorer state
- * restoration.
- */
-// private NodeContext[] topNodePath = NodeContext.NONE;
-// private int[] topNodePath = {};
-// private int currentTopNodePathIndex = -1;
-
- /**
- * See {@link #setAutoExpandLevel(int)}
- */
- private int autoExpandLevel = 0;
-
- /**
- * null
if not explicitly set through
- * {@link #setServiceLocator(IServiceLocator)}.
- */
- private IServiceLocator serviceLocator;
-
- /**
- * The global workbench context service, if the workbench is available.
- * Retrieved in the constructor.
- */
- private IContextService contextService = null;
-
- /**
- * The global workbench IFocusService, if the workbench is available.
- * Retrieved in the constructor.
- */
- private IFocusService focusService = null;
-
- /**
- * A Workbench UI context activation that is activated when starting inline
- * editing through {@link #startEditing(TreeItem, int)}.
- *
- * @see #activateEditingContext()
- * @see #deactivateEditingContext()
- */
- private IContextActivation editingContext = null;
-
- static class ImageTask {
- NodeContext node;
- TreeItem item;
- Object[] descsOrImages;
- public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
- this.node = node;
- this.item = item;
- this.descsOrImages = descsOrImages;
- }
- }
-
- /**
- * The job that is used for off-loading image loading tasks (see
- * {@link ImageTask} to a worker thread from the main UI thread.
- *
- * @see #setPendingImages(IProgressMonitor)
- */
- ImageLoaderJob imageLoaderJob;
-
- /**
- * The set of currently gathered up image loading tasks for
- * {@link #imageLoaderJob} to execute.
- *
- * @see #setPendingImages(IProgressMonitor)
- */
- Map imageTasks = new THashMap();
-
- /**
- * A state flag indicating whether the vertical scroll bar was visible for
- * {@link #tree} the last time it was checked. Since there is no listener
- * that can provide this information, we check it in {@link #setData(Event)}
- * every time any data for a TreeItem is updated. If the visibility changes,
- * we will force re-layouting of the tree's parent composite.
- *
- * @see #setData(Event)
- */
- private boolean verticalBarVisible = false;
-
- static class TransientStateImpl implements TransientExplorerState {
-
- private Integer activeColumn = null;
-
- @Override
- public synchronized Integer getActiveColumn() {
- return activeColumn;
- }
-
- public synchronized void setActiveColumn(Integer column) {
- activeColumn = column;
- }
-
- }
-
- private TransientStateImpl transientState = new TransientStateImpl();
-
- boolean scheduleUpdater() {
-
- if (tree.isDisposed())
- return false;
-
- if (pendingRoot == true || !pendingItems.isEmpty()) {
- assert(!tree.isDisposed());
-
- int activity = explorerContext.activityInt;
- long delay = 30;
- if (activity < 100) {
-// System.out.println("Scheduling update immediately.");
- } else if (activity < 1000) {
-// System.out.println("Scheduling update after 500ms.");
- delay = 500;
- } else {
-// System.out.println("Scheduling update after 3000ms.");
- delay = 3000;
- }
-
- updateCounter = 0;
-
- //System.out.println("Scheduling UI update after " + delay + " ms.");
- uiUpdateScheduler.schedule(new Runnable() {
- @Override
- public void run() {
-
- if (tree.isDisposed())
- return;
-
- if (updateCounter > 0) {
- updateCounter = 0;
- uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
- } else {
- tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
- }
-
- }
- }, delay, TimeUnit.MILLISECONDS);
-
- updating = true;
- return true;
- }
-
- return false;
- }
-
- int updateCounter = 0;
-
- void update(TreeItem item) {
-
- synchronized(pendingItems) {
-
-// System.out.println("update " + item);
-
- updateCounter++;
-
- if(item == null) pendingRoot = true;
- else pendingItems.add(item);
-
- if(updating == true) return;
-
- scheduleUpdater();
-
- }
-
- }
-
- private int maxChildren = DEFAULT_MAX_CHILDREN;
-
- @Override
- public int getMaxChildren() {
- return maxChildren;
- }
-
- @Override
- public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
- Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
- //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
- if (result != null) {
- if (result < 0)
- throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
- return result;
- }
- return maxChildren;
- }
-
- @Override
- public void setMaxChildren(int maxChildren) {
- this.maxChildren = maxChildren;
- }
-
- @Override
- public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
- this.modificationContext = modificationContext;
- }
-
- /**
- * @param parent the parent SWT composite
- */
- public GraphExplorerImpl(Composite parent) {
- this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
- }
-
- /**
- * Stores the node context and the modifier that is currently being
- * modified. These are used internally to prevent duplicate edits from being
- * initiated which should always be a sensible thing to do.
- */
- private Set currentlyModifiedNodes = new THashSet();
-
- private final TreeEditor editor;
- private Color invalidModificationColor = null;
-
- /**
- * @param item the TreeItem to start editing
- * @param columnIndex the index of the column to edit, starts counting from
- * 0
- * @return true
if the editing was initiated successfully or
- * false
if editing could not be started due to lack of
- * {@link Modifier} for the labeler in question.
- */
- private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
- if (!editable)
- return "Rename not supported for selection";
-
- GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
- final NodeContext context = (NodeContext) item.getData();
- Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
- if (labeler == null)
- return "Rename not supported for selection";
-
- if(columnKey == null) columnKey = columns[columnIndex].getKey();
-
- // columnKey might be prefixed with '#' to indicate
- // textual editing is preferred. Try to get modifier
- // for that first and only if it fails, try without
- // the '#' prefix.
- Modifier modifier = labeler.getModifier(modificationContext, columnKey);
- if (modifier == null) {
- if(columnKey.startsWith("#"))
- modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
- if (modifier == null)
- return "Rename not supported for selection";
- }
- if (modifier instanceof DeniedModifier) {
- DeniedModifier dm = (DeniedModifier)modifier;
- return dm.getMessage();
- }
-
- // Prevent editing of a single node context multiple times.
- if (currentlyModifiedNodes.contains(context)) {
- //System.out.println("discarding duplicate edit for context " + context);
- return "Rename not supported for selection";
- }
-
- // Clean up any previous editor control
- Control oldEditor = editor.getEditor();
- if (oldEditor != null)
- oldEditor.dispose();
-
- if (modifier instanceof DialogModifier) {
- performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
- } else if (modifier instanceof CustomModifier) {
- startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
- } else if (modifier instanceof EnumerationModifier) {
- startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
- } else {
- startTextEditing(item, columnIndex, context, modifier);
- }
-
- return null;
- }
-
- /**
- * @param item
- * @param columnIndex
- * @param context
- * @param modifier
- */
- void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
- final DialogModifier modifier) {
- final AtomicBoolean disposed = new AtomicBoolean(false);
- Consumer callback = result -> {
- if (disposed.get())
- return;
- String error = modifier.isValid(result);
- if (error == null) {
- modifier.modify(result);
- // Item may be disposed if the tree gets reset after a previous editing.
- if (!item.isDisposed()) {
- item.setText(columnIndex, result);
- queueSelectionRefresh(context);
- }
- }
- };
-
- currentlyModifiedNodes.add(context);
- try {
- String status = modifier.query(tree, item, columnIndex, context, callback);
- if (status != null)
- ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
- } finally {
- currentlyModifiedNodes.remove(context);
- disposed.set(true);
- }
- }
-
- private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
- Point size = control.computeSize(widthHint, heightHint);
- editor.horizontalAlignment = SWT.LEFT;
- Rectangle itemRect = item.getBounds(columnIndex),
- rect = tree.getClientArea();
- editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
- int left = itemRect.x,
- right = rect.x + rect.width;
- editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
- editor.minimumHeight = size.y + insetY * 2;
- editor.layout();
- }
-
- void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
- GC gc = new GC(control);
- Point size = gc.textExtent(text);
- gc.dispose();
- reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
- }
-
- /**
- * @param item
- * @param columnIndex
- * @param context
- * @param modifier
- */
- void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
- final CustomModifier modifier) {
- final Object obj = modifier.createControl(tree, item, columnIndex, context);
- if (!(obj instanceof Control))
- throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
- final Control control = (Control) obj;
-
-// final int insetX = 0;
-// final int insetY = 0;
-// control.addListener(SWT.Resize, new Listener() {
-// @Override
-// public void handleEvent(Event e) {
-// Rectangle rect = control.getBounds();
-// control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
-// }
-// });
- control.addListener(SWT.Dispose, new Listener() {
- @Override
- public void handleEvent(Event event) {
- currentlyModifiedNodes.remove(context);
- queueSelectionRefresh(context);
- deactivateEditingContext();
- }
- });
- editor.setEditor(control, item, columnIndex);
-
- control.setFocus();
-
- GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
-
- activateEditingContext(control);
-
- // Removed in disposeListener above
- currentlyModifiedNodes.add(context);
- //System.out.println("START CUSTOM EDITING: " + item);
- }
-
- /**
- * @param item
- * @param columnIndex
- * @param context
- * @param modifier
- */
- void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
- String initialText = modifier.getValue();
- if (initialText == null)
- throw new AssertionError("Labeler.Modifier.getValue() returned null");
-
- List values = modifier.getValues();
- String selectedValue = modifier.getValue();
- int selectedIndex = values.indexOf(selectedValue);
- if (selectedIndex == -1)
- throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
-
- final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
- combo.setVisibleItemCount(10);
- //combo.setEditable(false);
-
- for (String value : values) {
- combo.add(value);
- }
- combo.select(selectedIndex);
-
- Listener comboListener = new Listener() {
- boolean arrowTraverseUsed = false;
- @Override
- public void handleEvent(final Event e) {
- //System.out.println("FOO: " + e);
- switch (e.type) {
- case SWT.KeyDown:
- if (e.character == SWT.CR) {
- // Commit edit directly on ENTER press.
- String text = combo.getText();
- modifier.modify(text);
- // Item may be disposed if the tree gets reset after a previous editing.
- if (!item.isDisposed()) {
- item.setText(columnIndex, text);
- queueSelectionRefresh(context);
- }
- combo.dispose();
- e.doit = false;
- } else if (e.keyCode == SWT.ESC) {
- // Cancel editing immediately
- combo.dispose();
- e.doit = false;
- }
- break;
- case SWT.Selection:
- {
- if (arrowTraverseUsed) {
- arrowTraverseUsed = false;
- return;
- }
-
- String text = combo.getText();
- modifier.modify(text);
-
- // Item may be disposed if the tree gets reset after a previous editing.
- if (!item.isDisposed()) {
- item.setText(columnIndex, text);
- queueSelectionRefresh(context);
- }
- combo.dispose();
- break;
- }
- case SWT.FocusOut: {
- String text = combo.getText();
- modifier.modify(text);
-
- // Item may be disposed if the tree gets reset after a previous editing.
- if (!item.isDisposed()) {
- item.setText(columnIndex, text);
- queueSelectionRefresh(context);
- }
- combo.dispose();
- break;
- }
- case SWT.Traverse: {
- switch (e.detail) {
- case SWT.TRAVERSE_RETURN:
- String text = combo.getText();
- modifier.modify(text);
- if (!item.isDisposed()) {
- item.setText(columnIndex, text);
- queueSelectionRefresh(context);
- }
- arrowTraverseUsed = false;
- // FALL THROUGH
- case SWT.TRAVERSE_ESCAPE:
- combo.dispose();
- e.doit = false;
- break;
- case SWT.TRAVERSE_ARROW_NEXT:
- case SWT.TRAVERSE_ARROW_PREVIOUS:
- arrowTraverseUsed = true;
- break;
- default:
- //System.out.println("unhandled traversal: " + e.detail);
- break;
- }
- break;
- }
- case SWT.Dispose:
- currentlyModifiedNodes.remove(context);
- deactivateEditingContext();
- break;
- }
- }
- };
- combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
- combo.addListener(SWT.KeyDown, comboListener);
- combo.addListener(SWT.FocusOut, comboListener);
- combo.addListener(SWT.Traverse, comboListener);
- combo.addListener(SWT.Selection, comboListener);
- combo.addListener(SWT.Dispose, comboListener);
-
- editor.setEditor(combo, item, columnIndex);
-
- combo.setFocus();
- combo.setListVisible(true);
-
- GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
-
- activateEditingContext(combo);
-
- // Removed in comboListener
- currentlyModifiedNodes.add(context);
-
- //System.out.println("START ENUMERATION EDITING: " + item);
- }
-
- /**
- * @param item
- * @param columnIndex
- * @param context
- * @param modifier
- */
- void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {
- String initialText = modifier.getValue();
- if (initialText == null)
- throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);
-
- final Composite composite = new Composite(tree, SWT.NONE);
- //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));
- final Text text = new Text(composite, SWT.BORDER);
- final int insetX = 0;
- final int insetY = 0;
- composite.addListener(SWT.Resize, new Listener() {
- @Override
- public void handleEvent(Event e) {
- Rectangle rect = composite.getClientArea();
- text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height
- - insetY * 2);
- }
- });
- final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
- Listener textListener = new Listener() {
-
- boolean modified = false;
-
- @Override
- public void handleEvent(final Event e) {
- String error;
- String newText;
- switch (e.type) {
- case SWT.FocusOut:
- if(modified) {
- //System.out.println("FOCUS OUT " + item);
- newText = text.getText();
- error = modifier.isValid(newText);
- if (error == null) {
- modifier.modify(newText);
-
- // Item may be disposed if the tree gets reset after a previous editing.
- if (!item.isDisposed()) {
- item.setText(columnIndex, newText);
- queueSelectionRefresh(context);
- }
- } else {
- // System.out.println("validation error: " + error);
- }
- }
- composite.dispose();
- break;
- case SWT.Modify:
- newText = text.getText();
- error = modifier.isValid(newText);
- if (error != null) {
- text.setBackground(invalidModificationColor);
- errorStatus(error);
- //System.out.println("validation error: " + error);
- } else {
- text.setBackground(null);
- errorStatus(null);
- }
- modified = true;
- break;
- case SWT.Verify:
-
- // Safety check since it seems that this may happen with
- // virtual trees.
- if (item.isDisposed())
- return;
-
- // Filter input if necessary
- e.text = filter != null ? filter.filter(e.text) : e.text;
-
- newText = text.getText();
- String leftText = newText.substring(0, e.start);
- String rightText = newText.substring(e.end, newText.length());
- GraphExplorerImpl.this.reconfigureTreeEditorForText(
- item, columnIndex, text, leftText + e.text + rightText,
- SWT.DEFAULT, insetX, insetY);
- break;
- case SWT.Traverse:
- switch (e.detail) {
- case SWT.TRAVERSE_RETURN:
- if(modified) {
- newText = text.getText();
- error = modifier.isValid(newText);
- if (error == null) {
- modifier.modify(newText);
- if (!item.isDisposed()) {
- item.setText(columnIndex, newText);
- queueSelectionRefresh(context);
- }
- }
- }
- // FALL THROUGH
- case SWT.TRAVERSE_ESCAPE:
- composite.dispose();
- e.doit = false;
- break;
- default:
- //System.out.println("unhandled traversal: " + e.detail);
- break;
- }
- break;
-
- case SWT.Dispose:
- currentlyModifiedNodes.remove(context);
- deactivateEditingContext();
- errorStatus(null);
- break;
- }
- }
- };
- // Set the initial text before registering a listener. We do not want immediate modification!
- text.setText(initialText);
- text.addListener(SWT.FocusOut, textListener);
- text.addListener(SWT.Traverse, textListener);
- text.addListener(SWT.Verify, textListener);
- text.addListener(SWT.Modify, textListener);
- text.addListener(SWT.Dispose, textListener);
- editor.setEditor(composite, item, columnIndex);
- text.selectAll();
- text.setFocus();
-
- // Initialize TreeEditor properly.
- GraphExplorerImpl.this.reconfigureTreeEditorForText(
- item, columnIndex, text, initialText,
- SWT.DEFAULT, insetX, insetY);
-
- // Removed in textListener
- currentlyModifiedNodes.add(context);
-
- activateEditingContext(text);
-
- //System.out.println("START TEXT EDITING: " + item);
- }
-
- protected void errorStatus(String error) {
- IStatusLineManager status = getStatusLineManager();
- if (status != null) {
- status.setErrorMessage(error);
- }
- }
-
- protected IStatusLineManager getStatusLineManager() {
- if (serviceLocator instanceof IWorkbenchPart) {
- return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);
- } else if (serviceLocator instanceof IWorkbenchSite) {
- return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);
- }
- return null;
- }
-
- protected void activateEditingContext(Control control) {
- if (contextService != null) {
- editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
- }
- if (control != null && focusService != null) {
- focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);
- // No need to remove the control, it will be
- // removed automatically when it is disposed.
- }
- }
-
- protected void deactivateEditingContext() {
- IContextActivation a = editingContext;
- if (a != null) {
- editingContext = null;
- contextService.deactivateContext(a);
- }
- }
-
- /**
- * @param forContext
- */
- void queueSelectionRefresh(NodeContext forContext) {
- selectionRefreshContexts.add(forContext);
- }
-
- @Override
- public String startEditing(NodeContext context, String columnKey_) {
- assertNotDisposed();
- if (!thread.currentThreadAccess())
- throw new IllegalStateException("not in SWT display thread " + thread.getThread());
-
- String columnKey = columnKey_;
- if(columnKey.startsWith("#")) {
- columnKey = columnKey.substring(1);
- }
-
- Integer columnIndex = columnKeyToIndex.get(columnKey);
- if (columnIndex == null)
- return "Rename not supported for selection";
-
- TreeItem item = contextToItem.getRight(context);
- if (item == null)
- return "Rename not supported for selection";
-
- return startEditing(item, columnIndex, columnKey_);
-
- }
-
- @Override
- public String startEditing(String columnKey) {
-
- ISelection selection = postSelectionProvider.getSelection();
- if(selection == null) return "Rename not supported for selection";
- NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
- if(context == null) return "Rename not supported for selection";
-
- return startEditing(context, columnKey);
-
- }
-
- /**
- * @param site null
if the explorer is detached from the workbench
- * @param parent the parent SWT composite
- * @param style the tree style to use, check the see tags for the available flags
- *
- * @see SWT#SINGLE
- * @see SWT#MULTI
- * @see SWT#CHECK
- * @see SWT#FULL_SELECTION
- * @see SWT#NO_SCROLL
- * @see SWT#H_SCROLL
- * @see SWT#V_SCROLL
- */
- public GraphExplorerImpl(Composite parent, int style) {
-
- setServiceLocator(null);
-
- this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
- this.resourceManager = new DeviceResourceManager(parent.getDisplay());
-
- this.imageLoaderJob = new ImageLoaderJob(this);
- this.imageLoaderJob.setPriority(Job.DECORATE);
-
- invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
-
- this.thread = SWTThread.getThreadAccess(parent);
-
- for(int i=0;i<10;i++) explorerContext.activity.push(0);
-
- tree = new Tree(parent, style);
- tree.addListener(SWT.SetData, this);
- tree.addListener(SWT.Expand, this);
- tree.addListener(SWT.Dispose, this);
- tree.addListener(SWT.Activate, this);
-
- tree.setData(KEY_GRAPH_EXPLORER, this);
-
- // These are both required for performing column resizing without flicker.
- // See SWT.Resize event handling in #handleEvent() for more explanations.
- parent.addListener(SWT.Resize, this);
- tree.addListener(SWT.Resize, this);
-
- originalFont = JFaceResources.getDefaultFontDescriptor();
-// originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
-// originalForeground = tree.getForeground();
-
- tree.setFont((Font) localResourceManager.get(originalFont));
-
- columns = new Column[] { new Column(ColumnKeys.SINGLE) };
- columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
-
- editor = new TreeEditor(tree);
- editor.horizontalAlignment = SWT.LEFT;
- editor.grabHorizontal = true;
- editor.minimumWidth = 50;
-
- setBasicListeners();
- setDefaultProcessors();
- }
-
- @Override
- public IThreadWorkQueue getThread() {
- return thread;
- }
-
- TreeItem previousSingleSelection = null;
- long focusGainedAt = Long.MIN_VALUE;
-
- protected void setBasicListeners() {
- // Keep track of the previous single selection to help
- // decide whether to start editing a tree node on mouse
- // downs or not.
- tree.addListener(SWT.Selection, new Listener() {
- @Override
- public void handleEvent(Event event) {
- TreeItem[] selection = tree.getSelection();
- if (selection.length == 1) {
- //for (TreeItem item : selection)
- // System.out.println("selection: " + item);
- previousSingleSelection = selection[0];
- } else {
- previousSingleSelection = null;
- }
- }
- });
-
- // Try to start editing of tree column when clicked for the second time.
- Listener mouseEditListener = new Listener() {
-
- Future> startEdit = null;
-
- @Override
- public void handleEvent(Event event) {
- if (event.type == SWT.DragDetect) {
- // Needed to prevent editing from being started when in fact
- // the user starts dragging an item.
- //System.out.println("DRAG DETECT: " + event);
- cancelEdit();
- return;
- }
- //System.out.println("mouse down: " + event);
- if (event.button == 1) {
- // Always ignore the first mouse button press that focuses
- // the control. Do not let it start in-line editing since
- // that is very annoying to users and not how the UI's that
- // people are used to behave.
- long eventTime = ((long) event.time) & 0xFFFFFFFFL;
- if ((eventTime - focusGainedAt) < 250L) {
- //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));
- return;
- }
- //System.out.println("testing whether to start editing");
-
- final Point point = new Point(event.x, event.y);
- final TreeItem item = tree.getItem(point);
- if (item == null)
- return;
- //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
-
- // Only start editing if the item was already selected.
- if (!item.equals(previousSingleSelection)) {
- cancelEdit();
- return;
- }
-
- if (tree.getColumnCount() > 1) {
- // TODO: reconsider this logic, might not be good in general.
- for (int i = 0; i < tree.getColumnCount(); i++) {
- if (item.getBounds(i).contains(point)) {
- tryScheduleEdit(event, item, point, 100, i);
- return;
- }
- }
- } else {
- //System.out.println("clicks: " + event.count);
- if (item.getBounds().contains(point)) {
- if (event.count == 1) {
- tryScheduleEdit(event, item, point, 500, 0);
- } else {
- cancelEdit();
- }
- }
- }
- }
- }
-
- void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {
- //System.out.println("\tCONTAINS: " + item);
- if (!cancelEdit())
- return;
-
- //System.out.println("\tScheduling edit: " + item);
- startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
- @Override
- public void run() {
- ThreadUtils.asyncExec(thread, new Runnable() {
- @Override
- public void run() {
- if (item.isDisposed())
- return;
- startEditing(item, column, null);
- }
- });
- }
- }, delayMs, TimeUnit.MILLISECONDS);
- }
-
- boolean cancelEdit() {
- Future> f = startEdit;
- if (f != null) {
- // Try to cancel the start edit task if it's not running yet.
- startEdit = null;
- if (!f.isDone()) {
- boolean ret = f.cancel(false);
- //System.out.println("\tCancelled edit: " + ret);
- return ret;
- }
- }
- //System.out.println("\tNo edit in progress to cancel");
- return true;
- }
- };
- tree.addListener(SWT.MouseDown, mouseEditListener);
- tree.addListener(SWT.DragDetect, mouseEditListener);
- tree.addListener(SWT.DragDetect, new Listener() {
- @Override
- public void handleEvent(Event event) {
- Point test = new Point(event.x, event.y);
- TreeItem item = tree.getItem(test);
- if(item != null) {
- for(int i=0;i set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);
- selectedItems.clear();
- for (NodeContext nc : set) {
- TreeItem item = contextToItem.getRight(nc);
- if (item != null)
- selectedItems.put(item, nc);
- }
- //System.out.println("newly selected items: " + selectedItems);
- }
- });
- }
-
- /**
- * Mod count for delaying post selection changed events.
- */
- int postSelectionModCount = 0;
-
- /**
- * Last tree selection modification time for implementing a quiet
- * time for selection changes.
- */
- long lastSelectionModTime = System.currentTimeMillis() - 10000;
-
- /**
- * Current target time for the selection to be set. Calculated
- * according to the set quiet time and last selection modification
- * time.
- */
- long selectionSetTargetTime = 0;
-
- /**
- * true
if delayed selection runnable is current scheduled or
- * running.
- */
- boolean delayedSelectionScheduled = false;
-
- Runnable SELECTION_DELAY = new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed())
- return;
- long now = System.currentTimeMillis();
- long waitTimeLeft = selectionSetTargetTime - now;
- if (waitTimeLeft > 0) {
- // Not enough quiet time, reschedule.
- delayedSelectionScheduled = true;
- tree.getDisplay().timerExec((int) waitTimeLeft, this);
- } else {
- // Time to perform selection, stop rescheduling.
- delayedSelectionScheduled = false;
- resetSelection();
- }
- }
- };
-
- private void widgetSelectionChanged(boolean forceSelectionChange) {
- long modTime = System.currentTimeMillis();
- long delta = modTime - lastSelectionModTime;
- lastSelectionModTime = modTime;
- if (!forceSelectionChange && delta < SELECTION_CHANGE_QUIET_TIME) {
- long msToWait = SELECTION_CHANGE_QUIET_TIME - delta;
- selectionSetTargetTime = modTime + msToWait;
- if (!delayedSelectionScheduled) {
- delayedSelectionScheduled = true;
- tree.getDisplay().timerExec((int) msToWait, SELECTION_DELAY);
- }
- // Make sure that post selection change events do not fire.
- ++postSelectionModCount;
- return;
- }
-
- // Immediate selection reconstruction.
- resetSelection();
- }
-
- private void resetSelection() {
- final ISelection selection = getWidgetSelection();
-
- //System.out.println("resetSelection(" + postSelectionModCount + ")");
- //System.out.println(" provider selection: " + selectionProvider.getSelection());
- //System.out.println(" widget selection: " + selection);
-
- selectionProvider.setAndFireNonEqualSelection(selection);
-
- // Implement deferred firing of post selection events
- final int count = ++postSelectionModCount;
- //System.out.println("[" + System.currentTimeMillis() + "] scheduling postSelectionChanged " + count + ": " + selection);
- ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
- @Override
- public void run() {
- int newCount = postSelectionModCount;
- // Don't publish selection yet, there's another change incoming.
- //System.out.println("[" + System.currentTimeMillis() + "] checking post selection publish: " + count + " vs. " + newCount + ": " + selection);
- if (newCount != count)
- return;
- //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);
-
- if (tree.isDisposed())
- return;
-
- //System.out.println("scheduling fire post selection changed: " + selection);
- tree.getDisplay().asyncExec(new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed() || selectionProvider == null)
- return;
- //System.out.println("firing post selection changed: " + selection);
- selectionProvider.firePostSelection(selection);
- }
- });
- }
- }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);
- }
-
- protected void setDefaultProcessors() {
- // Add a simple IMAGER query processor that always returns null.
- // With this processor no images will ever be shown.
-// setPrimitiveProcessor(new StaticImagerProcessor(null));
-
- setProcessor(new DefaultComparableChildrenProcessor());
- setProcessor(new DefaultLabelDecoratorsProcessor());
- setProcessor(new DefaultImageDecoratorsProcessor());
- setProcessor(new DefaultSelectedLabelerProcessor());
- setProcessor(new DefaultLabelerFactoriesProcessor());
- setProcessor(new DefaultSelectedImagerProcessor());
- setProcessor(new DefaultImagerFactoriesProcessor());
- setPrimitiveProcessor(new DefaultLabelerProcessor());
- setPrimitiveProcessor(new DefaultCheckedStateProcessor());
- setPrimitiveProcessor(new DefaultImagerProcessor());
- setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());
- setPrimitiveProcessor(new DefaultImageDecoratorProcessor());
- setPrimitiveProcessor(new NoSelectionRequestProcessor());
-
- setProcessor(new DefaultFinalChildrenProcessor(this));
-
- setProcessor(new DefaultPrunedChildrenProcessor());
- setProcessor(new DefaultSelectedViewpointProcessor());
- setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());
- setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());
- setProcessor(new DefaultViewpointContributionsProcessor());
-
- setPrimitiveProcessor(new DefaultViewpointProcessor());
- setPrimitiveProcessor(new DefaultViewpointContributionProcessor());
- setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());
- setPrimitiveProcessor(new DefaultIsExpandedProcessor());
- setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());
- }
-
- @Override
- public void setProcessor(NodeQueryProcessor processor) {
- assertNotDisposed();
- if (processor == null)
- throw new IllegalArgumentException("null processor");
-
- processors.put(processor.getIdentifier(), processor);
- }
-
- @Override
- public void setPrimitiveProcessor(PrimitiveQueryProcessor processor) {
- assertNotDisposed();
- if (processor == null)
- throw new IllegalArgumentException("null processor");
-
- PrimitiveQueryProcessor> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
-
- if (oldProcessor instanceof ProcessorLifecycle)
- ((ProcessorLifecycle) oldProcessor).detached(this);
- if (processor instanceof ProcessorLifecycle)
- ((ProcessorLifecycle) processor).attached(this);
- }
-
- @Override
- public void setDataSource(DataSource provider) {
- assertNotDisposed();
- if (provider == null)
- throw new IllegalArgumentException("null provider");
- dataSources.put(provider.getProvidedClass(), provider);
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public DataSource removeDataSource(Class forProvidedClass) {
- assertNotDisposed();
- if (forProvidedClass == null)
- throw new IllegalArgumentException("null class");
- return dataSources.remove(forProvidedClass);
- }
-
- @Override
- public void setPersistor(StatePersistor persistor) {
- this.persistor = persistor;
- }
-
- @Override
- public SelectionDataResolver getSelectionDataResolver() {
- return selectionDataResolver;
- }
-
- @Override
- public void setSelectionDataResolver(SelectionDataResolver r) {
- this.selectionDataResolver = r;
- }
-
- @Override
- public SelectionFilter getSelectionFilter() {
- return selectionFilter;
- }
-
- @Override
- public void setSelectionFilter(SelectionFilter f) {
- this.selectionFilter = f;
- // TODO: re-filter current selection?
- }
-
- @Override
- public void setSelectionTransformation(BinaryFunction f) {
- this.selectionTransformation = f;
- }
-
- @Override
- public void addListener(T listener) {
- if(listener instanceof FocusListener) {
- focusListeners.add((FocusListener)listener);
- } else if(listener instanceof MouseListener) {
- mouseListeners.add((MouseListener)listener);
- } else if(listener instanceof KeyListener) {
- keyListeners.add((KeyListener)listener);
- }
- }
-
- @Override
- public void removeListener(T listener) {
- if(listener instanceof FocusListener) {
- focusListeners.remove(listener);
- } else if(listener instanceof MouseListener) {
- mouseListeners.remove(listener);
- } else if(listener instanceof KeyListener) {
- keyListeners.remove(listener);
- }
- }
-
- public void addSelectionListener(SelectionListener listener) {
- tree.addSelectionListener(listener);
- }
-
- public void removeSelectionListener(SelectionListener listener) {
- tree.removeSelectionListener(listener);
- }
-
- private Set uiContexts;
-
- @Override
- public void setUIContexts(Set contexts) {
- this.uiContexts = contexts;
- }
-
- @Override
- public void setRoot(final Object root) {
- if(uiContexts != null && uiContexts.size() == 1)
- setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
- else
- setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
- }
-
- @Override
- public void setRootContext(final NodeContext context) {
- setRootContext0(context);
- }
-
- private void setRootContext0(final NodeContext context) {
- Assert.isNotNull(context, "root must not be null");
- if (isDisposed() || tree.isDisposed())
- return;
- Display display = tree.getDisplay();
- if (display.getThread() == Thread.currentThread()) {
- doSetRoot(context);
- } else {
- display.asyncExec(new Runnable() {
- @Override
- public void run() {
- doSetRoot(context);
- }
- });
- }
- }
-
- private void initializeState() {
- if (persistor == null)
- return;
-
- ExplorerState state = persistor.deserialize(
- Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
- getRoot());
-
- // topNodeToSet will be processed by #setData when it encounters a
- // NodeContext that matches this one.
-// topNodePath = state.topNodePath;
-// topNodePathChildIndex = state.topNodePathChildIndex;
-// currentTopNodePathIndex = 0;
-
- Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
- if (processor instanceof DefaultIsExpandedProcessor) {
- DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
- for(NodeContext expanded : state.expandedNodes) {
- isExpandedProcessor.setExpanded(expanded, true);
- }
- }
- }
-
- private void saveState() {
- if (persistor == null)
- return;
-
- NodeContext[] topNodePath = NodeContext.NONE;
- int[] topNodePathChildIndex = {};
- Collection expandedNodes = Collections.emptyList();
- Map columnWidths = Collections. emptyMap();
-
- // Resolve top node path
- TreeItem topItem = tree.getTopItem();
- if (topItem != null) {
- NodeContext topNode = (NodeContext) topItem.getData();
- if (topNode != null) {
- topNodePath = getNodeContextPathSegments(topNode);
- topNodePathChildIndex = new int[topNodePath.length];
- for (int i = 0; i < topNodePath.length; ++i) {
- // TODO: get child indexes
- topNodePathChildIndex[i] = 0;
- }
- }
- }
-
- // Resolve expanded nodes
- Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
- if (processor instanceof IsExpandedProcessor) {
- IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;
- expandedNodes = isExpandedProcessor.getExpanded();
- }
-
- // Column widths
- TreeColumn[] columns = tree.getColumns();
- if (columns.length > 1) {
- columnWidths = new HashMap();
- for (int i = 0; i < columns.length; ++i) {
- columnWidths.put(columns[i].getText(), columns[i].getWidth());
- }
- }
-
- persistor.serialize(
- Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
- getRoot(),
- new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
- }
-
- /**
- * Invoke only from SWT thread to reset the root of the graph explorer tree.
- *
- * @param root
- */
- private void doSetRoot(NodeContext root) {
- if (tree.isDisposed())
- return;
- if (root.getConstant(BuiltinKeys.INPUT) == null) {
- ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));
- return;
- }
-
- // Empty caches, release queries.
- GraphExplorerContext oldContext = explorerContext;
- GraphExplorerContext newContext = new GraphExplorerContext();
- GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));
- this.explorerContext = newContext;
- oldContext.safeDispose();
-
- // Need to empty these or otherwise they won't be emptied until the
- // explorer is disposed which would mean that many unwanted references
- // will be held by this map.
- clearPrimitiveProcessors();
-
- this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
- : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
-
- explorerContext.getCache().incRef(this.rootContext);
-
- initializeState();
-
- NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
-
- tree.setItemCount(contexts.length);
-
- select(rootContext);
- refreshColumnSizes();
- }
-
- @Override
- public NodeContext getRoot() {
- return rootContext;
- }
-
- @Override
- public NodeContext getParentContext(NodeContext context) {
- if (disposed)
- throw new IllegalStateException("disposed");
- if (!thread.currentThreadAccess())
- throw new IllegalStateException("not in SWT display thread " + thread.getThread());
-
- TreeItem item = contextToItem.getRight(context);
- if(item == null) return null;
- TreeItem parentItem = item.getParentItem();
- if(parentItem == null) return null;
- return (NodeContext)parentItem.getData();
- }
-
- Point previousTreeSize;
- Point previousTreeParentSize;
- boolean activatedBefore = false;
-
- @Override
- public void handleEvent(Event event) {
- //System.out.println("EVENT: " + event);
- switch(event.type) {
- case SWT.Expand:
- //System.out.println("EXPAND: " + event.item);
- if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
- expandVirtual(event);
- } else {
- System.out.println("TODO: non-virtual tree item expand");
- }
- break;
- case SWT.SetData:
- // Only invoked for SWT.VIRTUAL trees
- if (disposed)
- // Happened for Hannu once during program startup.
- // java.lang.AssertionError
- // at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)
- // at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)
- // at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)
- // I do not know whether SWT guarantees that SetData events
- // don't come after Dispose event has been issued, but I
- // think its better to have this check here just incase.
- return;
- setData(event);
- break;
- case SWT.Activate:
- // This ensures that column sizes are refreshed at
- // least once when the GE is first shown.
- if (!activatedBefore) {
- refreshColumnSizes();
- activatedBefore = true;
- }
- break;
- case SWT.Dispose:
- //new Exception().printStackTrace();
- if (disposed)
- return;
- disposed = true;
- doDispose();
- break;
- case SWT.Resize:
- if (event.widget == tree) {
- // This case is meant for listening to tree width increase.
- // The column resizing must be performed only after the tree
- // itself as been resized.
- Point size = tree.getSize();
- int dx = 0;
- if (previousTreeSize != null) {
- dx = size.x - previousTreeSize.x;
- }
- previousTreeSize = size;
- //System.out.println("RESIZE: " + dx + " - size=" + size);
-
- if (dx > 0) {
- tree.setRedraw(false);
- refreshColumnSizes(size);
- tree.setRedraw(true);
- }
- } else if (event.widget == tree.getParent()) {
- // This case is meant for listening to tree width decrease.
- // The columns must be resized before the tree widget itself
- // is resized to prevent scroll bar flicker. This can be achieved
- // by listening to the resize events of the tree parent widget.
- Composite parent = tree.getParent();
- Point size = parent.getSize();
-
- // We must subtract the parent's border and possible
- // scroll bar width from the new target width of the columns.
- size.x -= tree.getParent().getBorderWidth() * 2;
- ScrollBar vBar = parent.getVerticalBar();
- if (vBar != null && vBar.isVisible())
- size.x -= vBar.getSize().x;
-
- int dx = 0;
- if (previousTreeParentSize != null) {
- dx = size.x - previousTreeParentSize.x;
- }
- previousTreeParentSize = size;
- //System.out.println("RESIZE: " + dx + " - size=" + size);
-
- if (dx < 0) {
- tree.setRedraw(false);
- refreshColumnSizes(size);
- tree.setRedraw(true);
- }
- }
- break;
- default:
- break;
- }
-
- }
-
- protected void refreshColumnSizes() {
-// Composite treeParent = tree.getParent();
-// Point size = treeParent.getSize();
-// size.x -= treeParent.getBorderWidth() * 2;
- Point size = tree.getSize();
- refreshColumnSizes(size);
- tree.getParent().layout();
- }
-
- /**
- * This has been disabled since the logic of handling column widths has been
- * externalized to parties creating {@link GraphExplorerImpl} instances.
- */
- protected void refreshColumnSizes(Point toSize) {
- /*
- refreshingColumnSizes = true;
- try {
- int columnCount = tree.getColumnCount();
- if (columnCount > 0) {
- Point size = toSize;
- int targetWidth = size.x - tree.getBorderWidth() * 2;
- targetWidth -= 0;
-
- // Take the vertical scroll bar existence into to account when
- // calculating the overflow column width.
- ScrollBar vBar = tree.getVerticalBar();
- //if (vBar != null && vBar.isVisible())
- if (vBar != null)
- targetWidth -= vBar.getSize().x;
-
- List resizing = new ArrayList();
- int usedWidth = 0;
- int resizingWidth = 0;
- int totalWeight = 0;
- for (int i = 0; i < columnCount - 1; ++i) {
- TreeColumn col = tree.getColumn(i);
- //System.out.println(" " + col.getText() + ": " + col.getWidth());
- int width = col.getWidth();
- usedWidth += width;
- Column c = (Column) col.getData();
- if (c.hasGrab()) {
- resizing.add(col);
- resizingWidth += width;
- totalWeight += c.getWeight();
- }
- }
-
- int requiredWidthAdjustment = targetWidth - usedWidth;
- if (requiredWidthAdjustment < 0)
- requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());
- double diff = requiredWidthAdjustment;
- //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);
-
- // Decide how much to give space to / take space from each grabbing column
- double wrel = 1.0 / resizing.size();
-
- double[] weightedShares = new double[resizing.size()];
- for (int i = 0; i < resizing.size(); ++i) {
- TreeColumn col = resizing.get(i);
- Column c = (Column) col.getData();
- if (totalWeight == 0) {
- weightedShares[i] = wrel;
- } else {
- weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
- }
- }
- //System.out.println("grabbing columns:" + resizing);
- //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
-
- // Always shrink the columns if necessary, but don't enlarge before
- // there is sufficient space to at least give all resizable columns
- // some more width.
- if (diff < 0 || (diff > 0 && diff > resizing.size())) {
- // Need to either shrink or enlarge the resizable columns if possible.
- for (int i = 0; i < resizing.size(); ++i) {
- TreeColumn col = resizing.get(i);
- Column c = (Column) col.getData();
- int cw = col.getWidth();
- //double wrel = (double) cw / (double) resizingWidth;
- //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);
- double ddelta = weightedShares[i] * diff;
- int delta = 0;
- if (diff < 0) {
- delta = (int) Math.floor(ddelta);
- } else {
- delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
- }
- //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);
- //System.out.println("argh(" + col.getText() + "): " + c.getWidth() + " vs. " + col.getWidth() + " vs. " + (cw+delta));
- int newWidth = Math.max(c.getWidth(), cw + delta);
- requiredWidthAdjustment -= (newWidth - cw);
- col.setWidth(newWidth);
- }
- }
-
- //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
-
- TreeColumn last = tree.getColumn(columnCount - 1);
- // HACK: see #setColumns for why this is here.
- if (FILLER.equals(last.getText())) {
- last.setWidth(Math.max(0, requiredWidthAdjustment));
- }
- }
- } finally {
- refreshingColumnSizes = false;
- }
- */
- }
-
- private void doDispose() {
- explorerContext.dispose();
-
- // No longer necessary, the used executors are shared.
- //scheduler.shutdown();
- //scheduler2.shutdown();
-
- processors.clear();
- detachPrimitiveProcessors();
- primitiveProcessors.clear();
- dataSources.clear();
-
- pendingItems.clear();
-
- rootContext = null;
-
- contextToItem.clear();
-
- mouseListeners.clear();
-
- selectionProvider.clearListeners();
- selectionProvider = null;
- selectionDataResolver = null;
- selectionRefreshContexts.clear();
- selectedItems.clear();
- originalFont = null;
-
- localResourceManager.dispose();
-
- // Must shutdown image loader job before disposing its ResourceManager
- imageLoaderJob.dispose();
- imageLoaderJob.cancel();
- try {
- imageLoaderJob.join();
- } catch (InterruptedException e) {
- ErrorLogger.defaultLogError(e);
- }
- resourceManager.dispose();
-
- postSelectionProvider.dispose();
-
- }
-
- private void expandVirtual(final Event event) {
- TreeItem item = (TreeItem) event.item;
- assert (item != null);
- NodeContext context = (NodeContext) item.getData();
- assert (context != null);
-
- GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));
- NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);
- int maxChildren = getMaxChildren(manager, context);
- item.setItemCount(children.length < maxChildren ? children.length : maxChildren);
- }
-
- private NodeContext getNodeContext(TreeItem item) {
- assert(item != null);
-
- NodeContext context = (NodeContext)item.getData();
- assert(context != null);
-
- return context;
- }
-
- private NodeContext getParentContext(TreeItem item) {
- TreeItem parentItem = item.getParentItem();
- if(parentItem != null) {
- return getNodeContext(parentItem);
- } else {
- return rootContext;
- }
- }
-
- private static final String LISTENER_SET_INDICATOR = "LSI";
- private static final String PENDING = "PENDING";
- private int contextSelectionChangeModCount = 0;
-
- /**
- * Only invoked for SWT.VIRTUAL widgets.
- *
- * @param event
- */
- private void setData(final Event event) {
- assert (event != null);
- TreeItem item = (TreeItem) event.item;
- assert (item != null);
-
- // Based on experience it seems to be possible that
- // SetData events are sent for disposed TreeItems.
- if (item.isDisposed() || item.getData(PENDING) != null)
- return;
-
- //System.out.println("GE.SetData " + item);
-
- GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
-
- NodeContext parentContext = getParentContext(item);
- assert (parentContext != null);
-
- NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
-
- // Some children have disappeared since counting
- if (event.index < 0) {
- ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());
- return;
- }
- if (event.index >= parentChildren.length)
- return;
-
- NodeContext context = parentChildren[event.index];
- assert (context != null);
- item.setData(context);
-
- // Manage NodeContext -> TreeItem mappings
- contextToItem.map(context, item);
- if (item.getData(LISTENER_SET_INDICATOR) == null) {
- // This "if" exists because setData will get called many
- // times for the same (NodeContext, TreeItem) pairs.
- // Each TreeItem only needs one listener, but this
- // is needed to tell whether it already has a listener
- // or not.
- item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
- item.addListener(SWT.Dispose, itemDisposeListener);
- }
-
- boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
-
- PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);
- int maxChildren = getMaxChildren(manager, context);
- //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);
-
- NodeContext[] pruned = children.getPrunedChildren();
- int count = Math.min(pruned.length, maxChildren);
-
- if (isExpanded || item.getItemCount() > 1) {
- item.setItemCount(count);
- TreeItem[] childItems = item.getItems();
- for(int i=0;i 1) && !isExpanded) {
- //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");
- int level = getTreeItemLevel(item);
- if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)
- && !explorerContext.autoExpanded.containsKey(context))
- {
- //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
- explorerContext.autoExpanded.put(context, Boolean.TRUE);
- setExpanded(context, true);
- }
- }
-
- item.setExpanded(isExpanded);
-
- if ((tree.getStyle() & SWT.CHECK) != 0) {
- CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);
- item.setChecked(CheckedState.CHECKED_STATES.contains(checked));
- item.setGrayed(CheckedState.GRAYED == checked);
- }
-
- //System.out.println("GE.SetData completed " + item);
-
- // This test makes sure that selectionProvider holds the correct
- // selection with respect to the actual selection stored by the virtual
- // SWT Tree widget.
- // The data items shown below the items occupied by the selected and now removed data
- // will be squeezed to use the tree items previously used for the now
- // removed data. When this happens, the NodeContext items stored by the
- // tree items will be different from what the GraphExplorer's
- // ISelectionProvider thinks the selection currently is. To compensate,
- // 1. Recognize the situation
- // 2. ASAP set the selection provider selection to what is actually
- // offered by the tree widget.
- NodeContext selectedContext = selectedItems.get(item);
-// System.out.println("selectedContext(" + item + "): " + selectedContext);
- if (selectedContext != null && !selectedContext.equals(context)) {
- final int modCount = ++contextSelectionChangeModCount;
-// System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);
-// System.out.println(" old context: " + selectedContext);
-// System.out.println(" new context: " + context);
-// System.out.println(" provider selection: " + selectionProvider.getSelection());
-// System.out.println(" widget selection: " + getWidgetSelection());
- ThreadUtils.asyncExec(thread, new Runnable() {
- @Override
- public void run() {
- if (isDisposed())
- return;
- int count = contextSelectionChangeModCount;
-// System.out.println("MODCOUNT: " + modCount + " vs. " + count);
- if (modCount != count)
- return;
- widgetSelectionChanged(true);
- }
- });
- }
-
- // This must be done to keep the visible tree selection properly
- // in sync with the selectionProvider JFace proxy of this class in
- // cases where an in-line editor was previously active for the node
- // context.
- if (selectionRefreshContexts.remove(context)) {
- final ISelection currentSelection = selectionProvider.getSelection();
- // asyncExec is here to prevent ui glitches that
- // seem to occur if the selection setting is done
- // directly here in between setData invocations.
- ThreadUtils.asyncExec(thread, new Runnable() {
- @Override
- public void run() {
- if (isDisposed())
- return;
-// System.out.println("REFRESHING SELECTION: " + currentSelection);
-// System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));
-// System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());
- setSelection(currentSelection, true);
-// System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));
-// System.out.println("AFTER setSelection: " + selectionProvider.getSelection());
- }
- });
- }
-
- // TODO: doesn't work if any part of the node path that should be
- // revealed is out of view.
- // Disabled until a better solution is devised.
- // Suggestion: include item indexes into the stored node context path
- // to make it possible for this method to know whether the current
- // node path segment is currently out of view based on event.index.
- // If out of view, this code needs to scroll the view programmatically
- // onwards.
-// if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {
-// NodeContext topNode = topNodePath[currentTopNodePathIndex];
-// if (topNode.equals(context)) {
-// final TreeItem topItem = item;
-// ++currentTopNodePathIndex;
-// if (currentTopNodePathIndex >= topNodePath.length) {
-// // Mission accomplished. End search for top node here.
-// topNodePath = NodeContext.NONE;
-// currentTopNodePathIndex = -1;
-// }
-// ThreadUtils.asyncExec(thread, new Runnable() {
-// @Override
-// public void run() {
-// if (isDisposed())
-// return;
-// tree.setTopItem(topItem);
-// }
-// });
-// }
-// }
-
- // Check if vertical scroll bar has become visible and refresh layout.
- ScrollBar verticalBar = tree.getVerticalBar();
- if(verticalBar != null) {
- boolean currentlyVerticalBarVisible = verticalBar.isVisible();
- if (verticalBarVisible != currentlyVerticalBarVisible) {
- verticalBarVisible = currentlyVerticalBarVisible;
- Composite parent = tree.getParent();
- if (parent != null)
- parent.layout();
- }
- }
- }
-
- /**
- * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the
- * return value is calculated. Items without parents have level=2,
- * their children level=3, etc. Returns 0 for invalid items
- */
- private int getTreeItemLevel(TreeItem item) {
- if (item == null)
- return 0;
- int level = 1;
- for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);
- //System.out.println("\tgetTreeItemLevel(" + parent + ")");
- //System.out.println("level(" + item + "): " + level);
- return level;
- }
-
- /**
- * @param node
- * @return
- */
- private NodeContext[] getNodeContextPathSegments(NodeContext node) {
- TreeItem item = contextToItem.getRight(node);
- if (item == null)
- return NodeContext.NONE;
- int level = getTreeItemLevel(item);
- if (level == 0)
- return NodeContext.NONE;
- // Exclude root from the saved node path.
- --level;
- NodeContext[] segments = new NodeContext[level];
- for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {
- NodeContext ctx = (NodeContext) item.getData();
- if (ctx == null)
- return NodeContext.NONE;
- segments[level-1] = ctx;
- }
- return segments;
- }
-
- /**
- * @param node
- * @return
- */
- @SuppressWarnings("unused")
- private NodeContextPath getNodeContextPath(NodeContext node) {
- NodeContext[] path = getNodeContextPathSegments(node);
- return new NodeContextPath(path);
- }
-
- void setImage(NodeContext node, TreeItem item, Imager imager, Collection decorators, int itemIndex) {
- Image[] images = columnImageArray;
- Arrays.fill(images, null);
- if (imager == null) {
- item.setImage(images);
- return;
- }
-
- Object[] descOrImage = columnDescOrImageArray;
- Arrays.fill(descOrImage, null);
- boolean finishLoadingInJob = false;
- int index = 0;
- for (Column column : columns) {
- String key = column.getKey();
- ImageDescriptor desc = imager.getImage(key);
- if (desc != null) {
- // Attempt to decorate the label
- if (!decorators.isEmpty()) {
- for (ImageDecorator id : decorators) {
- ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);
- if (ds != null)
- desc = ds;
- }
- }
-
- // Try resolving only cached images here and now
- Object img = localResourceManager.find(desc);
- if (img == null)
- img = resourceManager.find(desc);
-
- images[index] = img != null ? (Image) img : null;
- descOrImage[index] = img == null ? desc : img;
- finishLoadingInJob |= img == null;
- }
- ++index;
- }
-
- // Finish loading the final image in the image loader job if necessary.
- if (finishLoadingInJob) {
- // Prevent UI from flashing unnecessarily by reusing the old image
- // in the item if it exists.
- for (int c = 0; c < columns.length; ++c) {
- Image img = item.getImage(c);
- if (img != null)
- images[c] = img;
- }
- item.setImage(images);
-
- // Schedule loading to another thread to refrain from blocking
- // the UI with database operations.
- queueImageTask(item, new ImageTask(
- node,
- item,
- Arrays.copyOf(descOrImage, descOrImage.length)));
- } else {
- // Set any images that were resolved.
- item.setImage(images);
- }
- }
-
- private void queueImageTask(TreeItem item, ImageTask task) {
- synchronized (imageTasks) {
- imageTasks.put(item, task);
- }
- imageLoaderJob.scheduleIfNecessary(100);
- }
-
- /**
- * Invoked in a job worker thread.
- *
- * @param monitor
- * @see ImageLoaderJob
- */
- @Override
- protected IStatus setPendingImages(IProgressMonitor monitor) {
- ImageTask[] tasks = null;
- synchronized (imageTasks) {
- tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);
- imageTasks.clear();
- }
- if (tasks.length == 0)
- return Status.OK_STATUS;
-
- MultiStatus status = null;
-
- // Load missing images
- for (ImageTask task : tasks) {
- Object[] descs = task.descsOrImages;
- for (int i = 0; i < descs.length; ++i) {
- Object obj = descs[i];
- if (obj instanceof ImageDescriptor) {
- ImageDescriptor desc = (ImageDescriptor) obj;
- try {
- descs[i] = resourceManager.get((ImageDescriptor) desc);
- } catch (DeviceResourceException e) {
- if (status == null)
- status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);
- status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));
- }
- }
- }
- }
-
- // Perform final UI updates in the UI thread.
- final ImageTask[] _tasks = tasks;
- thread.asyncExec(new Runnable() {
- @Override
- public void run() {
- if (!tree.isDisposed()) {
- tree.setRedraw(false);
- setImages(_tasks);
- tree.setRedraw(true);
- }
- }
- });
-
- return status != null ? status : Status.OK_STATUS;
- }
-
- /**
- * Invoked in the UI thread only.
- *
- * @param task
- */
- void setImages(ImageTask[] tasks) {
- for (ImageTask task : tasks)
- if (task != null)
- setImage(task);
- }
-
- /**
- * Invoked in the UI thread only.
- *
- * @param task
- */
- void setImage(ImageTask task) {
- // Be sure not to process disposed items.
- if (task.item.isDisposed())
- return;
- // Discard this task if the TreeItem has switched owning NodeContext.
- if (!contextToItem.contains(task.node, task.item))
- return;
-
- Object[] descs = task.descsOrImages;
- Image[] images = columnImageArray;
- Arrays.fill(images, null);
- for (int i = 0; i < descs.length; ++i) {
- Object desc = descs[i];
- if (desc instanceof Image) {
- images[i] = (Image) desc;
- }
- }
- task.item.setImage(images);
- }
-
- void setText(TreeItem item, Labeler labeler, Collection decorators, int itemIndex) {
- if (labeler != null) {
- String[] texts = new String[columns.length];
- int index = 0;
- Map labels = labeler.getLabels();
- Map runtimeLabels = labeler.getRuntimeLabels();
- for (Column column : columns) {
- String key = column.getKey();
- String s = null;
- if (runtimeLabels != null) s = runtimeLabels.get(key);
- if (s == null) s = labels.get(key);
- if (s != null) {
- FontDescriptor font = originalFont;
- ColorDescriptor bg = originalBackground;
- ColorDescriptor fg = originalForeground;
-
- // Attempt to decorate the label
- if (!decorators.isEmpty()) {
- for (LabelDecorator ld : decorators) {
- String ds = ld.decorateLabel(s, key, itemIndex);
- if (ds != null)
- s = ds;
-
- FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
- if (dfont != null)
- font = dfont;
-
- ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
- if (dbg != null)
- bg = dbg;
-
- ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
- if (dfg != null)
- fg = dfg;
- }
- }
-
- if (font != originalFont) {
- //System.out.println("set font: " + index + ": " + font);
- item.setFont(index, (Font) localResourceManager.get(font));
- }
- if (bg != originalBackground)
- item.setBackground(index, (Color) localResourceManager.get(bg));
- if (fg != originalForeground)
- item.setForeground(index, (Color) localResourceManager.get(fg));
-
- texts[index] = s;
- }
- ++index;
- }
- item.setText(texts);
- } else {
- item.setText(Labeler.NO_LABEL);
- }
- }
-
- void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {
- Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
- if (labeler != null) {
- labeler.setListener(labelListener);
- }
- Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);
- Collection labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);
- Collection imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);
-
- setText(item, labeler, labelDecorators, itemIndex);
- setImage(context, item, imager, imageDecorators, itemIndex);
- }
-
- @Override
- public void setFocus() {
- tree.setFocus();
- }
-
- @Override
- public T query(NodeContext context, CacheKey key) {
- return this.explorerContext.cache.get(context, key);
- }
-
- @Override
- public boolean isDisposed() {
- return disposed;
- }
-
- protected void assertNotDisposed() {
- if (isDisposed())
- throw new IllegalStateException("disposed");
- }
-
-
-
- /**
- * @param selection
- * @param forceControlUpdate
- * @thread any
- */
- public void setSelection(final ISelection selection, boolean forceControlUpdate) {
- assertNotDisposed();
- boolean equalsOld = selectionProvider.selectionEquals(selection);
- if (equalsOld && !forceControlUpdate) {
- // Just set the selection object instance, fire no events nor update
- // the viewer selection.
- selectionProvider.setSelection(selection);
- } else {
- // Schedule viewer and selection update if necessary.
- if (tree.isDisposed())
- return;
- Display d = tree.getDisplay();
- if (d.getThread() == Thread.currentThread()) {
- updateSelectionToControl(selection);
- } else {
- d.asyncExec(new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed())
- return;
- updateSelectionToControl(selection);
- }
- });
- }
- }
- }
-
-
- /* Contains the best currently found tree item and its priority
- */
- private static class SelectionResolutionStatus {
- int bestPriority = Integer.MAX_VALUE;
- TreeItem bestItem;
- }
-
- /**
- * @param selection
- * @thread SWT
- */
- private void updateSelectionToControl(ISelection selection) {
- if (selectionDataResolver == null)
- return;
- if (!(selection instanceof IStructuredSelection))
- return;
-
- // Initialize selection resolution status map
- IStructuredSelection iss = (IStructuredSelection) selection;
- final THashMap statusMap =
- new THashMap(iss.size());
- for(Iterator> it = iss.iterator(); it.hasNext();) {
- Object selectionElement = it.next();
- Object resolvedElement = selectionDataResolver.resolve(selectionElement);
- statusMap.put(
- resolvedElement,
- new SelectionResolutionStatus());
- }
-
- // Iterate all tree items and try to match them to the selection
- iterateTreeItems(new TObjectProcedure() {
- @Override
- public boolean execute(TreeItem treeItem) {
- NodeContext nodeContext = (NodeContext)treeItem.getData();
- if(nodeContext == null)
- return true;
- SelectionResolutionStatus status = statusMap.get(nodeContext);
- if(status != null) {
- status.bestPriority = 0; // best possible match
- status.bestItem = treeItem;
- return true;
- }
-
- Object input = nodeContext.getConstant(BuiltinKeys.INPUT);
- status = statusMap.get(input);
- if(status != null) {
- NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);
- int curPriority = nodeType instanceof EntityNodeType
- ? 1 // Prefer EntityNodeType matches to other node types
- : 2;
- if(curPriority < status.bestPriority) {
- status.bestPriority = curPriority;
- status.bestItem = treeItem;
- }
- }
- return true;
- }
- });
-
- // Update selection
- ArrayList items = new ArrayList(statusMap.size());
- for(SelectionResolutionStatus status : statusMap.values())
- if(status.bestItem != null)
- items.add(status.bestItem);
- select(items.toArray(new TreeItem[items.size()]));
- }
-
- /**
- * @thread SWT
- */
- public ISelection getWidgetSelection() {
- TreeItem[] items = tree.getSelection();
- if (items.length == 0)
- return StructuredSelection.EMPTY;
-
- List nodes = new ArrayList(items.length);
-
- // Caches for resolving node contexts the hard way if necessary.
- GENodeQueryManager manager = null;
- NodeContext lastParentContext = null;
- NodeContext[] lastChildren = null;
-
- for (int i = 0; i < items.length; i++) {
- TreeItem item = items[i];
- NodeContext ctx = (NodeContext) item.getData();
- // It may happen due to the virtual nature of the tree control
- // that it contains TreeItems which have not yet been ran through
- // #setData(Event).
- if (ctx != null) {
- nodes.add(ctx);
- } else {
- TreeItem parentItem = item.getParentItem();
- NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;
- if (parentContext != null) {
- NodeContext[] children = lastChildren;
- if (parentContext != lastParentContext) {
- if (manager == null)
- manager = new GENodeQueryManager(this.explorerContext, null, null, null);
- lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
- lastParentContext = parentContext;
- }
- int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);
- if (index >= 0 && index < children.length) {
- NodeContext child = children[index];
- if (child != null) {
- nodes.add(child);
- // Cache NodeContext in TreeItem for faster access
- item.setData(child);
- }
- }
- }
- }
- }
- //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
- ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
- return selection;
- }
-
- @Override
- public TransientExplorerState getTransientState() {
- if (!thread.currentThreadAccess())
- throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());
- return transientState;
- }
-
- /**
- * @param item
- * @thread SWT
- */
- private void select(TreeItem item) {
- tree.setSelection(item);
- tree.showSelection();
- selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
- }
-
- /**
- * @param items
- * @thread SWT
- */
- private void select(TreeItem[] items) {
- //System.out.println("Select: " + Arrays.toString(items));
- tree.setSelection(items);
- tree.showSelection();
- NodeContext[] data = new NodeContext[items.length];
- for (int i = 0; i < data.length; i++) {
- data[i] = (NodeContext) items[i].getData();
- }
- selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
- }
-
- private void iterateTreeItems(TObjectProcedure procedure) {
- for(TreeItem item : tree.getItems())
- if(!iterateTreeItems(item, procedure))
- return;
- }
-
- private boolean iterateTreeItems(TreeItem item,
- TObjectProcedure procedure) {
- if(!procedure.execute(item))
- return false;
- if(item.getExpanded())
- for(TreeItem child : item.getItems())
- if(!iterateTreeItems(child, procedure))
- return false;
- return true;
- }
-
- /**
- * @param item
- * @param context
- * @return
- */
- private boolean trySelect(TreeItem item, Object input) {
- NodeContext itemCtx = (NodeContext) item.getData();
- if (itemCtx != null) {
- if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {
- select(item);
- return true;
- }
- }
- if (item.getExpanded()) {
- for (TreeItem child : item.getItems()) {
- if (trySelect(child, input))
- return true;
- }
- }
- return false;
- }
-
- private boolean equalsEnough(NodeContext c1, NodeContext c2) {
-
- Object input1 = c1.getConstant(BuiltinKeys.INPUT);
- Object input2 = c2.getConstant(BuiltinKeys.INPUT);
- if(!ObjectUtils.objectEquals(input1, input2))
- return false;
-
- Object type1 = c1.getConstant(NodeType.TYPE);
- Object type2 = c2.getConstant(NodeType.TYPE);
- if(!ObjectUtils.objectEquals(type1, type2))
- return false;
-
- return true;
-
- }
-
- private NodeContext tryFind(NodeContext context) {
- for (TreeItem item : tree.getItems()) {
- NodeContext found = tryFind(item, context);
- if(found != null) return found;
- }
- return null;
- }
-
- private NodeContext tryFind(TreeItem item, NodeContext context) {
- NodeContext itemCtx = (NodeContext) item.getData();
- if (itemCtx != null) {
- if (equalsEnough(context, itemCtx)) {
- return itemCtx;
- }
- }
- if (item.getExpanded()) {
- for (TreeItem child : item.getItems()) {
- NodeContext found = tryFind(child, context);
- if(found != null) return found;
- }
- }
- return null;
- }
-
- @Override
- public boolean select(NodeContext context) {
-
- assertNotDisposed();
-
- if (context == null || context.equals(rootContext)) {
- tree.deselectAll();
- selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
- return true;
- }
-
-// if (context.equals(rootContext)) {
-// tree.deselectAll();
-// selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
-// return;
-// }
-
- Object input = context.getConstant(BuiltinKeys.INPUT);
-
- for (TreeItem item : tree.getItems()) {
- if (trySelect(item, input))
- return true;
- }
-
- return false;
-
- }
-
- private NodeContext tryFind2(NodeContext context) {
- Set ctxs = contextToItem.getLeftSet();
- for(NodeContext c : ctxs)
- if(equalsEnough(c, context))
- return c;
- return null;
- }
-
- private boolean waitVisible(NodeContext parent, NodeContext context) {
- long start = System.nanoTime();
-
- TreeItem parentItem = contextToItem.getRight(parent);
-
- if(parentItem == null)
- return false;
-
- while(true) {
- NodeContext target = tryFind2(context);
- if(target != null) {
- TreeItem item = contextToItem.getRight(target);
- if (!(item.getParentItem().equals(parentItem)))
- return false;
- tree.setTopItem(item);
- return true;
- }
-
- Display.getCurrent().readAndDispatch();
- long duration = System.nanoTime() - start;
- if(duration > 10e9)
- return false;
- }
- }
-
- private boolean selectPathInternal(NodeContext[] contexts, int position) {
- //System.out.println("NodeContext path : " + contexts);
-
- NodeContext head = tryFind(contexts[position]);
-
- if(position == contexts.length-1) {
- return select(head);
-
- }
-
- //setExpanded(head, true);
-
- if(!waitVisible(head, contexts[position+1]))
- return false;
-
- setExpanded(head, true);
-
- return selectPathInternal(contexts, position+1);
-
- }
-
- @Override
- public boolean selectPath(Collection contexts) {
-
- if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");
- if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");
-
- return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
-
- }
-
- @Override
- public boolean isVisible(NodeContext context) {
-
- for (TreeItem item : tree.getItems()) {
- NodeContext found = tryFind(item, context);
- if(found != null)
- return true;
- }
-
- return false;
-
- }
-
- protected ISelection constructSelection(NodeContext... contexts) {
- if (contexts == null)
- throw new IllegalArgumentException("null contexts");
- if (contexts.length == 0)
- return StructuredSelection.EMPTY;
- if (selectionFilter == null)
- return new StructuredSelection(transformSelection(contexts));
- return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );
- }
-
- protected Object[] transformSelection(Object[] objects) {
- return selectionTransformation.call(this, objects);
- }
-
- protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
- int len = contexts.length;
- Object[] objects = new Object[len];
- for (int i = 0; i < len; ++i)
- objects[i] = filter.filter(contexts[i]);
- return objects;
- }
-
- @Override
- public void setExpanded(final NodeContext context, final boolean expanded) {
- assertNotDisposed();
- ThreadUtils.asyncExec(thread, new Runnable() {
- @Override
- public void run() {
- if (!isDisposed())
- doSetExpanded(context, expanded);
- }
- });
- }
-
- private void doSetExpanded(NodeContext context, boolean expanded) {
- //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
- TreeItem item = contextToItem.getRight(context);
- if (item != null) {
- item.setExpanded(expanded);
- }
- PrimitiveQueryProcessor> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
- if (pqp instanceof IsExpandedProcessor) {
- IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
- iep.replaceExpanded(context, expanded);
- }
- }
-
- @Override
- public void setColumnsVisible(boolean visible) {
- columnsAreVisible = visible;
- if(tree != null) tree.setHeaderVisible(columnsAreVisible);
- }
-
- @Override
- public void setColumns(final Column[] columns) {
- setColumns(columns, null);
- }
-
- @Override
- public void setColumns(final Column[] columns, Consumer> callback) {
- assertNotDisposed();
- checkUniqueColumnKeys(columns);
-
- Display d = tree.getDisplay();
- if (d.getThread() == Thread.currentThread())
- doSetColumns(columns, callback);
- else
- d.asyncExec(() -> {
- if (tree.isDisposed())
- return;
- doSetColumns(columns, callback);
- });
- }
-
- private void checkUniqueColumnKeys(Column[] cols) {
- Set usedColumnKeys = new HashSet();
- List duplicateColumns = new ArrayList();
- for (Column c : cols) {
- if (!usedColumnKeys.add(c.getKey()))
- duplicateColumns.add(c);
- }
- if (!duplicateColumns.isEmpty()) {
- throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
- }
- }
-
- /**
- * Only meant to be invoked from the SWT UI thread.
- *
- * @param cols
- */
- private void doSetColumns(Column[] cols, Consumer> callback) {
- // Attempt to keep previous column widths.
- Map prevWidths = new HashMap();
- for (TreeColumn column : tree.getColumns()) {
- prevWidths.put(column.getText(), column.getWidth());
- column.dispose();
- }
-
- HashMap keyToIndex = new HashMap();
- for (int i = 0; i < cols.length; ++i) {
- keyToIndex.put(cols[i].getKey(), i);
- }
-
- this.columns = Arrays.copyOf(cols, cols.length);
- //this.columns[cols.length] = FILLER_COLUMN;
- this.columnKeyToIndex = keyToIndex;
- this.columnImageArray = new Image[cols.length];
- this.columnDescOrImageArray = new Object[cols.length];
-
- Map map = new HashMap();
-
- tree.setHeaderVisible(columnsAreVisible);
- for (Column column : columns) {
- TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));
- map.put(column, c);
- c.setData(column);
- c.setText(column.getLabel());
- c.setToolTipText(column.getTooltip());
-
- int cw = column.getWidth();
-
- // Try to keep previous widths
- Integer w = prevWidths.get(column);
- if (w != null)
- c.setWidth(w);
- else if (cw != Column.DEFAULT_CONTROL_WIDTH)
- c.setWidth(cw);
- else {
- // Go for some kind of default settings then...
- if (ColumnKeys.PROPERTY.equals(column.getKey()))
- c.setWidth(150);
- else
- c.setWidth(50);
- }
-
-// if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
-// c.addListener(SWT.Resize, resizeListener);
-// c.setResizable(true);
-// } else {
-// //c.setResizable(false);
-// }
-
- }
-
- if(callback != null) callback.accept(map);
-
- // Make sure the explorer fits the columns properly after initialization.
- tree.getDisplay().asyncExec(new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed())
- return;
- refreshColumnSizes();
- }
- });
- }
-
- int toSWT(Align alignment) {
- switch (alignment) {
- case LEFT: return SWT.LEFT;
- case CENTER: return SWT.CENTER;
- case RIGHT: return SWT.RIGHT;
- default: throw new Error("unhandled alignment: " + alignment);
- }
- }
-
- @Override
- public Column[] getColumns() {
- return Arrays.copyOf(columns, columns.length);
- }
-
- private void detachPrimitiveProcessors() {
- for (PrimitiveQueryProcessor> p : primitiveProcessors.values()) {
- if (p instanceof ProcessorLifecycle) {
- ((ProcessorLifecycle) p).detached(this);
- }
- }
- }
-
- private void clearPrimitiveProcessors() {
- for (PrimitiveQueryProcessor> p : primitiveProcessors.values()) {
- if (p instanceof ProcessorLifecycle) {
- ((ProcessorLifecycle) p).clear();
- }
- }
- }
-
- Listener resizeListener = new Listener() {
- @Override
- public void handleEvent(Event event) {
- // Prevent infinite recursion.
- if (refreshingColumnSizes)
- return;
- //TreeColumn column = (TreeColumn) event.widget;
- //Column c = (Column) column.getData();
- refreshColumnSizes();
- }
- };
-
- Listener itemDisposeListener = new Listener() {
- @Override
- public void handleEvent(Event event) {
- if (event.type == SWT.Dispose) {
- if (event.widget instanceof TreeItem) {
- TreeItem ti = (TreeItem) event.widget;
- //NodeContext ctx = (NodeContext) ti.getData();
-// System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));
-// System.out.println(" map size BEFORE: " + contextToItem.size());
- @SuppressWarnings("unused")
- NodeContext removed = contextToItem.removeWithRight(ti);
-// System.out.println(" REMOVED: " + removed);
-// System.out.println(" map size AFTER: " + contextToItem.size());
- }
- }
- }
- };
-
- /**
- *
- */
- LabelerListener labelListener = new LabelerListener() {
- @Override
- public boolean columnModified(final NodeContext context, final String key, final String newLabel) {
- //System.out.println("column " + key + " modified for " + context + " to " + newLabel);
- if (tree.isDisposed())
- return false;
-
- synchronized (labelRefreshRunnables) {
- Runnable refresher = new Runnable() {
- @Override
- public void run() {
- // Tree is guaranteed to be non-disposed if this is invoked.
-
- // contextToItem should be accessed only in the SWT thread to keep things thread-safe.
- final TreeItem item = contextToItem.getRight(context);
- if (item == null || item.isDisposed())
- return;
-
- final Integer index = columnKeyToIndex.get(key);
- if (index == null)
- return;
-
- //System.out.println(" found index: " + index);
- //System.out.println(" found item: " + item);
- try {
- GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
-
- // FIXME: indexOf is quadratic
- int itemIndex = 0;
- TreeItem parentItem = item.getParentItem();
- if (parentItem == null) {
- itemIndex = tree.indexOf(item);
- //tree.clear(parentIndex, false);
- } else {
- itemIndex = parentItem.indexOf(item);
- //item.clear(parentIndex, false);
- }
- setTextAndImage(item, manager, context, itemIndex);
- } catch (SWTException e) {
- ErrorLogger.defaultLogError(e);
- }
- }
- };
- //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
- labelRefreshRunnables.put(context, refresher);
-
- if (!refreshIsQueued) {
- refreshIsQueued = true;
- long delay = 0;
- long now = System.currentTimeMillis();
- long elapsed = now - lastLabelRefreshScheduled;
- if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)
- delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;
- //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");
- if (delay > 0) {
- ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
- @Override
- public void run() {
- scheduleImmediateLabelRefresh();
- }
- }, delay, TimeUnit.MILLISECONDS);
- } else {
- scheduleImmediateLabelRefresh();
- }
- lastLabelRefreshScheduled = now;
- }
- }
- return true;
- }
-
- @Override
- public boolean columnsModified(final NodeContext context, final Map columns) {
- System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
- return false;
- }
- };
-
- private void scheduleImmediateLabelRefresh() {
- Runnable[] runnables = null;
- synchronized (labelRefreshRunnables) {
- if (labelRefreshRunnables.isEmpty())
- return;
-
- runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
- labelRefreshRunnables.clear();
- refreshIsQueued = false;
- }
- final Runnable[] rs = runnables;
-
- if (tree.isDisposed())
- return;
- tree.getDisplay().asyncExec(new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed())
- return;
- //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");
- tree.setRedraw(false);
- for (Runnable r : rs) {
- r.run();
- }
- tree.setRedraw(true);
- }
- });
- }
-
- long lastLabelRefreshScheduled = 0;
- boolean refreshIsQueued = false;
- Map labelRefreshRunnables = new HashMap();
-
- @SuppressWarnings("unchecked")
- @Override
- public T getAdapter(Class adapter) {
- if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;
- else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;
- return null;
- }
-
- @SuppressWarnings("unchecked")
- @Override
- public T getControl() {
- return (T) tree;
- }
-
- /* (non-Javadoc)
- * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
- */
- @Override
- public void setAutoExpandLevel(int level) {
- this.autoExpandLevel = level;
- }
-
- @Override
- public NodeQueryProcessor getProcessor(QueryKey key) {
- return explorerContext.getProcessor(key);
- }
-
- @Override
- public PrimitiveQueryProcessor getPrimitiveProcessor(PrimitiveQueryKey key) {
- return explorerContext.getPrimitiveProcessor(key);
- }
-
- @Override
- public boolean isEditable() {
- return editable;
- }
-
- @Override
- public void setEditable(boolean editable) {
- if (!thread.currentThreadAccess())
- throw new IllegalStateException("not in SWT display thread " + thread.getThread());
-
- this.editable = editable;
- Display display = tree.getDisplay();
- tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
- }
-
- /**
- * For setting a more local service locator for the explorer than the global
- * workbench service locator. Sometimes required to give this implementation
- * access to local workbench services like IFocusService.
- *
- *
- * Must be invoked during right after construction.
- *
- * @param serviceLocator
- * a specific service locator or null
to use the
- * workbench global service locator
- */
- public void setServiceLocator(IServiceLocator serviceLocator) {
- if (serviceLocator == null && PlatformUI.isWorkbenchRunning())
- serviceLocator = PlatformUI.getWorkbench();
- this.serviceLocator = serviceLocator;
- if (serviceLocator != null) {
- this.contextService = (IContextService) serviceLocator.getService(IContextService.class);
- this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);
- }
- }
-
-}
+/*******************************************************************************
+ * Copyright (c) 2007, 2012 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.browsing.ui.swt;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.AssertionFailedException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.MultiStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.action.IStatusLineManager;
+import org.eclipse.jface.resource.ColorDescriptor;
+import org.eclipse.jface.resource.DeviceResourceException;
+import org.eclipse.jface.resource.DeviceResourceManager;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.resource.ResourceManager;
+import org.eclipse.jface.viewers.IPostSelectionProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.TreeEditor;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchSite;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.contexts.IContextActivation;
+import org.eclipse.ui.contexts.IContextService;
+import org.eclipse.ui.services.IServiceLocator;
+import org.eclipse.ui.swt.IFocusService;
+import org.simantics.browsing.ui.BuiltinKeys;
+import org.simantics.browsing.ui.CheckedState;
+import org.simantics.browsing.ui.Column;
+import org.simantics.browsing.ui.Column.Align;
+import org.simantics.browsing.ui.DataSource;
+import org.simantics.browsing.ui.ExplorerState;
+import org.simantics.browsing.ui.GraphExplorer;
+import org.simantics.browsing.ui.NodeContext;
+import org.simantics.browsing.ui.NodeContext.CacheKey;
+import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey;
+import org.simantics.browsing.ui.NodeContext.QueryKey;
+import org.simantics.browsing.ui.NodeContextPath;
+import org.simantics.browsing.ui.NodeQueryManager;
+import org.simantics.browsing.ui.NodeQueryProcessor;
+import org.simantics.browsing.ui.PrimitiveQueryProcessor;
+import org.simantics.browsing.ui.SelectionDataResolver;
+import org.simantics.browsing.ui.SelectionFilter;
+import org.simantics.browsing.ui.StatePersistor;
+import org.simantics.browsing.ui.common.AdaptableHintContext;
+import org.simantics.browsing.ui.common.ColumnKeys;
+import org.simantics.browsing.ui.common.ErrorLogger;
+import org.simantics.browsing.ui.common.NodeContextBuilder;
+import org.simantics.browsing.ui.common.NodeContextUtil;
+import org.simantics.browsing.ui.common.internal.GECache;
+import org.simantics.browsing.ui.common.internal.GENodeQueryManager;
+import org.simantics.browsing.ui.common.internal.IGECache;
+import org.simantics.browsing.ui.common.internal.IGraphExplorerContext;
+import org.simantics.browsing.ui.common.internal.UIElementReference;
+import org.simantics.browsing.ui.common.processors.DefaultCheckedStateProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultComparableChildrenProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultFinalChildrenProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultImageDecoratorProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultImagerFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultImagerProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultLabelDecoratorProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultLabelerFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultLabelerProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultPrunedChildrenProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedImageDecoratorFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelDecoratorFactoriesProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelerProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointFactoryProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionsProcessor;
+import org.simantics.browsing.ui.common.processors.DefaultViewpointProcessor;
+import org.simantics.browsing.ui.common.processors.IsExpandedProcessor;
+import org.simantics.browsing.ui.common.processors.NoSelectionRequestProcessor;
+import org.simantics.browsing.ui.common.processors.ProcessorLifecycle;
+import org.simantics.browsing.ui.common.state.ExplorerStates;
+import org.simantics.browsing.ui.content.ImageDecorator;
+import org.simantics.browsing.ui.content.Imager;
+import org.simantics.browsing.ui.content.LabelDecorator;
+import org.simantics.browsing.ui.content.Labeler;
+import org.simantics.browsing.ui.content.Labeler.CustomModifier;
+import org.simantics.browsing.ui.content.Labeler.DeniedModifier;
+import org.simantics.browsing.ui.content.Labeler.DialogModifier;
+import org.simantics.browsing.ui.content.Labeler.EnumerationModifier;
+import org.simantics.browsing.ui.content.Labeler.FilteringModifier;
+import org.simantics.browsing.ui.content.Labeler.LabelerListener;
+import org.simantics.browsing.ui.content.Labeler.Modifier;
+import org.simantics.browsing.ui.content.PrunedChildrenResult;
+import org.simantics.browsing.ui.model.nodetypes.EntityNodeType;
+import org.simantics.browsing.ui.model.nodetypes.NodeType;
+import org.simantics.browsing.ui.swt.internal.Threads;
+import org.simantics.db.layer0.SelectionHints;
+import org.simantics.utils.ObjectUtils;
+import org.simantics.utils.datastructures.BijectionMap;
+import org.simantics.utils.datastructures.disposable.AbstractDisposable;
+import org.simantics.utils.datastructures.hints.IHintContext;
+import org.simantics.utils.threads.IThreadWorkQueue;
+import org.simantics.utils.threads.SWTThread;
+import org.simantics.utils.threads.ThreadUtils;
+import org.simantics.utils.ui.ISelectionUtils;
+import org.simantics.utils.ui.SWTUtils;
+import org.simantics.utils.ui.jface.BasePostSelectionProvider;
+import org.simantics.utils.ui.widgets.VetoingEventHandler;
+import org.simantics.utils.ui.workbench.WorkbenchUtils;
+
+import gnu.trove.map.hash.THashMap;
+import gnu.trove.procedure.TObjectProcedure;
+import gnu.trove.set.hash.THashSet;
+
+/**
+ * @see #getMaxChildren()
+ * @see #setMaxChildren(int)
+ * @see #getMaxChildren(NodeQueryManager, NodeContext)
+ */
+class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ {
+
+ private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider {
+
+ private GraphExplorerImpl ge;
+
+ GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) {
+ this.ge = ge;
+ }
+
+ void dispose() {
+ ge = null;
+ }
+
+ @Override
+ public void setSelection(final ISelection selection) {
+ if(ge == null) return;
+ ge.setSelection(selection, false);
+ }
+
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ if(ge == null) return;
+ if(ge.isDisposed()) {
+ if (DEBUG_SELECTION_LISTENERS)
+ System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener);
+ return;
+ }
+ //assertNotDisposed();
+ //System.out.println("Remove selection changed listener: " + listener);
+ ge.selectionProvider.removeSelectionChangedListener(listener);
+ }
+
+ @Override
+ public void addPostSelectionChangedListener(ISelectionChangedListener listener) {
+ if(ge == null) return;
+ if (!ge.thread.currentThreadAccess())
+ throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
+ if(ge.isDisposed()) {
+ System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener);
+ return;
+ }
+ //System.out.println("Add POST selection changed listener: " + listener);
+ ge.selectionProvider.addPostSelectionChangedListener(listener);
+ }
+
+ @Override
+ public void removePostSelectionChangedListener(ISelectionChangedListener listener) {
+ if(ge == null) return;
+ if(ge.isDisposed()) {
+ if (DEBUG_SELECTION_LISTENERS)
+ System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener);
+ return;
+ }
+// assertNotDisposed();
+ //System.out.println("Remove POST selection changed listener: " + listener);
+ ge.selectionProvider.removePostSelectionChangedListener(listener);
+ }
+
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ if(ge == null) return;
+ if (!ge.thread.currentThreadAccess())
+ throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread());
+ //System.out.println("Add selection changed listener: " + listener);
+ if (ge.tree.isDisposed() || ge.selectionProvider == null) {
+ System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener);
+ return;
+ }
+
+ ge.selectionProvider.addSelectionChangedListener(listener);
+ }
+
+
+ @Override
+ public ISelection getSelection() {
+ if(ge == null) return StructuredSelection.EMPTY;
+ if (!ge.thread.currentThreadAccess())
+ throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread());
+ if (ge.tree.isDisposed() || ge.selectionProvider == null)
+ return StructuredSelection.EMPTY;
+ return ge.selectionProvider.getSelection();
+ }
+
+ }
+
+ /**
+ * If this explorer is running with an Eclipse workbench open, this
+ * Workbench UI context will be activated whenever inline editing is started
+ * through {@link #startEditing(TreeItem, int)} and deactivated when inline
+ * editing finishes.
+ *
+ * This context information can be used to for UI handler activity testing.
+ */
+ private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing";
+
+ private static final String KEY_DRAG_COLUMN = "dragColumn";
+
+ private static final boolean DEBUG_SELECTION_LISTENERS = false;
+
+ private static final int DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200;
+
+ public static final int DEFAULT_MAX_CHILDREN = 1000;
+
+ private static final long POST_SELECTION_DELAY = 300;
+
+ /**
+ * The time in milliseconds that must elapse between consecutive
+ * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)}
+ * invocations in order for this class to construct a new selection.
+ *
+ *
+ * This is done because selection construction can be very expensive as the
+ * selected set grows larger when the user is pressing shift+arrow keys.
+ * GraphExplorerImpl will naturally listen to all changes in the tree
+ * selection, but as an optimization will not construct new
+ * StructuredSelection instances for every selection change event. A new
+ * selection will be constructed and set only if the selection hasn't
+ * changed for the amount of milliseconds specified by this constant.
+ */
+ private static final long SELECTION_CHANGE_QUIET_TIME = 150;
+
+ private final IThreadWorkQueue thread;
+
+ /**
+ * Local method for checking from whether resources are loaded in
+ * JFaceResources.
+ */
+ private final LocalResourceManager localResourceManager;
+
+ /**
+ * Local device resource manager that is safe to use in
+ * {@link ImageLoaderJob} for creating images in a non-UI thread.
+ */
+ private final ResourceManager resourceManager;
+
+ /*
+ * Package visibility.
+ * TODO: Get rid of these.
+ */
+ Tree tree;
+
+ @SuppressWarnings({ "rawtypes" })
+ final HashMap, NodeQueryProcessor> processors = new HashMap, NodeQueryProcessor>();
+ @SuppressWarnings({ "rawtypes" })
+ final HashMap primitiveProcessors = new HashMap();
+ @SuppressWarnings({ "rawtypes" })
+ final HashMap dataSources = new HashMap();
+
+ class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
+ // This is for query debugging only.
+ int queryIndent = 0;
+
+ GECache cache = new GECache();
+ AtomicBoolean propagating = new AtomicBoolean(false);
+ Object propagateList = new Object();
+ Object propagate = new Object();
+ List scheduleList = new ArrayList();
+ final Deque activity = new LinkedList();
+ int activityInt = 0;
+
+ /**
+ * Stores the currently running query update runnable. If
+ * null
there's nothing scheduled yet in which case
+ * scheduling can commence. Otherwise the update should be skipped.
+ */
+ AtomicReference currentQueryUpdater = new AtomicReference();
+
+ /**
+ * Keeps track of nodes that have already been auto-expanded. After
+ * being inserted into this set, nodes will not be forced to stay in an
+ * expanded state after that. This makes it possible for the user to
+ * close auto-expanded nodes.
+ */
+ Map autoExpanded = new WeakHashMap();
+
+
+ @Override
+ protected void doDispose() {
+ saveState();
+ autoExpanded.clear();
+ }
+
+ @Override
+ public IGECache getCache() {
+ return cache;
+ }
+
+ @Override
+ public int queryIndent() {
+ return queryIndent;
+ }
+
+ @Override
+ public int queryIndent(int offset) {
+ queryIndent += offset;
+ return queryIndent;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public NodeQueryProcessor getProcessor(Object o) {
+ return processors.get(o);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public PrimitiveQueryProcessor getPrimitiveProcessor(Object o) {
+ return primitiveProcessors.get(o);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public DataSource getDataSource(Class clazz) {
+ return dataSources.get(clazz);
+ }
+
+ @Override
+ public void update(UIElementReference ref) {
+ //System.out.println("GE.update " + ref);
+ TreeItemReference tiref = (TreeItemReference) ref;
+ TreeItem item = tiref.getItem();
+ // NOTE: must be called regardless of the the item value.
+ // A null item is currently used to indicate a tree root update.
+ GraphExplorerImpl.this.update(item);
+ }
+
+ @Override
+ public Object getPropagateLock() {
+ return propagate;
+ }
+
+ @Override
+ public Object getPropagateListLock() {
+ return propagateList;
+ }
+
+ @Override
+ public boolean isPropagating() {
+ return propagating.get();
+ }
+
+ @Override
+ public void setPropagating(boolean b) {
+ this.propagating.set(b);
+ }
+
+ @Override
+ public List getScheduleList() {
+ return scheduleList;
+ }
+
+ @Override
+ public void setScheduleList(List list) {
+ this.scheduleList = list;
+ }
+
+ @Override
+ public Deque getActivity() {
+ return activity;
+ }
+
+ @Override
+ public void setActivityInt(int i) {
+ this.activityInt = i;
+ }
+
+ @Override
+ public int getActivityInt() {
+ return activityInt;
+ }
+
+ @Override
+ public void scheduleQueryUpdate(Runnable r) {
+ if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown())
+ return;
+ //System.out.println("Scheduling query update for runnable " + r);
+ if (currentQueryUpdater.compareAndSet(null, r)) {
+ //System.out.println("Scheduling query update for runnable " + r);
+ queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER);
+ }
+ }
+
+ Runnable QUERY_UPDATE_SCHEDULER = new Runnable() {
+ @Override
+ public void run() {
+ Runnable r = currentQueryUpdater.getAndSet(null);
+ if (r != null) {
+ //System.out.println("Running query update runnable " + r);
+ r.run();
+ }
+ }
+ };
+ }
+
+ GraphExplorerContext explorerContext = new GraphExplorerContext();
+
+ HashSet pendingItems = new HashSet();
+ boolean updating = false;
+ boolean pendingRoot = false;
+
+ @SuppressWarnings("deprecation")
+ ModificationContext modificationContext = null;
+
+ NodeContext rootContext;
+
+ StatePersistor persistor = null;
+
+ boolean editable = true;
+
+ /**
+ * This is a reverse mapping from {@link NodeContext} tree objects back to
+ * their owner TreeItems.
+ *
+ *
+ * Access this map only in the SWT thread to keep it thread-safe.
+ *
+ */
+ BijectionMap contextToItem = new BijectionMap();
+
+ /**
+ * Columns of the UI viewer. Use {@link #setColumns(Column[])} to
+ * initialize.
+ */
+ Column[] columns = new Column[0];
+ Map columnKeyToIndex = new HashMap();
+ boolean refreshingColumnSizes = false;
+ boolean columnsAreVisible = true;
+
+ /**
+ * An array reused for invoking {@link TreeItem#setImage(Image[])} instead
+ * of constantly allocating new arrays for setting each TreeItems images.
+ * This works because {@link TreeItem#setImage(Image[])} does not take hold
+ * of the array itself, only the contents of the array.
+ *
+ * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
+ */
+ Image[] columnImageArray = { null };
+
+ /**
+ * Used for collecting Image or ImageDescriptor instances for a single
+ * TreeItem when initially setting images for a TreeItem.
+ *
+ * @see #setImage(NodeContext, TreeItem, Imager, Collection, int)
+ */
+ Object[] columnDescOrImageArray = { null };
+
+ final ExecutorService queryUpdateScheduler = Threads.getExecutor();
+ final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor();
+
+ /** Set to true when the Tree widget is disposed. */
+ private boolean disposed = false;
+ private final CopyOnWriteArrayList focusListeners = new CopyOnWriteArrayList();
+ private final CopyOnWriteArrayList mouseListeners = new CopyOnWriteArrayList();
+ private final CopyOnWriteArrayList keyListeners = new CopyOnWriteArrayList();
+
+ /** Selection provider */
+ private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
+ protected BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider();
+ protected SelectionDataResolver selectionDataResolver;
+ protected SelectionFilter selectionFilter;
+ protected BiFunction selectionTransformation = new BiFunction() {
+
+ @Override
+ public Object[] apply(GraphExplorer explorer, Object[] objects) {
+ Object[] result = new Object[objects.length];
+ for (int i = 0; i < objects.length; i++) {
+ IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
+ context.setHint(SelectionHints.KEY_MAIN, objects[i]);
+ result[i] = context;
+ }
+ return result;
+ }
+
+ };
+ protected FontDescriptor originalFont;
+ protected ColorDescriptor originalForeground;
+ protected ColorDescriptor originalBackground;
+
+ /**
+ * The set of currently selected TreeItem instances. This set is needed
+ * because we need to know in {@link #setData(Event)} whether the updated
+ * item was a part of the current selection in which case the selection must
+ * be updated.
+ */
+ private final Map selectedItems = new HashMap();
+
+ /**
+ * TODO: specify what this is for
+ */
+ private final Set selectionRefreshContexts = new HashSet();
+
+ /**
+ * If this field is non-null, it means that if {@link #setData(Event)}
+ * encounters a NodeContext equal to this one, it must make the TreeItem
+ * assigned to that NodeContext the topmost item of the tree using
+ * {@link Tree#setTopItem(TreeItem)}. After this the field value is
+ * nullified.
+ *
+ *
+ * This is related to {@link #initializeState()}, i.e. explorer state
+ * restoration.
+ */
+// private NodeContext[] topNodePath = NodeContext.NONE;
+// private int[] topNodePath = {};
+// private int currentTopNodePathIndex = -1;
+
+ /**
+ * See {@link #setAutoExpandLevel(int)}
+ */
+ private int autoExpandLevel = 0;
+
+ /**
+ * null
if not explicitly set through
+ * {@link #setServiceLocator(IServiceLocator)}.
+ */
+ private IServiceLocator serviceLocator;
+
+ /**
+ * The global workbench context service, if the workbench is available.
+ * Retrieved in the constructor.
+ */
+ private IContextService contextService = null;
+
+ /**
+ * The global workbench IFocusService, if the workbench is available.
+ * Retrieved in the constructor.
+ */
+ private IFocusService focusService = null;
+
+ /**
+ * A Workbench UI context activation that is activated when starting inline
+ * editing through {@link #startEditing(TreeItem, int)}.
+ *
+ * @see #activateEditingContext()
+ * @see #deactivateEditingContext()
+ */
+ private IContextActivation editingContext = null;
+
+ static class ImageTask {
+ NodeContext node;
+ TreeItem item;
+ Object[] descsOrImages;
+ public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
+ this.node = node;
+ this.item = item;
+ this.descsOrImages = descsOrImages;
+ }
+ }
+
+ /**
+ * The job that is used for off-loading image loading tasks (see
+ * {@link ImageTask} to a worker thread from the main UI thread.
+ *
+ * @see #setPendingImages(IProgressMonitor)
+ */
+ ImageLoaderJob imageLoaderJob;
+
+ /**
+ * The set of currently gathered up image loading tasks for
+ * {@link #imageLoaderJob} to execute.
+ *
+ * @see #setPendingImages(IProgressMonitor)
+ */
+ Map imageTasks = new THashMap();
+
+ /**
+ * A state flag indicating whether the vertical scroll bar was visible for
+ * {@link #tree} the last time it was checked. Since there is no listener
+ * that can provide this information, we check it in {@link #setData(Event)}
+ * every time any data for a TreeItem is updated. If the visibility changes,
+ * we will force re-layouting of the tree's parent composite.
+ *
+ * @see #setData(Event)
+ */
+ private boolean verticalBarVisible = false;
+
+ static class TransientStateImpl implements TransientExplorerState {
+
+ private Integer activeColumn = null;
+
+ @Override
+ public synchronized Integer getActiveColumn() {
+ return activeColumn;
+ }
+
+ public synchronized void setActiveColumn(Integer column) {
+ activeColumn = column;
+ }
+
+ }
+
+ private TransientStateImpl transientState = new TransientStateImpl();
+
+ boolean scheduleUpdater() {
+
+ if (tree.isDisposed())
+ return false;
+
+ if (pendingRoot == true || !pendingItems.isEmpty()) {
+ assert(!tree.isDisposed());
+
+ int activity = explorerContext.activityInt;
+ long delay = 30;
+ if (activity < 100) {
+// System.out.println("Scheduling update immediately.");
+ } else if (activity < 1000) {
+// System.out.println("Scheduling update after 500ms.");
+ delay = 500;
+ } else {
+// System.out.println("Scheduling update after 3000ms.");
+ delay = 3000;
+ }
+
+ updateCounter = 0;
+
+ //System.out.println("Scheduling UI update after " + delay + " ms.");
+ uiUpdateScheduler.schedule(new Runnable() {
+ @Override
+ public void run() {
+
+ if (tree.isDisposed())
+ return;
+
+ if (updateCounter > 0) {
+ updateCounter = 0;
+ uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
+ } else {
+ tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
+ }
+
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+
+ updating = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ int updateCounter = 0;
+
+ void update(TreeItem item) {
+
+ synchronized(pendingItems) {
+
+// System.out.println("update " + item);
+
+ updateCounter++;
+
+ if(item == null) pendingRoot = true;
+ else pendingItems.add(item);
+
+ if(updating == true) return;
+
+ scheduleUpdater();
+
+ }
+
+ }
+
+ private int maxChildren = DEFAULT_MAX_CHILDREN;
+
+ @Override
+ public int getMaxChildren() {
+ return maxChildren;
+ }
+
+ @Override
+ public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
+ Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
+ //System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
+ if (result != null) {
+ if (result < 0)
+ throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
+ return result;
+ }
+ return maxChildren;
+ }
+
+ @Override
+ public void setMaxChildren(int maxChildren) {
+ this.maxChildren = maxChildren;
+ }
+
+ @Override
+ public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
+ this.modificationContext = modificationContext;
+ }
+
+ /**
+ * @param parent the parent SWT composite
+ */
+ public GraphExplorerImpl(Composite parent) {
+ this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+ }
+
+ /**
+ * Stores the node context and the modifier that is currently being
+ * modified. These are used internally to prevent duplicate edits from being
+ * initiated which should always be a sensible thing to do.
+ */
+ private Set currentlyModifiedNodes = new THashSet();
+
+ private final TreeEditor editor;
+ private Color invalidModificationColor = null;
+
+ /**
+ * @param item the TreeItem to start editing
+ * @param columnIndex the index of the column to edit, starts counting from
+ * 0
+ * @return true
if the editing was initiated successfully or
+ * false
if editing could not be started due to lack of
+ * {@link Modifier} for the labeler in question.
+ */
+ private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
+ if (!editable)
+ return "Rename not supported for selection";
+
+ GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
+ final NodeContext context = (NodeContext) item.getData();
+ Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
+ if (labeler == null)
+ return "Rename not supported for selection";
+
+ if(columnKey == null) columnKey = columns[columnIndex].getKey();
+
+ // columnKey might be prefixed with '#' to indicate
+ // textual editing is preferred. Try to get modifier
+ // for that first and only if it fails, try without
+ // the '#' prefix.
+ Modifier modifier = labeler.getModifier(modificationContext, columnKey);
+ if (modifier == null) {
+ if(columnKey.startsWith("#"))
+ modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
+ if (modifier == null)
+ return "Rename not supported for selection";
+ }
+ if (modifier instanceof DeniedModifier) {
+ DeniedModifier dm = (DeniedModifier)modifier;
+ return dm.getMessage();
+ }
+
+ // Prevent editing of a single node context multiple times.
+ if (currentlyModifiedNodes.contains(context)) {
+ //System.out.println("discarding duplicate edit for context " + context);
+ return "Rename not supported for selection";
+ }
+
+ // Clean up any previous editor control
+ Control oldEditor = editor.getEditor();
+ if (oldEditor != null)
+ oldEditor.dispose();
+
+ if (modifier instanceof DialogModifier) {
+ performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
+ } else if (modifier instanceof CustomModifier) {
+ startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
+ } else if (modifier instanceof EnumerationModifier) {
+ startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
+ } else {
+ startTextEditing(item, columnIndex, context, modifier);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param item
+ * @param columnIndex
+ * @param context
+ * @param modifier
+ */
+ void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
+ final DialogModifier modifier) {
+ final AtomicBoolean disposed = new AtomicBoolean(false);
+ Consumer callback = result -> {
+ if (disposed.get())
+ return;
+ String error = modifier.isValid(result);
+ if (error == null) {
+ modifier.modify(result);
+ // Item may be disposed if the tree gets reset after a previous editing.
+ if (!item.isDisposed()) {
+ item.setText(columnIndex, result);
+ queueSelectionRefresh(context);
+ }
+ }
+ };
+
+ currentlyModifiedNodes.add(context);
+ try {
+ String status = modifier.query(tree, item, columnIndex, context, callback);
+ if (status != null)
+ ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
+ } finally {
+ currentlyModifiedNodes.remove(context);
+ disposed.set(true);
+ }
+ }
+
+ private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
+ Point size = control.computeSize(widthHint, heightHint);
+ editor.horizontalAlignment = SWT.LEFT;
+ Rectangle itemRect = item.getBounds(columnIndex),
+ rect = tree.getClientArea();
+ editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
+ int left = itemRect.x,
+ right = rect.x + rect.width;
+ editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
+ editor.minimumHeight = size.y + insetY * 2;
+ editor.layout();
+ }
+
+ void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
+ GC gc = new GC(control);
+ Point size = gc.textExtent(text);
+ gc.dispose();
+ reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
+ }
+
+ /**
+ * @param item
+ * @param columnIndex
+ * @param context
+ * @param modifier
+ */
+ void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
+ final CustomModifier modifier) {
+ final Object obj = modifier.createControl(tree, item, columnIndex, context);
+ if (!(obj instanceof Control))
+ throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
+ final Control control = (Control) obj;
+
+// final int insetX = 0;
+// final int insetY = 0;
+// control.addListener(SWT.Resize, new Listener() {
+// @Override
+// public void handleEvent(Event e) {
+// Rectangle rect = control.getBounds();
+// control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
+// }
+// });
+ control.addListener(SWT.Dispose, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ currentlyModifiedNodes.remove(context);
+ queueSelectionRefresh(context);
+ deactivateEditingContext();
+ }
+ });
+
+ if (!(control instanceof Shell)) {
+ editor.setEditor(control, item, columnIndex);
+ }
+
+
+ control.setFocus();
+
+ GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
+
+ activateEditingContext(control);
+
+ // Removed in disposeListener above
+ currentlyModifiedNodes.add(context);
+ //System.out.println("START CUSTOM EDITING: " + item);
+ }
+
+ /**
+ * @param item
+ * @param columnIndex
+ * @param context
+ * @param modifier
+ */
+ void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
+ String initialText = modifier.getValue();
+ if (initialText == null)
+ throw new AssertionError("Labeler.Modifier.getValue() returned null");
+
+ List values = modifier.getValues();
+ String selectedValue = modifier.getValue();
+ int selectedIndex = values.indexOf(selectedValue);
+ if (selectedIndex == -1)
+ throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
+
+ final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
+ combo.setVisibleItemCount(10);
+ //combo.setEditable(false);
+
+ for (String value : values) {
+ combo.add(value);
+ }
+ combo.select(selectedIndex);
+
+ Listener comboListener = new Listener() {
+ boolean arrowTraverseUsed = false;
+ @Override
+ public void handleEvent(final Event e) {
+ //System.out.println("FOO: " + e);
+ switch (e.type) {
+ case SWT.KeyDown:
+ if (e.character == SWT.CR) {
+ // Commit edit directly on ENTER press.
+ String text = combo.getText();
+ modifier.modify(text);
+ // Item may be disposed if the tree gets reset after a previous editing.
+ if (!item.isDisposed()) {
+ item.setText(columnIndex, text);
+ queueSelectionRefresh(context);
+ }
+ combo.dispose();
+ e.doit = false;
+ } else if (e.keyCode == SWT.ESC) {
+ // Cancel editing immediately
+ combo.dispose();
+ e.doit = false;
+ }
+ break;
+ case SWT.Selection:
+ {
+ if (arrowTraverseUsed) {
+ arrowTraverseUsed = false;
+ return;
+ }
+
+ String text = combo.getText();
+ modifier.modify(text);
+
+ // Item may be disposed if the tree gets reset after a previous editing.
+ if (!item.isDisposed()) {
+ item.setText(columnIndex, text);
+ queueSelectionRefresh(context);
+ }
+ combo.dispose();
+ break;
+ }
+ case SWT.FocusOut: {
+ String text = combo.getText();
+ modifier.modify(text);
+
+ // Item may be disposed if the tree gets reset after a previous editing.
+ if (!item.isDisposed()) {
+ item.setText(columnIndex, text);
+ queueSelectionRefresh(context);
+ }
+ combo.dispose();
+ break;
+ }
+ case SWT.Traverse: {
+ switch (e.detail) {
+ case SWT.TRAVERSE_RETURN:
+ String text = combo.getText();
+ modifier.modify(text);
+ if (!item.isDisposed()) {
+ item.setText(columnIndex, text);
+ queueSelectionRefresh(context);
+ }
+ arrowTraverseUsed = false;
+ // FALL THROUGH
+ case SWT.TRAVERSE_ESCAPE:
+ combo.dispose();
+ e.doit = false;
+ break;
+ case SWT.TRAVERSE_ARROW_NEXT:
+ case SWT.TRAVERSE_ARROW_PREVIOUS:
+ arrowTraverseUsed = true;
+ break;
+ default:
+ //System.out.println("unhandled traversal: " + e.detail);
+ break;
+ }
+ break;
+ }
+ case SWT.Dispose:
+ currentlyModifiedNodes.remove(context);
+ deactivateEditingContext();
+ break;
+ }
+ }
+ };
+ combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
+ combo.addListener(SWT.KeyDown, comboListener);
+ combo.addListener(SWT.FocusOut, comboListener);
+ combo.addListener(SWT.Traverse, comboListener);
+ combo.addListener(SWT.Selection, comboListener);
+ combo.addListener(SWT.Dispose, comboListener);
+
+ editor.setEditor(combo, item, columnIndex);
+
+ combo.setFocus();
+
+ GraphExplorerImpl.this.reconfigureTreeEditorForText(item, columnIndex, combo, combo.getText(), SWT.DEFAULT, 0, 0);
+
+ activateEditingContext(combo);
+
+ // Removed in comboListener
+ currentlyModifiedNodes.add(context);
+
+ combo.setListVisible(true);
+ //System.out.println("START ENUMERATION EDITING: " + item);
+ }
+
+ /**
+ * @param item
+ * @param columnIndex
+ * @param context
+ * @param modifier
+ */
+ void startTextEditing(final TreeItem item, final int columnIndex, final NodeContext context, final Modifier modifier) {
+ String initialText = modifier.getValue();
+ if (initialText == null)
+ throw new AssertionError("Labeler.Modifier.getValue() returned null, modifier=" + modifier);
+
+ final Composite composite = new Composite(tree, SWT.NONE);
+ //composite.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_RED));
+ final Text text = new Text(composite, SWT.BORDER);
+ final int insetX = 0;
+ final int insetY = 0;
+ composite.addListener(SWT.Resize, new Listener() {
+ @Override
+ public void handleEvent(Event e) {
+ Rectangle rect = composite.getClientArea();
+ text.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height
+ - insetY * 2);
+ }
+ });
+ final FilteringModifier filter = modifier instanceof FilteringModifier ? (FilteringModifier) modifier : null;
+ Listener textListener = new Listener() {
+
+ boolean modified = false;
+
+ @Override
+ public void handleEvent(final Event e) {
+ String error;
+ String newText;
+ switch (e.type) {
+ case SWT.FocusOut:
+ if(modified) {
+ //System.out.println("FOCUS OUT " + item);
+ newText = text.getText();
+ error = modifier.isValid(newText);
+ if (error == null) {
+ modifier.modify(newText);
+
+ // Item may be disposed if the tree gets reset after a previous editing.
+ if (!item.isDisposed()) {
+ item.setText(columnIndex, newText);
+ queueSelectionRefresh(context);
+ }
+ } else {
+ // System.out.println("validation error: " + error);
+ }
+ }
+ composite.dispose();
+ break;
+ case SWT.Modify:
+ newText = text.getText();
+ error = modifier.isValid(newText);
+ if (error != null) {
+ text.setBackground(invalidModificationColor);
+ errorStatus(error);
+ //System.out.println("validation error: " + error);
+ } else {
+ text.setBackground(null);
+ errorStatus(null);
+ }
+ modified = true;
+ break;
+ case SWT.Verify:
+
+ // Safety check since it seems that this may happen with
+ // virtual trees.
+ if (item.isDisposed())
+ return;
+
+ // Filter input if necessary
+ e.text = filter != null ? filter.filter(e.text) : e.text;
+
+ newText = text.getText();
+ String leftText = newText.substring(0, e.start);
+ String rightText = newText.substring(e.end, newText.length());
+ GraphExplorerImpl.this.reconfigureTreeEditorForText(
+ item, columnIndex, text, leftText + e.text + rightText,
+ SWT.DEFAULT, insetX, insetY);
+ break;
+ case SWT.Traverse:
+ switch (e.detail) {
+ case SWT.TRAVERSE_RETURN:
+ if(modified) {
+ newText = text.getText();
+ error = modifier.isValid(newText);
+ if (error == null) {
+ modifier.modify(newText);
+ if (!item.isDisposed()) {
+ item.setText(columnIndex, newText);
+ queueSelectionRefresh(context);
+ }
+ }
+ }
+ // FALL THROUGH
+ case SWT.TRAVERSE_ESCAPE:
+ composite.dispose();
+ e.doit = false;
+ break;
+ default:
+ //System.out.println("unhandled traversal: " + e.detail);
+ break;
+ }
+ break;
+
+ case SWT.Dispose:
+ currentlyModifiedNodes.remove(context);
+ deactivateEditingContext();
+ errorStatus(null);
+ break;
+ }
+ }
+ };
+ // Set the initial text before registering a listener. We do not want immediate modification!
+ text.setText(initialText);
+ text.addListener(SWT.FocusOut, textListener);
+ text.addListener(SWT.Traverse, textListener);
+ text.addListener(SWT.Verify, textListener);
+ text.addListener(SWT.Modify, textListener);
+ text.addListener(SWT.Dispose, textListener);
+ editor.setEditor(composite, item, columnIndex);
+ text.selectAll();
+ text.setFocus();
+
+ // Initialize TreeEditor properly.
+ GraphExplorerImpl.this.reconfigureTreeEditorForText(
+ item, columnIndex, text, initialText,
+ SWT.DEFAULT, insetX, insetY);
+
+ // Removed in textListener
+ currentlyModifiedNodes.add(context);
+
+ activateEditingContext(text);
+
+ //System.out.println("START TEXT EDITING: " + item);
+ }
+
+ protected void errorStatus(String error) {
+ IStatusLineManager status = getStatusLineManager();
+ if (status != null) {
+ status.setErrorMessage(error);
+ }
+ }
+
+ protected IStatusLineManager getStatusLineManager() {
+ if (serviceLocator instanceof IWorkbenchPart) {
+ return WorkbenchUtils.getStatusLine((IWorkbenchPart) serviceLocator);
+ } else if (serviceLocator instanceof IWorkbenchSite) {
+ return WorkbenchUtils.getStatusLine((IWorkbenchSite) serviceLocator);
+ }
+ return null;
+ }
+
+ protected void activateEditingContext(Control control) {
+ if (contextService != null) {
+ editingContext = contextService.activateContext(INLINE_EDITING_UI_CONTEXT);
+ }
+ if (control != null && focusService != null) {
+ focusService.addFocusTracker(control, INLINE_EDITING_UI_CONTEXT);
+ // No need to remove the control, it will be
+ // removed automatically when it is disposed.
+ }
+ }
+
+ protected void deactivateEditingContext() {
+ IContextActivation a = editingContext;
+ if (a != null) {
+ editingContext = null;
+ contextService.deactivateContext(a);
+ }
+ }
+
+ /**
+ * @param forContext
+ */
+ void queueSelectionRefresh(NodeContext forContext) {
+ selectionRefreshContexts.add(forContext);
+ }
+
+ @Override
+ public String startEditing(NodeContext context, String columnKey_) {
+ assertNotDisposed();
+ if (!thread.currentThreadAccess())
+ throw new IllegalStateException("not in SWT display thread " + thread.getThread());
+
+ String columnKey = columnKey_;
+ if(columnKey.startsWith("#")) {
+ columnKey = columnKey.substring(1);
+ }
+
+ Integer columnIndex = columnKeyToIndex.get(columnKey);
+ if (columnIndex == null)
+ return "Rename not supported for selection";
+
+ TreeItem item = contextToItem.getRight(context);
+ if (item == null)
+ return "Rename not supported for selection";
+
+ return startEditing(item, columnIndex, columnKey_);
+
+ }
+
+ @Override
+ public String startEditing(String columnKey) {
+
+ ISelection selection = postSelectionProvider.getSelection();
+ if(selection == null) return "Rename not supported for selection";
+ NodeContext context = ISelectionUtils.filterSingleSelection(selection, NodeContext.class);
+ if(context == null) return "Rename not supported for selection";
+
+ return startEditing(context, columnKey);
+
+ }
+
+ /**
+ * @param site null
if the explorer is detached from the workbench
+ * @param parent the parent SWT composite
+ * @param style the tree style to use, check the see tags for the available flags
+ *
+ * @see SWT#SINGLE
+ * @see SWT#MULTI
+ * @see SWT#CHECK
+ * @see SWT#FULL_SELECTION
+ * @see SWT#NO_SCROLL
+ * @see SWT#H_SCROLL
+ * @see SWT#V_SCROLL
+ */
+ public GraphExplorerImpl(Composite parent, int style) {
+
+ setServiceLocator(null);
+
+ this.localResourceManager = new LocalResourceManager(JFaceResources.getResources());
+ this.resourceManager = new DeviceResourceManager(parent.getDisplay());
+
+ this.imageLoaderJob = new ImageLoaderJob(this);
+ this.imageLoaderJob.setPriority(Job.DECORATE);
+
+ invalidModificationColor = (Color) localResourceManager.get( ColorDescriptor.createFrom( new RGB(255, 128, 128) ) );
+
+ this.thread = SWTThread.getThreadAccess(parent);
+
+ for(int i=0;i<10;i++) explorerContext.activity.push(0);
+
+ tree = new Tree(parent, style);
+ tree.addListener(SWT.SetData, this);
+ tree.addListener(SWT.Expand, this);
+ tree.addListener(SWT.Dispose, this);
+ tree.addListener(SWT.Activate, this);
+
+ tree.setData(KEY_GRAPH_EXPLORER, this);
+
+ // These are both required for performing column resizing without flicker.
+ // See SWT.Resize event handling in #handleEvent() for more explanations.
+ parent.addListener(SWT.Resize, this);
+ tree.addListener(SWT.Resize, this);
+
+ originalFont = JFaceResources.getDefaultFontDescriptor();
+// originalBackground = JFaceResources.getColorRegistry().get(symbolicName);
+// originalForeground = tree.getForeground();
+
+ tree.setFont((Font) localResourceManager.get(originalFont));
+
+ columns = new Column[] { new Column(ColumnKeys.SINGLE) };
+ columnKeyToIndex = Collections.singletonMap(ColumnKeys.SINGLE, 0);
+
+ editor = new TreeEditor(tree);
+ editor.horizontalAlignment = SWT.LEFT;
+ editor.grabHorizontal = true;
+ editor.minimumWidth = 50;
+
+ setBasicListeners();
+ setDefaultProcessors();
+
+ this.toolTip = new GraphExplorerToolTip(explorerContext, tree);
+ }
+
+ @Override
+ public IThreadWorkQueue getThread() {
+ return thread;
+ }
+
+ TreeItem previousSingleSelection = null;
+ long focusGainedAt = Long.MIN_VALUE;
+
+ protected GraphExplorerToolTip toolTip;
+
+ protected void setBasicListeners() {
+ // Keep track of the previous single selection to help
+ // decide whether to start editing a tree node on mouse
+ // downs or not.
+ tree.addListener(SWT.Selection, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ TreeItem[] selection = tree.getSelection();
+ if (selection.length == 1) {
+ //for (TreeItem item : selection)
+ // System.out.println("selection: " + item);
+ previousSingleSelection = selection[0];
+ } else {
+ previousSingleSelection = null;
+ }
+ }
+ });
+
+ // Try to start editing of tree column when clicked for the second time.
+ Listener mouseEditListener = new Listener() {
+
+ Future> startEdit = null;
+
+ @Override
+ public void handleEvent(Event event) {
+ if (event.type == SWT.DragDetect) {
+ // Needed to prevent editing from being started when in fact
+ // the user starts dragging an item.
+ //System.out.println("DRAG DETECT: " + event);
+ cancelEdit();
+ return;
+ }
+ //System.out.println("mouse down: " + event);
+ if (event.button == 1) {
+ // Always ignore the first mouse button press that focuses
+ // the control. Do not let it start in-line editing since
+ // that is very annoying to users and not how the UI's that
+ // people are used to behave.
+ long eventTime = ((long) event.time) & 0xFFFFFFFFL;
+ if ((eventTime - focusGainedAt) < 250L) {
+ //System.out.println("ignore mouse down " + focusGainedAt + ", " + eventTime + " = " + (eventTime-focusGainedAt));
+ return;
+ }
+ //System.out.println("testing whether to start editing");
+
+ final Point point = new Point(event.x, event.y);
+ final TreeItem item = tree.getItem(point);
+ if (item == null)
+ return;
+ //System.out.println("mouse down @ " + point + ": " + item + ", previous item: " + previousSingleSelection);
+
+ // Only start editing if the item was already selected.
+ if (!item.equals(previousSingleSelection)) {
+ cancelEdit();
+ return;
+ }
+
+ if (tree.getColumnCount() > 1) {
+ // TODO: reconsider this logic, might not be good in general.
+ for (int i = 0; i < tree.getColumnCount(); i++) {
+ if (item.getBounds(i).contains(point)) {
+ tryScheduleEdit(event, item, point, 100, i);
+ return;
+ }
+ }
+ } else {
+ //System.out.println("clicks: " + event.count);
+ if (item.getBounds().contains(point)) {
+ if (event.count == 1) {
+ tryScheduleEdit(event, item, point, 500, 0);
+ } else {
+ cancelEdit();
+ }
+ }
+ }
+ }
+ }
+
+ void tryScheduleEdit(Event event, final TreeItem item, Point point, long delayMs, final int column) {
+ //System.out.println("\tCONTAINS: " + item);
+ if (!cancelEdit())
+ return;
+
+ //System.out.println("\tScheduling edit: " + item);
+ startEdit = ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
+ @Override
+ public void run() {
+ ThreadUtils.asyncExec(thread, new Runnable() {
+ @Override
+ public void run() {
+ if (item.isDisposed())
+ return;
+ startEditing(item, column, null);
+ }
+ });
+ }
+ }, delayMs, TimeUnit.MILLISECONDS);
+ }
+
+ boolean cancelEdit() {
+ Future> f = startEdit;
+ if (f != null) {
+ // Try to cancel the start edit task if it's not running yet.
+ startEdit = null;
+ if (!f.isDone()) {
+ boolean ret = f.cancel(false);
+ //System.out.println("\tCancelled edit: " + ret);
+ return ret;
+ }
+ }
+ //System.out.println("\tNo edit in progress to cancel");
+ return true;
+ }
+ };
+ tree.addListener(SWT.MouseDown, mouseEditListener);
+ tree.addListener(SWT.DragDetect, mouseEditListener);
+ tree.addListener(SWT.DragDetect, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ Point test = new Point(event.x, event.y);
+ TreeItem item = tree.getItem(test);
+ if(item != null) {
+ for(int i=0;i set = ISelectionUtils.filterSetSelection(event.getSelection(), NodeContext.class);
+ selectedItems.clear();
+ for (NodeContext nc : set) {
+ TreeItem item = contextToItem.getRight(nc);
+ if (item != null)
+ selectedItems.put(item, nc);
+ }
+ //System.out.println("newly selected items: " + selectedItems);
+ }
+ });
+ }
+
+ /**
+ * Mod count for delaying post selection changed events.
+ */
+ int postSelectionModCount = 0;
+
+ /**
+ * Last tree selection modification time for implementing a quiet
+ * time for selection changes.
+ */
+ long lastSelectionModTime = System.currentTimeMillis() - 10000;
+
+ /**
+ * Current target time for the selection to be set. Calculated
+ * according to the set quiet time and last selection modification
+ * time.
+ */
+ long selectionSetTargetTime = 0;
+
+ /**
+ * true
if delayed selection runnable is current scheduled or
+ * running.
+ */
+ boolean delayedSelectionScheduled = false;
+
+ Runnable SELECTION_DELAY = new Runnable() {
+ @Override
+ public void run() {
+ if (tree.isDisposed())
+ return;
+ long now = System.currentTimeMillis();
+ long waitTimeLeft = selectionSetTargetTime - now;
+ if (waitTimeLeft > 0) {
+ // Not enough quiet time, reschedule.
+ delayedSelectionScheduled = true;
+ tree.getDisplay().timerExec((int) waitTimeLeft, this);
+ } else {
+ // Time to perform selection, stop rescheduling.
+ delayedSelectionScheduled = false;
+ resetSelection();
+ }
+ }
+ };
+
+ private void widgetSelectionChanged(boolean forceSelectionChange) {
+ long modTime = System.currentTimeMillis();
+ long delta = modTime - lastSelectionModTime;
+ lastSelectionModTime = modTime;
+ if (!forceSelectionChange && delta < SELECTION_CHANGE_QUIET_TIME) {
+ long msToWait = SELECTION_CHANGE_QUIET_TIME - delta;
+ selectionSetTargetTime = modTime + msToWait;
+ if (!delayedSelectionScheduled) {
+ delayedSelectionScheduled = true;
+ tree.getDisplay().timerExec((int) msToWait, SELECTION_DELAY);
+ }
+ // Make sure that post selection change events do not fire.
+ ++postSelectionModCount;
+ return;
+ }
+
+ // Immediate selection reconstruction.
+ resetSelection();
+ }
+
+ private void resetSelection() {
+ final ISelection selection = getWidgetSelection();
+
+ //System.out.println("resetSelection(" + postSelectionModCount + ")");
+ //System.out.println(" provider selection: " + selectionProvider.getSelection());
+ //System.out.println(" widget selection: " + selection);
+
+ selectionProvider.setAndFireNonEqualSelection(selection);
+
+ // Implement deferred firing of post selection events
+ final int count = ++postSelectionModCount;
+ //System.out.println("[" + System.currentTimeMillis() + "] scheduling postSelectionChanged " + count + ": " + selection);
+ ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
+ @Override
+ public void run() {
+ int newCount = postSelectionModCount;
+ // Don't publish selection yet, there's another change incoming.
+ //System.out.println("[" + System.currentTimeMillis() + "] checking post selection publish: " + count + " vs. " + newCount + ": " + selection);
+ if (newCount != count)
+ return;
+ //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);
+
+ if (tree.isDisposed())
+ return;
+
+ //System.out.println("scheduling fire post selection changed: " + selection);
+ tree.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (tree.isDisposed() || selectionProvider == null)
+ return;
+ //System.out.println("firing post selection changed: " + selection);
+ selectionProvider.firePostSelection(selection);
+ }
+ });
+ }
+ }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);
+ }
+
+ protected void setDefaultProcessors() {
+ // Add a simple IMAGER query processor that always returns null.
+ // With this processor no images will ever be shown.
+// setPrimitiveProcessor(new StaticImagerProcessor(null));
+
+ setProcessor(new DefaultComparableChildrenProcessor());
+ setProcessor(new DefaultLabelDecoratorsProcessor());
+ setProcessor(new DefaultImageDecoratorsProcessor());
+ setProcessor(new DefaultSelectedLabelerProcessor());
+ setProcessor(new DefaultLabelerFactoriesProcessor());
+ setProcessor(new DefaultSelectedImagerProcessor());
+ setProcessor(new DefaultImagerFactoriesProcessor());
+ setPrimitiveProcessor(new DefaultLabelerProcessor());
+ setPrimitiveProcessor(new DefaultCheckedStateProcessor());
+ setPrimitiveProcessor(new DefaultImagerProcessor());
+ setPrimitiveProcessor(new DefaultLabelDecoratorProcessor());
+ setPrimitiveProcessor(new DefaultImageDecoratorProcessor());
+ setPrimitiveProcessor(new NoSelectionRequestProcessor());
+
+ setProcessor(new DefaultFinalChildrenProcessor(this));
+
+ setProcessor(new DefaultPrunedChildrenProcessor());
+ setProcessor(new DefaultSelectedViewpointProcessor());
+ setProcessor(new DefaultSelectedLabelDecoratorFactoriesProcessor());
+ setProcessor(new DefaultSelectedImageDecoratorFactoriesProcessor());
+ setProcessor(new DefaultViewpointContributionsProcessor());
+
+ setPrimitiveProcessor(new DefaultViewpointProcessor());
+ setPrimitiveProcessor(new DefaultViewpointContributionProcessor());
+ setPrimitiveProcessor(new DefaultSelectedViewpointFactoryProcessor());
+ setPrimitiveProcessor(new DefaultIsExpandedProcessor());
+ setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor());
+ }
+
+ @Override
+ public void setProcessor(NodeQueryProcessor processor) {
+ assertNotDisposed();
+ if (processor == null)
+ throw new IllegalArgumentException("null processor");
+
+ processors.put(processor.getIdentifier(), processor);
+ }
+
+ @Override
+ public void setPrimitiveProcessor(PrimitiveQueryProcessor processor) {
+ assertNotDisposed();
+ if (processor == null)
+ throw new IllegalArgumentException("null processor");
+
+ PrimitiveQueryProcessor> oldProcessor = primitiveProcessors.put(processor.getIdentifier(), processor);
+
+ if (oldProcessor instanceof ProcessorLifecycle)
+ ((ProcessorLifecycle) oldProcessor).detached(this);
+ if (processor instanceof ProcessorLifecycle)
+ ((ProcessorLifecycle) processor).attached(this);
+ }
+
+ @Override
+ public void setDataSource(DataSource provider) {
+ assertNotDisposed();
+ if (provider == null)
+ throw new IllegalArgumentException("null provider");
+ dataSources.put(provider.getProvidedClass(), provider);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public DataSource removeDataSource(Class forProvidedClass) {
+ assertNotDisposed();
+ if (forProvidedClass == null)
+ throw new IllegalArgumentException("null class");
+ return dataSources.remove(forProvidedClass);
+ }
+
+ @Override
+ public void setPersistor(StatePersistor persistor) {
+ this.persistor = persistor;
+ }
+
+ @Override
+ public SelectionDataResolver getSelectionDataResolver() {
+ return selectionDataResolver;
+ }
+
+ @Override
+ public void setSelectionDataResolver(SelectionDataResolver r) {
+ this.selectionDataResolver = r;
+ }
+
+ @Override
+ public SelectionFilter getSelectionFilter() {
+ return selectionFilter;
+ }
+
+ @Override
+ public void setSelectionFilter(SelectionFilter f) {
+ this.selectionFilter = f;
+ // TODO: re-filter current selection?
+ }
+
+ @Override
+ public void setSelectionTransformation(BiFunction f) {
+ this.selectionTransformation = f;
+ }
+
+ @Override
+ public void addListener(T listener) {
+ if(listener instanceof FocusListener) {
+ focusListeners.add((FocusListener)listener);
+ } else if(listener instanceof MouseListener) {
+ mouseListeners.add((MouseListener)listener);
+ } else if(listener instanceof KeyListener) {
+ keyListeners.add((KeyListener)listener);
+ }
+ }
+
+ @Override
+ public void removeListener(T listener) {
+ if(listener instanceof FocusListener) {
+ focusListeners.remove(listener);
+ } else if(listener instanceof MouseListener) {
+ mouseListeners.remove(listener);
+ } else if(listener instanceof KeyListener) {
+ keyListeners.remove(listener);
+ }
+ }
+
+ public void addSelectionListener(SelectionListener listener) {
+ tree.addSelectionListener(listener);
+ }
+
+ public void removeSelectionListener(SelectionListener listener) {
+ tree.removeSelectionListener(listener);
+ }
+
+ private Set uiContexts;
+
+ @Override
+ public void setUIContexts(Set contexts) {
+ this.uiContexts = contexts;
+ }
+
+ @Override
+ public void setRoot(final Object root) {
+ if(uiContexts != null && uiContexts.size() == 1)
+ setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
+ else
+ setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
+ }
+
+ @Override
+ public void setRootContext(final NodeContext context) {
+ setRootContext0(context);
+ }
+
+ private void setRootContext0(final NodeContext context) {
+ Assert.isNotNull(context, "root must not be null");
+ if (isDisposed() || tree.isDisposed())
+ return;
+ Display display = tree.getDisplay();
+ if (display.getThread() == Thread.currentThread()) {
+ doSetRoot(context);
+ } else {
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ doSetRoot(context);
+ }
+ });
+ }
+ }
+
+ private void initializeState() {
+ if (persistor == null)
+ return;
+ ExplorerStates.scheduleRead(getRoot(), persistor)
+ .thenAccept(state -> SWTUtils.asyncExec(tree, () -> restoreState(state)));
+ }
+
+ private void restoreState(ExplorerState state) {
+ // topNodeToSet will be processed by #setData when it encounters a
+ // NodeContext that matches this one.
+// topNodePath = state.topNodePath;
+// topNodePathChildIndex = state.topNodePathChildIndex;
+// currentTopNodePathIndex = 0;
+
+ Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
+ if (processor instanceof DefaultIsExpandedProcessor) {
+ DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
+ for(NodeContext expanded : state.expandedNodes) {
+ isExpandedProcessor.replaceExpanded(expanded, true);
+ }
+ }
+ }
+
+ private void saveState() {
+ if (persistor == null)
+ return;
+
+ NodeContext[] topNodePath = NodeContext.NONE;
+ int[] topNodePathChildIndex = {};
+ Collection expandedNodes = Collections.emptyList();
+ Map columnWidths = Collections. emptyMap();
+
+ // Resolve top node path
+ TreeItem topItem = tree.getTopItem();
+ if (topItem != null) {
+ NodeContext topNode = (NodeContext) topItem.getData();
+ if (topNode != null) {
+ topNodePath = getNodeContextPathSegments(topNode);
+ topNodePathChildIndex = new int[topNodePath.length];
+ for (int i = 0; i < topNodePath.length; ++i) {
+ // TODO: get child indexes
+ topNodePathChildIndex[i] = 0;
+ }
+ }
+ }
+
+ // Resolve expanded nodes
+ Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
+ if (processor instanceof IsExpandedProcessor) {
+ IsExpandedProcessor isExpandedProcessor = (IsExpandedProcessor) processor;
+ expandedNodes = isExpandedProcessor.getExpanded();
+ }
+
+ // Column widths
+ TreeColumn[] columns = tree.getColumns();
+ if (columns.length > 1) {
+ columnWidths = new HashMap();
+ for (int i = 0; i < columns.length; ++i) {
+ columnWidths.put(columns[i].getText(), columns[i].getWidth());
+ }
+ }
+
+ persistor.serialize(
+ ExplorerStates.explorerStateLocation(),
+ getRoot(),
+ new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
+ }
+
+ /**
+ * Invoke only from SWT thread to reset the root of the graph explorer tree.
+ *
+ * @param root
+ */
+ private void doSetRoot(NodeContext root) {
+ if (tree.isDisposed())
+ return;
+ if (root.getConstant(BuiltinKeys.INPUT) == null) {
+ ErrorLogger.defaultLogError("root node context does not contain BuiltinKeys.INPUT key. Node = " + root, new Exception("trace"));
+ return;
+ }
+
+ // Empty caches, release queries.
+ GraphExplorerContext oldContext = explorerContext;
+ GraphExplorerContext newContext = new GraphExplorerContext();
+ GENodeQueryManager manager = new GENodeQueryManager(newContext, null, null, TreeItemReference.create(null));
+ this.explorerContext = newContext;
+ oldContext.safeDispose();
+ toolTip.setGraphExplorerContext(explorerContext);
+
+ // Need to empty these or otherwise they won't be emptied until the
+ // explorer is disposed which would mean that many unwanted references
+ // will be held by this map.
+ clearPrimitiveProcessors();
+
+ this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root
+ : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE);
+
+ explorerContext.getCache().incRef(this.rootContext);
+
+ initializeState();
+
+ NodeContext[] contexts = manager.query(rootContext, BuiltinKeys.FINAL_CHILDREN);
+
+ tree.setItemCount(contexts.length);
+
+ select(rootContext);
+ refreshColumnSizes();
+ }
+
+ @Override
+ public NodeContext getRoot() {
+ return rootContext;
+ }
+
+ @Override
+ public NodeContext getParentContext(NodeContext context) {
+ if (disposed)
+ throw new IllegalStateException("disposed");
+ if (!thread.currentThreadAccess())
+ throw new IllegalStateException("not in SWT display thread " + thread.getThread());
+
+ TreeItem item = contextToItem.getRight(context);
+ if(item == null) return null;
+ TreeItem parentItem = item.getParentItem();
+ if(parentItem == null) return null;
+ return (NodeContext)parentItem.getData();
+ }
+
+ Point previousTreeSize;
+ Point previousTreeParentSize;
+ boolean activatedBefore = false;
+
+ @Override
+ public void handleEvent(Event event) {
+ //System.out.println("EVENT: " + event);
+ switch(event.type) {
+ case SWT.Expand:
+ //System.out.println("EXPAND: " + event.item);
+ if ((tree.getStyle() & SWT.VIRTUAL) != 0) {
+ expandVirtual(event);
+ } else {
+ System.out.println("TODO: non-virtual tree item expand");
+ }
+ break;
+ case SWT.SetData:
+ // Only invoked for SWT.VIRTUAL trees
+ if (disposed)
+ // Happened for Hannu once during program startup.
+ // java.lang.AssertionError
+ // at org.simantics.browsing.ui.common.internal.GENodeQueryManager.query(GENodeQueryManager.java:190)
+ // at org.simantics.browsing.ui.swt.GraphExplorerImpl.setData(GraphExplorerImpl.java:2315)
+ // at org.simantics.browsing.ui.swt.GraphExplorerImpl.handleEvent(GraphExplorerImpl.java:2039)
+ // I do not know whether SWT guarantees that SetData events
+ // don't come after Dispose event has been issued, but I
+ // think its better to have this check here just incase.
+ return;
+ setData(event);
+ break;
+ case SWT.Activate:
+ // This ensures that column sizes are refreshed at
+ // least once when the GE is first shown.
+ if (!activatedBefore) {
+ refreshColumnSizes();
+ activatedBefore = true;
+ }
+ break;
+ case SWT.Dispose:
+ //new Exception().printStackTrace();
+ if (disposed)
+ return;
+ disposed = true;
+ doDispose();
+ break;
+ case SWT.Resize:
+ if (event.widget == tree) {
+ // This case is meant for listening to tree width increase.
+ // The column resizing must be performed only after the tree
+ // itself as been resized.
+ Point size = tree.getSize();
+ int dx = 0;
+ if (previousTreeSize != null) {
+ dx = size.x - previousTreeSize.x;
+ }
+ previousTreeSize = size;
+ //System.out.println("RESIZE: " + dx + " - size=" + size);
+
+ if (dx > 0) {
+ tree.setRedraw(false);
+ refreshColumnSizes(size);
+ tree.setRedraw(true);
+ }
+ } else if (event.widget == tree.getParent()) {
+ // This case is meant for listening to tree width decrease.
+ // The columns must be resized before the tree widget itself
+ // is resized to prevent scroll bar flicker. This can be achieved
+ // by listening to the resize events of the tree parent widget.
+ Composite parent = tree.getParent();
+ Point size = parent.getSize();
+
+ // We must subtract the parent's border and possible
+ // scroll bar width from the new target width of the columns.
+ size.x -= tree.getParent().getBorderWidth() * 2;
+ ScrollBar vBar = parent.getVerticalBar();
+ if (vBar != null && vBar.isVisible())
+ size.x -= vBar.getSize().x;
+
+ int dx = 0;
+ if (previousTreeParentSize != null) {
+ dx = size.x - previousTreeParentSize.x;
+ }
+ previousTreeParentSize = size;
+ //System.out.println("RESIZE: " + dx + " - size=" + size);
+
+ if (dx < 0) {
+ tree.setRedraw(false);
+ refreshColumnSizes(size);
+ tree.setRedraw(true);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ protected void refreshColumnSizes() {
+// Composite treeParent = tree.getParent();
+// Point size = treeParent.getSize();
+// size.x -= treeParent.getBorderWidth() * 2;
+ Point size = tree.getSize();
+ refreshColumnSizes(size);
+ tree.getParent().layout();
+ }
+
+ /**
+ * This has been disabled since the logic of handling column widths has been
+ * externalized to parties creating {@link GraphExplorerImpl} instances.
+ */
+ protected void refreshColumnSizes(Point toSize) {
+ /*
+ refreshingColumnSizes = true;
+ try {
+ int columnCount = tree.getColumnCount();
+ if (columnCount > 0) {
+ Point size = toSize;
+ int targetWidth = size.x - tree.getBorderWidth() * 2;
+ targetWidth -= 0;
+
+ // Take the vertical scroll bar existence into to account when
+ // calculating the overflow column width.
+ ScrollBar vBar = tree.getVerticalBar();
+ //if (vBar != null && vBar.isVisible())
+ if (vBar != null)
+ targetWidth -= vBar.getSize().x;
+
+ List resizing = new ArrayList();
+ int usedWidth = 0;
+ int resizingWidth = 0;
+ int totalWeight = 0;
+ for (int i = 0; i < columnCount - 1; ++i) {
+ TreeColumn col = tree.getColumn(i);
+ //System.out.println(" " + col.getText() + ": " + col.getWidth());
+ int width = col.getWidth();
+ usedWidth += width;
+ Column c = (Column) col.getData();
+ if (c.hasGrab()) {
+ resizing.add(col);
+ resizingWidth += width;
+ totalWeight += c.getWeight();
+ }
+ }
+
+ int requiredWidthAdjustment = targetWidth - usedWidth;
+ if (requiredWidthAdjustment < 0)
+ requiredWidthAdjustment = Math.min(requiredWidthAdjustment, -resizing.size());
+ double diff = requiredWidthAdjustment;
+ //System.out.println("REQUIRED WIDTH ADJUSTMENT: " + requiredWidthAdjustment);
+
+ // Decide how much to give space to / take space from each grabbing column
+ double wrel = 1.0 / resizing.size();
+
+ double[] weightedShares = new double[resizing.size()];
+ for (int i = 0; i < resizing.size(); ++i) {
+ TreeColumn col = resizing.get(i);
+ Column c = (Column) col.getData();
+ if (totalWeight == 0) {
+ weightedShares[i] = wrel;
+ } else {
+ weightedShares[i] = (double) c.getWeight() / (double) totalWeight;
+ }
+ }
+ //System.out.println("grabbing columns:" + resizing);
+ //System.out.println("weighted space distribution: " + Arrays.toString(weightedShares));
+
+ // Always shrink the columns if necessary, but don't enlarge before
+ // there is sufficient space to at least give all resizable columns
+ // some more width.
+ if (diff < 0 || (diff > 0 && diff > resizing.size())) {
+ // Need to either shrink or enlarge the resizable columns if possible.
+ for (int i = 0; i < resizing.size(); ++i) {
+ TreeColumn col = resizing.get(i);
+ Column c = (Column) col.getData();
+ int cw = col.getWidth();
+ //double wrel = (double) cw / (double) resizingWidth;
+ //int delta = Math.min((int) Math.round(wrel * diff), requiredWidthAdjustment);
+ double ddelta = weightedShares[i] * diff;
+ int delta = 0;
+ if (diff < 0) {
+ delta = (int) Math.floor(ddelta);
+ } else {
+ delta = Math.min((int) Math.floor(ddelta), requiredWidthAdjustment);
+ }
+ //System.out.println("size delta(" + col.getText() + "): " + ddelta + " => " + delta);
+ //System.out.println("argh(" + col.getText() + "): " + c.getWidth() + " vs. " + col.getWidth() + " vs. " + (cw+delta));
+ int newWidth = Math.max(c.getWidth(), cw + delta);
+ requiredWidthAdjustment -= (newWidth - cw);
+ col.setWidth(newWidth);
+ }
+ }
+
+ //System.out.println("FILLER WIDTH LEFT: " + requiredWidthAdjustment);
+
+ TreeColumn last = tree.getColumn(columnCount - 1);
+ // HACK: see #setColumns for why this is here.
+ if (FILLER.equals(last.getText())) {
+ last.setWidth(Math.max(0, requiredWidthAdjustment));
+ }
+ }
+ } finally {
+ refreshingColumnSizes = false;
+ }
+ */
+ }
+
+ private void doDispose() {
+ explorerContext.dispose();
+
+ // No longer necessary, the used executors are shared.
+ //scheduler.shutdown();
+ //scheduler2.shutdown();
+
+ processors.clear();
+ detachPrimitiveProcessors();
+ primitiveProcessors.clear();
+ dataSources.clear();
+
+ pendingItems.clear();
+
+ rootContext = null;
+
+ contextToItem.clear();
+
+ mouseListeners.clear();
+
+ selectionProvider.clearListeners();
+ selectionProvider = null;
+ selectionDataResolver = null;
+ selectionRefreshContexts.clear();
+ selectedItems.clear();
+ originalFont = null;
+
+ localResourceManager.dispose();
+
+ // Must shutdown image loader job before disposing its ResourceManager
+ imageLoaderJob.dispose();
+ imageLoaderJob.cancel();
+ try {
+ imageLoaderJob.join();
+ } catch (InterruptedException e) {
+ ErrorLogger.defaultLogError(e);
+ }
+ resourceManager.dispose();
+
+ postSelectionProvider.dispose();
+
+ }
+
+ private void expandVirtual(final Event event) {
+ TreeItem item = (TreeItem) event.item;
+ assert (item != null);
+ NodeContext context = (NodeContext) item.getData();
+ assert (context != null);
+
+ GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item));
+ NodeContext[] children = manager.query(context, BuiltinKeys.FINAL_CHILDREN);
+ int maxChildren = getMaxChildren(manager, context);
+ item.setItemCount(children.length < maxChildren ? children.length : maxChildren);
+ }
+
+ private NodeContext getNodeContext(TreeItem item) {
+ assert(item != null);
+
+ NodeContext context = (NodeContext)item.getData();
+ assert(context != null);
+
+ return context;
+ }
+
+ private NodeContext getParentContext(TreeItem item) {
+ TreeItem parentItem = item.getParentItem();
+ if(parentItem != null) {
+ return getNodeContext(parentItem);
+ } else {
+ return rootContext;
+ }
+ }
+
+ private static final String LISTENER_SET_INDICATOR = "LSI";
+ private static final String PENDING = "PENDING";
+ private int contextSelectionChangeModCount = 0;
+
+ /**
+ * Only invoked for SWT.VIRTUAL widgets.
+ *
+ * @param event
+ */
+ private void setData(final Event event) {
+ assert (event != null);
+ TreeItem item = (TreeItem) event.item;
+ assert (item != null);
+
+ // Based on experience it seems to be possible that
+ // SetData events are sent for disposed TreeItems.
+ if (item.isDisposed() || item.getData(PENDING) != null)
+ return;
+
+ //System.out.println("GE.SetData " + item);
+
+ GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
+
+ NodeContext parentContext = getParentContext(item);
+ assert (parentContext != null);
+
+ NodeContext[] parentChildren = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
+
+ // Some children have disappeared since counting
+ if (event.index < 0) {
+ ErrorLogger.defaultLogError("GraphExplorer.setData: how can event.index be < 0: " + event.index + " ??", new Exception());
+ return;
+ }
+ if (event.index >= parentChildren.length)
+ return;
+
+ NodeContext context = parentChildren[event.index];
+ assert (context != null);
+ item.setData(context);
+
+ // Manage NodeContext -> TreeItem mappings
+ contextToItem.map(context, item);
+ if (item.getData(LISTENER_SET_INDICATOR) == null) {
+ // This "if" exists because setData will get called many
+ // times for the same (NodeContext, TreeItem) pairs.
+ // Each TreeItem only needs one listener, but this
+ // is needed to tell whether it already has a listener
+ // or not.
+ item.setData(LISTENER_SET_INDICATOR, LISTENER_SET_INDICATOR);
+ item.addListener(SWT.Dispose, itemDisposeListener);
+ }
+
+ boolean isExpanded = manager.query(context, BuiltinKeys.IS_EXPANDED);
+
+ PrunedChildrenResult children = manager.query(context, BuiltinKeys.PRUNED_CHILDREN);
+ int maxChildren = getMaxChildren(manager, context);
+ //item.setItemCount(children.getPrunedChildren().length < maxChildren ? children.getPrunedChildren().length : maxChildren);
+
+ NodeContext[] pruned = children.getPrunedChildren();
+ int count = Math.min(pruned.length, maxChildren);
+
+ if (isExpanded || item.getItemCount() > 1) {
+ item.setItemCount(count);
+ TreeItem[] childItems = item.getItems();
+ for(int i=0;i 1) && !isExpanded) {
+ //System.out.println("NOT EXPANDED(" +context + ", " + item + ")");
+ int level = getTreeItemLevel(item);
+ if ((autoExpandLevel == ALL_LEVELS || level <= autoExpandLevel)
+ && !explorerContext.autoExpanded.containsKey(context))
+ {
+ //System.out.println("AUTO-EXPANDING(" + context + ", " + item + ")");
+ explorerContext.autoExpanded.put(context, Boolean.TRUE);
+ setExpanded(context, true);
+ }
+ }
+
+ item.setExpanded(isExpanded);
+
+ if ((tree.getStyle() & SWT.CHECK) != 0) {
+ CheckedState checked = manager.query(context, BuiltinKeys.IS_CHECKED);
+ item.setChecked(CheckedState.CHECKED_STATES.contains(checked));
+ item.setGrayed(CheckedState.GRAYED == checked);
+ }
+
+ //System.out.println("GE.SetData completed " + item);
+
+ // This test makes sure that selectionProvider holds the correct
+ // selection with respect to the actual selection stored by the virtual
+ // SWT Tree widget.
+ // The data items shown below the items occupied by the selected and now removed data
+ // will be squeezed to use the tree items previously used for the now
+ // removed data. When this happens, the NodeContext items stored by the
+ // tree items will be different from what the GraphExplorer's
+ // ISelectionProvider thinks the selection currently is. To compensate,
+ // 1. Recognize the situation
+ // 2. ASAP set the selection provider selection to what is actually
+ // offered by the tree widget.
+ NodeContext selectedContext = selectedItems.get(item);
+// System.out.println("selectedContext(" + item + "): " + selectedContext);
+ if (selectedContext != null && !selectedContext.equals(context)) {
+ final int modCount = ++contextSelectionChangeModCount;
+// System.out.println("SELECTION MUST BE UPDATED (modCount=" + modCount + "): " + item);
+// System.out.println(" old context: " + selectedContext);
+// System.out.println(" new context: " + context);
+// System.out.println(" provider selection: " + selectionProvider.getSelection());
+// System.out.println(" widget selection: " + getWidgetSelection());
+ ThreadUtils.asyncExec(thread, new Runnable() {
+ @Override
+ public void run() {
+ if (isDisposed())
+ return;
+ int count = contextSelectionChangeModCount;
+// System.out.println("MODCOUNT: " + modCount + " vs. " + count);
+ if (modCount != count)
+ return;
+ widgetSelectionChanged(true);
+ }
+ });
+ }
+
+ // This must be done to keep the visible tree selection properly
+ // in sync with the selectionProvider JFace proxy of this class in
+ // cases where an in-line editor was previously active for the node
+ // context.
+ if (selectionRefreshContexts.remove(context)) {
+ final ISelection currentSelection = selectionProvider.getSelection();
+ // asyncExec is here to prevent ui glitches that
+ // seem to occur if the selection setting is done
+ // directly here in between setData invocations.
+ ThreadUtils.asyncExec(thread, new Runnable() {
+ @Override
+ public void run() {
+ if (isDisposed())
+ return;
+// System.out.println("REFRESHING SELECTION: " + currentSelection);
+// System.out.println("BEFORE setSelection: " + Arrays.toString(tree.getSelection()));
+// System.out.println("BEFORE setSelection: " + selectionProvider.getSelection());
+ setSelection(currentSelection, true);
+// System.out.println("AFTER setSelection: " + Arrays.toString(tree.getSelection()));
+// System.out.println("AFTER setSelection: " + selectionProvider.getSelection());
+ }
+ });
+ }
+
+ // TODO: doesn't work if any part of the node path that should be
+ // revealed is out of view.
+ // Disabled until a better solution is devised.
+ // Suggestion: include item indexes into the stored node context path
+ // to make it possible for this method to know whether the current
+ // node path segment is currently out of view based on event.index.
+ // If out of view, this code needs to scroll the view programmatically
+ // onwards.
+// if (currentTopNodePathIndex >= 0 && topNodePath.length > 0) {
+// NodeContext topNode = topNodePath[currentTopNodePathIndex];
+// if (topNode.equals(context)) {
+// final TreeItem topItem = item;
+// ++currentTopNodePathIndex;
+// if (currentTopNodePathIndex >= topNodePath.length) {
+// // Mission accomplished. End search for top node here.
+// topNodePath = NodeContext.NONE;
+// currentTopNodePathIndex = -1;
+// }
+// ThreadUtils.asyncExec(thread, new Runnable() {
+// @Override
+// public void run() {
+// if (isDisposed())
+// return;
+// tree.setTopItem(topItem);
+// }
+// });
+// }
+// }
+
+ // Check if vertical scroll bar has become visible and refresh layout.
+ ScrollBar verticalBar = tree.getVerticalBar();
+ if(verticalBar != null) {
+ boolean currentlyVerticalBarVisible = verticalBar.isVisible();
+ if (verticalBarVisible != currentlyVerticalBarVisible) {
+ verticalBarVisible = currentlyVerticalBarVisible;
+ Composite parent = tree.getParent();
+ if (parent != null)
+ parent.layout();
+ }
+ }
+ }
+
+ /**
+ * @return see {@link GraphExplorer#setAutoExpandLevel(int)} for how the
+ * return value is calculated. Items without parents have level=2,
+ * their children level=3, etc. Returns 0 for invalid items
+ */
+ private int getTreeItemLevel(TreeItem item) {
+ if (item == null)
+ return 0;
+ int level = 1;
+ for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), ++level);
+ //System.out.println("\tgetTreeItemLevel(" + parent + ")");
+ //System.out.println("level(" + item + "): " + level);
+ return level;
+ }
+
+ /**
+ * @param node
+ * @return
+ */
+ private NodeContext[] getNodeContextPathSegments(NodeContext node) {
+ TreeItem item = contextToItem.getRight(node);
+ if (item == null)
+ return NodeContext.NONE;
+ int level = getTreeItemLevel(item);
+ if (level == 0)
+ return NodeContext.NONE;
+ // Exclude root from the saved node path.
+ --level;
+ NodeContext[] segments = new NodeContext[level];
+ for (TreeItem parent = item; parent != null; parent = parent.getParentItem(), --level) {
+ NodeContext ctx = (NodeContext) item.getData();
+ if (ctx == null)
+ return NodeContext.NONE;
+ segments[level-1] = ctx;
+ }
+ return segments;
+ }
+
+ /**
+ * @param node
+ * @return
+ */
+ @SuppressWarnings("unused")
+ private NodeContextPath getNodeContextPath(NodeContext node) {
+ NodeContext[] path = getNodeContextPathSegments(node);
+ return new NodeContextPath(path);
+ }
+
+ void setImage(NodeContext node, TreeItem item, Imager imager, Collection decorators, int itemIndex) {
+ Image[] images = columnImageArray;
+ Arrays.fill(images, null);
+ if (imager == null) {
+ item.setImage(images);
+ return;
+ }
+
+ Object[] descOrImage = columnDescOrImageArray;
+ Arrays.fill(descOrImage, null);
+ boolean finishLoadingInJob = false;
+ int index = 0;
+ for (Column column : columns) {
+ String key = column.getKey();
+ ImageDescriptor desc = imager.getImage(key);
+ if (desc != null) {
+ // Attempt to decorate the label
+ if (!decorators.isEmpty()) {
+ for (ImageDecorator id : decorators) {
+ ImageDescriptor ds = id.decorateImage(desc, key, itemIndex);
+ if (ds != null)
+ desc = ds;
+ }
+ }
+
+ // Try resolving only cached images here and now
+ Object img = localResourceManager.find(desc);
+ if (img == null)
+ img = resourceManager.find(desc);
+
+ images[index] = img != null ? (Image) img : null;
+ descOrImage[index] = img == null ? desc : img;
+ finishLoadingInJob |= img == null;
+ }
+ ++index;
+ }
+
+ // Finish loading the final image in the image loader job if necessary.
+ if (finishLoadingInJob) {
+ // Prevent UI from flashing unnecessarily by reusing the old image
+ // in the item if it exists.
+ for (int c = 0; c < columns.length; ++c) {
+ Image img = item.getImage(c);
+ if (img != null)
+ images[c] = img;
+ }
+ item.setImage(images);
+
+ // Schedule loading to another thread to refrain from blocking
+ // the UI with database operations.
+ queueImageTask(item, new ImageTask(
+ node,
+ item,
+ Arrays.copyOf(descOrImage, descOrImage.length)));
+ } else {
+ // Set any images that were resolved.
+ item.setImage(images);
+ }
+ }
+
+ private void queueImageTask(TreeItem item, ImageTask task) {
+ synchronized (imageTasks) {
+ imageTasks.put(item, task);
+ }
+ imageLoaderJob.scheduleIfNecessary(100);
+ }
+
+ /**
+ * Invoked in a job worker thread.
+ *
+ * @param monitor
+ * @see ImageLoaderJob
+ */
+ @Override
+ protected IStatus setPendingImages(IProgressMonitor monitor) {
+ ImageTask[] tasks = null;
+ synchronized (imageTasks) {
+ tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]);
+ imageTasks.clear();
+ }
+ if (tasks.length == 0)
+ return Status.OK_STATUS;
+
+ MultiStatus status = null;
+
+ // Load missing images
+ for (ImageTask task : tasks) {
+ Object[] descs = task.descsOrImages;
+ for (int i = 0; i < descs.length; ++i) {
+ Object obj = descs[i];
+ if (obj instanceof ImageDescriptor) {
+ ImageDescriptor desc = (ImageDescriptor) obj;
+ try {
+ descs[i] = resourceManager.get((ImageDescriptor) desc);
+ } catch (DeviceResourceException e) {
+ if (status == null)
+ status = new MultiStatus(Activator.PLUGIN_ID, 0, "Problems loading images:", null);
+ status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Image descriptor loading failed: " + desc, e));
+ }
+ }
+ }
+ }
+
+ // Perform final UI updates in the UI thread.
+ final ImageTask[] _tasks = tasks;
+ thread.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!tree.isDisposed()) {
+ tree.setRedraw(false);
+ setImages(_tasks);
+ tree.setRedraw(true);
+ }
+ }
+ });
+
+ return status != null ? status : Status.OK_STATUS;
+ }
+
+ /**
+ * Invoked in the UI thread only.
+ *
+ * @param task
+ */
+ void setImages(ImageTask[] tasks) {
+ for (ImageTask task : tasks)
+ if (task != null)
+ setImage(task);
+ }
+
+ /**
+ * Invoked in the UI thread only.
+ *
+ * @param task
+ */
+ void setImage(ImageTask task) {
+ // Be sure not to process disposed items.
+ if (task.item.isDisposed())
+ return;
+ // Discard this task if the TreeItem has switched owning NodeContext.
+ if (!contextToItem.contains(task.node, task.item))
+ return;
+
+ Object[] descs = task.descsOrImages;
+ Image[] images = columnImageArray;
+ Arrays.fill(images, null);
+ for (int i = 0; i < descs.length; ++i) {
+ Object desc = descs[i];
+ if (desc instanceof Image) {
+ images[i] = (Image) desc;
+ }
+ }
+ task.item.setImage(images);
+ }
+
+ void setText(TreeItem item, Labeler labeler, Collection decorators, int itemIndex) {
+ if (labeler != null) {
+ String[] texts = new String[columns.length];
+ int index = 0;
+ Map labels = labeler.getLabels();
+ Map runtimeLabels = labeler.getRuntimeLabels();
+ for (Column column : columns) {
+ String key = column.getKey();
+ String s = null;
+ if (runtimeLabels != null) s = runtimeLabels.get(key);
+ if (s == null) s = labels.get(key);
+ if (s != null) {
+ FontDescriptor font = originalFont;
+ ColorDescriptor bg = originalBackground;
+ ColorDescriptor fg = originalForeground;
+
+ // Attempt to decorate the label
+ if (!decorators.isEmpty()) {
+ for (LabelDecorator ld : decorators) {
+ String ds = ld.decorateLabel(s, key, itemIndex);
+ if (ds != null)
+ s = ds;
+
+ FontDescriptor dfont = ld.decorateFont(font, key, itemIndex);
+ if (dfont != null)
+ font = dfont;
+
+ ColorDescriptor dbg = ld.decorateBackground(bg, key, itemIndex);
+ if (dbg != null)
+ bg = dbg;
+
+ ColorDescriptor dfg = ld.decorateForeground(fg, key, itemIndex);
+ if (dfg != null)
+ fg = dfg;
+ }
+ }
+
+ if (font != originalFont) {
+ //System.out.println("set font: " + index + ": " + font);
+ item.setFont(index, (Font) localResourceManager.get(font));
+ }
+ if (bg != originalBackground)
+ item.setBackground(index, (Color) localResourceManager.get(bg));
+ if (fg != originalForeground)
+ item.setForeground(index, (Color) localResourceManager.get(fg));
+
+ texts[index] = s;
+ }
+ ++index;
+ }
+ item.setText(texts);
+ } else {
+ item.setText(Labeler.NO_LABEL);
+ }
+ }
+
+ void setTextAndImage(TreeItem item, NodeQueryManager manager, NodeContext context, int itemIndex) {
+ Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
+ if (labeler != null) {
+ labeler.setListener(labelListener);
+ }
+ Imager imager = manager.query(context, BuiltinKeys.SELECTED_IMAGER);
+ Collection labelDecorators = manager.query(context, BuiltinKeys.LABEL_DECORATORS);
+ Collection imageDecorators = manager.query(context, BuiltinKeys.IMAGE_DECORATORS);
+
+ setText(item, labeler, labelDecorators, itemIndex);
+ setImage(context, item, imager, imageDecorators, itemIndex);
+ }
+
+ @Override
+ public void setFocus() {
+ tree.setFocus();
+ }
+
+ @Override
+ public T query(NodeContext context, CacheKey key) {
+ return this.explorerContext.cache.get(context, key);
+ }
+
+ @Override
+ public boolean isDisposed() {
+ return disposed;
+ }
+
+ protected void assertNotDisposed() {
+ if (isDisposed())
+ throw new IllegalStateException("disposed");
+ }
+
+
+
+ /**
+ * @param selection
+ * @param forceControlUpdate
+ * @thread any
+ */
+ public void setSelection(final ISelection selection, boolean forceControlUpdate) {
+ assertNotDisposed();
+ boolean equalsOld = selectionProvider.selectionEquals(selection);
+ if (equalsOld && !forceControlUpdate) {
+ // Just set the selection object instance, fire no events nor update
+ // the viewer selection.
+ selectionProvider.setSelection(selection);
+ } else {
+ // Schedule viewer and selection update if necessary.
+ if (tree.isDisposed())
+ return;
+ Display d = tree.getDisplay();
+ if (d.getThread() == Thread.currentThread()) {
+ updateSelectionToControl(selection);
+ } else {
+ d.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (tree.isDisposed())
+ return;
+ updateSelectionToControl(selection);
+ }
+ });
+ }
+ }
+ }
+
+
+ /* Contains the best currently found tree item and its priority
+ */
+ private static class SelectionResolutionStatus {
+ int bestPriority = Integer.MAX_VALUE;
+ TreeItem bestItem;
+ }
+
+ /**
+ * @param selection
+ * @thread SWT
+ */
+ private void updateSelectionToControl(ISelection selection) {
+ if (selectionDataResolver == null)
+ return;
+ if (!(selection instanceof IStructuredSelection))
+ return;
+
+ // Initialize selection resolution status map
+ IStructuredSelection iss = (IStructuredSelection) selection;
+ final THashMap statusMap =
+ new THashMap(iss.size());
+ for(Iterator> it = iss.iterator(); it.hasNext();) {
+ Object selectionElement = it.next();
+ Object resolvedElement = selectionDataResolver.resolve(selectionElement);
+ statusMap.put(
+ resolvedElement,
+ new SelectionResolutionStatus());
+ }
+
+ // Iterate all tree items and try to match them to the selection
+ iterateTreeItems(new TObjectProcedure() {
+ @Override
+ public boolean execute(TreeItem treeItem) {
+ NodeContext nodeContext = (NodeContext)treeItem.getData();
+ if(nodeContext == null)
+ return true;
+ SelectionResolutionStatus status = statusMap.get(nodeContext);
+ if(status != null) {
+ status.bestPriority = 0; // best possible match
+ status.bestItem = treeItem;
+ return true;
+ }
+
+ Object input = nodeContext.getConstant(BuiltinKeys.INPUT);
+ status = statusMap.get(input);
+ if(status != null) {
+ NodeType nodeType = nodeContext.getConstant(NodeType.TYPE);
+ int curPriority = nodeType instanceof EntityNodeType
+ ? 1 // Prefer EntityNodeType matches to other node types
+ : 2;
+ if(curPriority < status.bestPriority) {
+ status.bestPriority = curPriority;
+ status.bestItem = treeItem;
+ }
+ }
+ return true;
+ }
+ });
+
+ // Update selection
+ ArrayList items = new ArrayList(statusMap.size());
+ for(SelectionResolutionStatus status : statusMap.values())
+ if(status.bestItem != null)
+ items.add(status.bestItem);
+ select(items.toArray(new TreeItem[items.size()]));
+ }
+
+ /**
+ * @thread SWT
+ */
+ public ISelection getWidgetSelection() {
+ TreeItem[] items = tree.getSelection();
+ if (items.length == 0)
+ return StructuredSelection.EMPTY;
+
+ List nodes = new ArrayList(items.length);
+
+ // Caches for resolving node contexts the hard way if necessary.
+ GENodeQueryManager manager = null;
+ NodeContext lastParentContext = null;
+ NodeContext[] lastChildren = null;
+
+ for (int i = 0; i < items.length; i++) {
+ TreeItem item = items[i];
+ NodeContext ctx = (NodeContext) item.getData();
+ // It may happen due to the virtual nature of the tree control
+ // that it contains TreeItems which have not yet been ran through
+ // #setData(Event).
+ if (ctx != null) {
+ nodes.add(ctx);
+ } else {
+ TreeItem parentItem = item.getParentItem();
+ NodeContext parentContext = parentItem != null ? getNodeContext(parentItem) : rootContext;
+ if (parentContext != null) {
+ NodeContext[] children = lastChildren;
+ if (parentContext != lastParentContext) {
+ if (manager == null)
+ manager = new GENodeQueryManager(this.explorerContext, null, null, null);
+ lastChildren = children = manager.query(parentContext, BuiltinKeys.FINAL_CHILDREN);
+ lastParentContext = parentContext;
+ }
+ int index = parentItem != null ? parentItem.indexOf(item) : tree.indexOf(item);
+ if (index >= 0 && index < children.length) {
+ NodeContext child = children[index];
+ if (child != null) {
+ nodes.add(child);
+ // Cache NodeContext in TreeItem for faster access
+ item.setData(child);
+ }
+ }
+ }
+ }
+ }
+ //System.out.println("widget selection " + items.length + " items / " + nodes.size() + " node contexts");
+ ISelection selection = constructSelection(nodes.toArray(NodeContext.NONE));
+ return selection;
+ }
+
+ @Override
+ public TransientExplorerState getTransientState() {
+ if (!thread.currentThreadAccess())
+ throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread());
+ return transientState;
+ }
+
+ /**
+ * @param item
+ * @thread SWT
+ */
+ private void select(TreeItem item) {
+ tree.setSelection(item);
+ tree.showSelection();
+ selectionProvider.setAndFireNonEqualSelection(constructSelection((NodeContext) item.getData()));
+ }
+
+ /**
+ * @param items
+ * @thread SWT
+ */
+ private void select(TreeItem[] items) {
+ //System.out.println("Select: " + Arrays.toString(items));
+ tree.setSelection(items);
+ tree.showSelection();
+ NodeContext[] data = new NodeContext[items.length];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (NodeContext) items[i].getData();
+ }
+ selectionProvider.setAndFireNonEqualSelection(constructSelection(data));
+ }
+
+ private void iterateTreeItems(TObjectProcedure procedure) {
+ for(TreeItem item : tree.getItems())
+ if(!iterateTreeItems(item, procedure))
+ return;
+ }
+
+ private boolean iterateTreeItems(TreeItem item,
+ TObjectProcedure procedure) {
+ if(!procedure.execute(item))
+ return false;
+ if(item.getExpanded())
+ for(TreeItem child : item.getItems())
+ if(!iterateTreeItems(child, procedure))
+ return false;
+ return true;
+ }
+
+ /**
+ * @param item
+ * @param context
+ * @return
+ */
+ private boolean trySelect(TreeItem item, Object input) {
+ NodeContext itemCtx = (NodeContext) item.getData();
+ if (itemCtx != null) {
+ if (input.equals(itemCtx.getConstant(BuiltinKeys.INPUT))) {
+ select(item);
+ return true;
+ }
+ }
+ if (item.getExpanded()) {
+ for (TreeItem child : item.getItems()) {
+ if (trySelect(child, input))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean equalsEnough(NodeContext c1, NodeContext c2) {
+
+ Object input1 = c1.getConstant(BuiltinKeys.INPUT);
+ Object input2 = c2.getConstant(BuiltinKeys.INPUT);
+ if(!ObjectUtils.objectEquals(input1, input2))
+ return false;
+
+ Object type1 = c1.getConstant(NodeType.TYPE);
+ Object type2 = c2.getConstant(NodeType.TYPE);
+ if(!ObjectUtils.objectEquals(type1, type2))
+ return false;
+
+ return true;
+
+ }
+
+ private NodeContext tryFind(NodeContext context) {
+ for (TreeItem item : tree.getItems()) {
+ NodeContext found = tryFind(item, context);
+ if(found != null) return found;
+ }
+ return null;
+ }
+
+ private NodeContext tryFind(TreeItem item, NodeContext context) {
+ NodeContext itemCtx = (NodeContext) item.getData();
+ if (itemCtx != null) {
+ if (equalsEnough(context, itemCtx)) {
+ return itemCtx;
+ }
+ }
+ if (item.getExpanded()) {
+ for (TreeItem child : item.getItems()) {
+ NodeContext found = tryFind(child, context);
+ if(found != null) return found;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean select(NodeContext context) {
+
+ assertNotDisposed();
+
+ if (context == null || context.equals(rootContext)) {
+ tree.deselectAll();
+ selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY);
+ return true;
+ }
+
+// if (context.equals(rootContext)) {
+// tree.deselectAll();
+// selectionProvider.setAndFireNonEqualSelection(constructSelection(context));
+// return;
+// }
+
+ Object input = context.getConstant(BuiltinKeys.INPUT);
+
+ for (TreeItem item : tree.getItems()) {
+ if (trySelect(item, input))
+ return true;
+ }
+
+ return false;
+
+ }
+
+ private NodeContext tryFind2(NodeContext context) {
+ Set ctxs = contextToItem.getLeftSet();
+ for(NodeContext c : ctxs)
+ if(equalsEnough(c, context))
+ return c;
+ return null;
+ }
+
+ private boolean waitVisible(NodeContext parent, NodeContext context) {
+ long start = System.nanoTime();
+
+ TreeItem parentItem = contextToItem.getRight(parent);
+
+ if(parentItem == null)
+ return false;
+
+ while(true) {
+ NodeContext target = tryFind2(context);
+ if(target != null) {
+ TreeItem item = contextToItem.getRight(target);
+ if (!(item.getParentItem().equals(parentItem)))
+ return false;
+ tree.setTopItem(item);
+ return true;
+ }
+
+ Display.getCurrent().readAndDispatch();
+ long duration = System.nanoTime() - start;
+ if(duration > 10e9)
+ return false;
+ }
+ }
+
+ private boolean selectPathInternal(NodeContext[] contexts, int position) {
+ //System.out.println("NodeContext path : " + contexts);
+
+ NodeContext head = tryFind(contexts[position]);
+ // tryFind may return null for positions, that actually have NodeContext.
+ if (head == null)
+ return false;
+
+ if(position == contexts.length-1) {
+ return select(head);
+
+ }
+
+ //setExpanded(head, true);
+
+ if(!waitVisible(head, contexts[position+1]))
+ return false;
+
+ setExpanded(head, true);
+
+ return selectPathInternal(contexts, position+1);
+
+ }
+
+ @Override
+ public boolean selectPath(Collection contexts) {
+
+ if(contexts == null) throw new IllegalArgumentException("Null list is not allowed");
+ if(contexts.isEmpty()) throw new IllegalArgumentException("Empty list is not allowed");
+
+ return selectPathInternal(contexts.toArray(new NodeContext[contexts.size()]), 0);
+
+ }
+
+ @Override
+ public boolean isVisible(NodeContext context) {
+
+ for (TreeItem item : tree.getItems()) {
+ NodeContext found = tryFind(item, context);
+ if(found != null)
+ return true;
+ }
+
+ return false;
+
+ }
+
+ protected ISelection constructSelection(NodeContext... contexts) {
+ if (contexts == null)
+ throw new IllegalArgumentException("null contexts");
+ if (contexts.length == 0)
+ return StructuredSelection.EMPTY;
+ if (selectionFilter == null)
+ return new StructuredSelection(transformSelection(contexts));
+ return new StructuredSelection( transformSelection(filter(selectionFilter, contexts)) );
+ }
+
+ protected Object[] transformSelection(Object[] objects) {
+ return selectionTransformation.apply(this, objects);
+ }
+
+ protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
+ int len = contexts.length;
+ Object[] objects = new Object[len];
+ for (int i = 0; i < len; ++i)
+ objects[i] = filter.filter(contexts[i]);
+ return objects;
+ }
+
+ @Override
+ public void setExpanded(final NodeContext context, final boolean expanded) {
+ assertNotDisposed();
+ ThreadUtils.asyncExec(thread, new Runnable() {
+ @Override
+ public void run() {
+ if (!isDisposed())
+ doSetExpanded(context, expanded);
+ }
+ });
+ }
+
+ private void doSetExpanded(NodeContext context, boolean expanded) {
+ //System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
+ TreeItem item = contextToItem.getRight(context);
+ if (item != null) {
+ item.setExpanded(expanded);
+ }
+ PrimitiveQueryProcessor> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
+ if (pqp instanceof IsExpandedProcessor) {
+ IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
+ iep.replaceExpanded(context, expanded);
+ }
+ }
+
+ @Override
+ public void setColumnsVisible(boolean visible) {
+ columnsAreVisible = visible;
+ if(tree != null) tree.setHeaderVisible(columnsAreVisible);
+ }
+
+ @Override
+ public void setColumns(final Column[] columns) {
+ setColumns(columns, null);
+ }
+
+ @Override
+ public void setColumns(final Column[] columns, Consumer> callback) {
+ assertNotDisposed();
+ checkUniqueColumnKeys(columns);
+
+ Display d = tree.getDisplay();
+ if (d.getThread() == Thread.currentThread())
+ doSetColumns(columns, callback);
+ else
+ d.asyncExec(() -> {
+ if (tree.isDisposed())
+ return;
+ doSetColumns(columns, callback);
+ });
+ }
+
+ private void checkUniqueColumnKeys(Column[] cols) {
+ Set usedColumnKeys = new HashSet();
+ List duplicateColumns = new ArrayList();
+ for (Column c : cols) {
+ if (!usedColumnKeys.add(c.getKey()))
+ duplicateColumns.add(c);
+ }
+ if (!duplicateColumns.isEmpty()) {
+ throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
+ }
+ }
+
+ /**
+ * Only meant to be invoked from the SWT UI thread.
+ *
+ * @param cols
+ */
+ private void doSetColumns(Column[] cols, Consumer> callback) {
+ // Attempt to keep previous column widths.
+ Map prevWidths = new HashMap<>();
+ for (TreeColumn column : tree.getColumns()) {
+ Column c = (Column) column.getData();
+ if (c != null) {
+ prevWidths.put(c.getKey(), column.getWidth());
+ column.dispose();
+ }
+ }
+
+ HashMap keyToIndex = new HashMap<>();
+ for (int i = 0; i < cols.length; ++i) {
+ keyToIndex.put(cols[i].getKey(), i);
+ }
+
+ this.columns = Arrays.copyOf(cols, cols.length);
+ //this.columns[cols.length] = FILLER_COLUMN;
+ this.columnKeyToIndex = keyToIndex;
+ this.columnImageArray = new Image[cols.length];
+ this.columnDescOrImageArray = new Object[cols.length];
+
+ Map map = new HashMap<>();
+
+ tree.setHeaderVisible(columnsAreVisible);
+ for (Column column : columns) {
+ TreeColumn c = new TreeColumn(tree, toSWT(column.getAlignment()));
+ map.put(column, c);
+ c.setData(column);
+ c.setText(column.getLabel());
+ c.setToolTipText(column.getTooltip());
+
+ int cw = column.getWidth();
+
+ // Try to keep previous widths
+ Integer w = prevWidths.get(column.getKey());
+ if (w != null)
+ c.setWidth(w);
+ else if (cw != Column.DEFAULT_CONTROL_WIDTH)
+ c.setWidth(cw);
+ else {
+ // Go for some kind of default settings then...
+ if (ColumnKeys.PROPERTY.equals(column.getKey()))
+ c.setWidth(150);
+ else
+ c.setWidth(50);
+ }
+
+// if (!column.hasGrab() && !FILLER.equals(column.getKey())) {
+// c.addListener(SWT.Resize, resizeListener);
+// c.setResizable(true);
+// } else {
+// //c.setResizable(false);
+// }
+
+ }
+
+ if(callback != null) callback.accept(map);
+
+ // Make sure the explorer fits the columns properly after initialization.
+ SWTUtils.asyncExec(tree, () -> {
+ if (!tree.isDisposed())
+ refreshColumnSizes();
+ });
+ }
+
+ int toSWT(Align alignment) {
+ switch (alignment) {
+ case LEFT: return SWT.LEFT;
+ case CENTER: return SWT.CENTER;
+ case RIGHT: return SWT.RIGHT;
+ default: throw new Error("unhandled alignment: " + alignment);
+ }
+ }
+
+ @Override
+ public Column[] getColumns() {
+ return Arrays.copyOf(columns, columns.length);
+ }
+
+ private void detachPrimitiveProcessors() {
+ for (PrimitiveQueryProcessor> p : primitiveProcessors.values()) {
+ if (p instanceof ProcessorLifecycle) {
+ ((ProcessorLifecycle) p).detached(this);
+ }
+ }
+ }
+
+ private void clearPrimitiveProcessors() {
+ for (PrimitiveQueryProcessor> p : primitiveProcessors.values()) {
+ if (p instanceof ProcessorLifecycle) {
+ ((ProcessorLifecycle) p).clear();
+ }
+ }
+ }
+
+ Listener resizeListener = new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ // Prevent infinite recursion.
+ if (refreshingColumnSizes)
+ return;
+ //TreeColumn column = (TreeColumn) event.widget;
+ //Column c = (Column) column.getData();
+ refreshColumnSizes();
+ }
+ };
+
+ Listener itemDisposeListener = new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ if (event.type == SWT.Dispose) {
+ if (event.widget instanceof TreeItem) {
+ TreeItem ti = (TreeItem) event.widget;
+ //NodeContext ctx = (NodeContext) ti.getData();
+// System.out.println("DISPOSE CONTEXT TO ITEM: " + ctx + " -> " + System.identityHashCode(ti));
+// System.out.println(" map size BEFORE: " + contextToItem.size());
+ @SuppressWarnings("unused")
+ NodeContext removed = contextToItem.removeWithRight(ti);
+// System.out.println(" REMOVED: " + removed);
+// System.out.println(" map size AFTER: " + contextToItem.size());
+ }
+ }
+ }
+ };
+
+ /**
+ *
+ */
+ LabelerListener labelListener = new LabelerListener() {
+ @Override
+ public boolean columnModified(final NodeContext context, final String key, final String newLabel) {
+ //System.out.println("column " + key + " modified for " + context + " to " + newLabel);
+ if (tree.isDisposed())
+ return false;
+
+ synchronized (labelRefreshRunnables) {
+ Runnable refresher = new Runnable() {
+ @Override
+ public void run() {
+ // Tree is guaranteed to be non-disposed if this is invoked.
+
+ // contextToItem should be accessed only in the SWT thread to keep things thread-safe.
+ final TreeItem item = contextToItem.getRight(context);
+ if (item == null || item.isDisposed())
+ return;
+
+ final Integer index = columnKeyToIndex.get(key);
+ if (index == null)
+ return;
+
+ //System.out.println(" found index: " + index);
+ //System.out.println(" found item: " + item);
+ try {
+ GENodeQueryManager manager = new GENodeQueryManager(explorerContext, null, null, null);
+
+ // FIXME: indexOf is quadratic
+ int itemIndex = 0;
+ TreeItem parentItem = item.getParentItem();
+ if (parentItem == null) {
+ itemIndex = tree.indexOf(item);
+ //tree.clear(parentIndex, false);
+ } else {
+ itemIndex = parentItem.indexOf(item);
+ //item.clear(parentIndex, false);
+ }
+ setTextAndImage(item, manager, context, itemIndex);
+ } catch (SWTException e) {
+ ErrorLogger.defaultLogError(e);
+ }
+ }
+ };
+ //System.out.println(System.currentTimeMillis() + " queueing label refresher: " + refresher);
+ labelRefreshRunnables.put(context, refresher);
+
+ if (!refreshIsQueued) {
+ refreshIsQueued = true;
+ long delay = 0;
+ long now = System.currentTimeMillis();
+ long elapsed = now - lastLabelRefreshScheduled;
+ if (elapsed < DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY)
+ delay = DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY - elapsed;
+ //System.out.println("scheduling with delay: " + delay + " (" + lastLabelRefreshScheduled + " -> " + now + " = " + elapsed + ")");
+ if (delay > 0) {
+ ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
+ @Override
+ public void run() {
+ scheduleImmediateLabelRefresh();
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+ } else {
+ scheduleImmediateLabelRefresh();
+ }
+ lastLabelRefreshScheduled = now;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean columnsModified(final NodeContext context, final Map columns) {
+ System.out.println("TODO: implement GraphExplorerImpl.labelListener.columnsModified");
+ return false;
+ }
+ };
+
+ private void scheduleImmediateLabelRefresh() {
+ Runnable[] runnables = null;
+ synchronized (labelRefreshRunnables) {
+ if (labelRefreshRunnables.isEmpty())
+ return;
+
+ runnables = labelRefreshRunnables.values().toArray(new Runnable[labelRefreshRunnables.size()]);
+ labelRefreshRunnables.clear();
+ refreshIsQueued = false;
+ }
+ final Runnable[] rs = runnables;
+
+ if (tree.isDisposed())
+ return;
+ tree.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (tree.isDisposed())
+ return;
+ //System.out.println(System.currentTimeMillis() + " EXECUTING " + rs.length + " label refresh runnables");
+ tree.setRedraw(false);
+ for (Runnable r : rs) {
+ r.run();
+ }
+ tree.setRedraw(true);
+ }
+ });
+ }
+
+ long lastLabelRefreshScheduled = 0;
+ boolean refreshIsQueued = false;
+ Map labelRefreshRunnables = new HashMap();
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T getAdapter(Class adapter) {
+ if(ISelectionProvider.class == adapter) return (T) postSelectionProvider;
+ else if(IPostSelectionProvider.class == adapter) return (T) postSelectionProvider;
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T getControl() {
+ return (T) tree;
+ }
+
+ /* (non-Javadoc)
+ * @see org.simantics.browsing.ui.GraphExplorer#setAutoExpandLevel(int)
+ */
+ @Override
+ public void setAutoExpandLevel(int level) {
+ this.autoExpandLevel = level;
+ }
+
+ @Override
+ public NodeQueryProcessor getProcessor(QueryKey key) {
+ return explorerContext.getProcessor(key);
+ }
+
+ @Override
+ public PrimitiveQueryProcessor getPrimitiveProcessor(PrimitiveQueryKey key) {
+ return explorerContext.getPrimitiveProcessor(key);
+ }
+
+ @Override
+ public boolean isEditable() {
+ return editable;
+ }
+
+ @Override
+ public void setEditable(boolean editable) {
+ if (!thread.currentThreadAccess())
+ throw new IllegalStateException("not in SWT display thread " + thread.getThread());
+
+ this.editable = editable;
+ Display display = tree.getDisplay();
+ tree.setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
+ }
+
+ /**
+ * For setting a more local service locator for the explorer than the global
+ * workbench service locator. Sometimes required to give this implementation
+ * access to local workbench services like IFocusService.
+ *
+ *
+ * Must be invoked during right after construction.
+ *
+ * @param serviceLocator
+ * a specific service locator or null
to use the
+ * workbench global service locator
+ */
+ public void setServiceLocator(IServiceLocator serviceLocator) {
+ if (serviceLocator == null && PlatformUI.isWorkbenchRunning())
+ serviceLocator = PlatformUI.getWorkbench();
+ this.serviceLocator = serviceLocator;
+ if (serviceLocator != null) {
+ this.contextService = (IContextService) serviceLocator.getService(IContextService.class);
+ this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class);
+ }
+ }
+
+ @Override
+ public Object getClicked(Object event) {
+ MouseEvent e = (MouseEvent)event;
+ final Tree tree = (Tree) e.getSource();
+ Point point = new Point(e.x, e.y);
+ TreeItem item = tree.getItem(point);
+
+ // No selectable item at point?
+ if (item == null)
+ return null;
+
+ Object data = item.getData();
+ return data;
+ }
+
+}