X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.browsing.ui.swt%2Fsrc%2Forg%2Fsimantics%2Fbrowsing%2Fui%2Fswt%2FGraphExplorerImpl.java;h=62b284858cd9893383719e5ec7b89632437335f9;hp=3085e6f07e58890a7bce4368ff8c63fa939e9ff6;hb=0ae2b770234dfc3cbb18bd38f324125cf0faca07;hpb=24e2b34260f219f0d1644ca7a138894980e25b14 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 3085e6f07..62b284858 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,3575 +1,3575 @@ -/******************************************************************************* - * 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.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.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(); - } - }); - - 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(); - 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(); - - 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(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(); - 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]); - - 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); - } - } - - @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; - } - -} +/******************************************************************************* + * 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.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.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(); + } + }); + + 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(); + 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(); + + 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(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(); + 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]); + + 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); + } + } + + @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; + } + +}