/*******************************************************************************
* Copyright (c) 2007, 2012 Association for Decentralized Information Management
* in Industry THTH ry.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VTT Technical Research Centre of Finland - initial API and implementation
*******************************************************************************/
package org.simantics.browsing.ui.swt;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.AssertionFailedException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.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.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 BiFunction selectionTransformation = new BiFunction() {
@Override
public Object[] apply(GraphExplorer explorer, Object[] objects) {
Object[] result = new Object[objects.length];
for (int i = 0; i < objects.length; i++) {
IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN);
context.setHint(SelectionHints.KEY_MAIN, objects[i]);
result[i] = context;
}
return result;
}
};
protected FontDescriptor originalFont;
protected ColorDescriptor originalForeground;
protected ColorDescriptor originalBackground;
/**
* The set of currently selected TreeItem instances. This set is needed
* because we need to know in {@link #setData(Event)} whether the updated
* item was a part of the current selection in which case the selection must
* be updated.
*/
private final Map selectedItems = new HashMap();
/**
* TODO: specify what this is for
*/
private final Set selectionRefreshContexts = new HashSet();
/**
* If this field is non-null, it means that if {@link #setData(Event)}
* encounters a NodeContext equal to this one, it must make the TreeItem
* assigned to that NodeContext the topmost item of the tree using
* {@link Tree#setTopItem(TreeItem)}. After this the field value is
* nullified.
*
*
* This is related to {@link #initializeState()}, i.e. explorer state
* restoration.
*/
// private NodeContext[] topNodePath = NodeContext.NONE;
// private int[] topNodePath = {};
// private int currentTopNodePathIndex = -1;
/**
* See {@link #setAutoExpandLevel(int)}
*/
private int autoExpandLevel = 0;
/**
* null
if not explicitly set through
* {@link #setServiceLocator(IServiceLocator)}.
*/
private IServiceLocator serviceLocator;
/**
* The global workbench context service, if the workbench is available.
* Retrieved in the constructor.
*/
private IContextService contextService = null;
/**
* The global workbench IFocusService, if the workbench is available.
* Retrieved in the constructor.
*/
private IFocusService focusService = null;
/**
* A Workbench UI context activation that is activated when starting inline
* editing through {@link #startEditing(TreeItem, int)}.
*
* @see #activateEditingContext()
* @see #deactivateEditingContext()
*/
private IContextActivation editingContext = null;
static class ImageTask {
NodeContext node;
TreeItem item;
Object[] descsOrImages;
public ImageTask(NodeContext node, TreeItem item, Object[] descsOrImages) {
this.node = node;
this.item = item;
this.descsOrImages = descsOrImages;
}
}
/**
* The job that is used for off-loading image loading tasks (see
* {@link ImageTask} to a worker thread from the main UI thread.
*
* @see #setPendingImages(IProgressMonitor)
*/
ImageLoaderJob imageLoaderJob;
/**
* The set of currently gathered up image loading tasks for
* {@link #imageLoaderJob} to execute.
*
* @see #setPendingImages(IProgressMonitor)
*/
Map imageTasks = new THashMap();
/**
* A state flag indicating whether the vertical scroll bar was visible for
* {@link #tree} the last time it was checked. Since there is no listener
* that can provide this information, we check it in {@link #setData(Event)}
* every time any data for a TreeItem is updated. If the visibility changes,
* we will force re-layouting of the tree's parent composite.
*
* @see #setData(Event)
*/
private boolean verticalBarVisible = false;
static class TransientStateImpl implements TransientExplorerState {
private Integer activeColumn = null;
@Override
public synchronized Integer getActiveColumn() {
return activeColumn;
}
public synchronized void setActiveColumn(Integer column) {
activeColumn = column;
}
}
private TransientStateImpl transientState = new TransientStateImpl();
boolean scheduleUpdater() {
if (tree.isDisposed())
return false;
if (pendingRoot == true || !pendingItems.isEmpty()) {
assert(!tree.isDisposed());
int activity = explorerContext.activityInt;
long delay = 30;
if (activity < 100) {
// System.out.println("Scheduling update immediately.");
} else if (activity < 1000) {
// System.out.println("Scheduling update after 500ms.");
delay = 500;
} else {
// System.out.println("Scheduling update after 3000ms.");
delay = 3000;
}
updateCounter = 0;
//System.out.println("Scheduling UI update after " + delay + " ms.");
uiUpdateScheduler.schedule(new Runnable() {
@Override
public void run() {
if (tree.isDisposed())
return;
if (updateCounter > 0) {
updateCounter = 0;
uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
} else {
tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
}
}
}, delay, TimeUnit.MILLISECONDS);
updating = true;
return true;
}
return false;
}
int updateCounter = 0;
void update(TreeItem item) {
synchronized(pendingItems) {
// System.out.println("update " + item);
updateCounter++;
if(item == null) pendingRoot = true;
else pendingItems.add(item);
if(updating == true) return;
scheduleUpdater();
}
}
private int maxChildren = DEFAULT_MAX_CHILDREN;
@Override
public int getMaxChildren() {
return maxChildren;
}
@Override
public int getMaxChildren(NodeQueryManager manager, NodeContext context) {
Integer result = manager.query(context, BuiltinKeys.SHOW_MAX_CHILDREN);
//System.out.println("getMaxChildren(" + manager + ", " + context + "): " + result);
if (result != null) {
if (result < 0)
throw new AssertionError("BuiltinKeys.SHOW_MAX_CHILDREN query must never return < 0, got " + result);
return result;
}
return maxChildren;
}
@Override
public void setMaxChildren(int maxChildren) {
this.maxChildren = maxChildren;
}
@Override
public void setModificationContext(@SuppressWarnings("deprecation") ModificationContext modificationContext) {
this.modificationContext = modificationContext;
}
/**
* @param parent the parent SWT composite
*/
public GraphExplorerImpl(Composite parent) {
this(parent, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
}
/**
* Stores the node context and the modifier that is currently being
* modified. These are used internally to prevent duplicate edits from being
* initiated which should always be a sensible thing to do.
*/
private Set currentlyModifiedNodes = new THashSet();
private final TreeEditor editor;
private Color invalidModificationColor = null;
/**
* @param item the TreeItem to start editing
* @param columnIndex the index of the column to edit, starts counting from
* 0
* @return true
if the editing was initiated successfully or
* false
if editing could not be started due to lack of
* {@link Modifier} for the labeler in question.
*/
private String startEditing(final TreeItem item, final int columnIndex, String columnKey) {
if (!editable)
return "Rename not supported for selection";
GENodeQueryManager manager = new GENodeQueryManager(this.explorerContext, null, null, TreeItemReference.create(item.getParentItem()));
final NodeContext context = (NodeContext) item.getData();
Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER);
if (labeler == null)
return "Rename not supported for selection";
if(columnKey == null) columnKey = columns[columnIndex].getKey();
// columnKey might be prefixed with '#' to indicate
// textual editing is preferred. Try to get modifier
// for that first and only if it fails, try without
// the '#' prefix.
Modifier modifier = labeler.getModifier(modificationContext, columnKey);
if (modifier == null) {
if(columnKey.startsWith("#"))
modifier = labeler.getModifier(modificationContext, columnKey.substring(1));
if (modifier == null)
return "Rename not supported for selection";
}
if (modifier instanceof DeniedModifier) {
DeniedModifier dm = (DeniedModifier)modifier;
return dm.getMessage();
}
// Prevent editing of a single node context multiple times.
if (currentlyModifiedNodes.contains(context)) {
//System.out.println("discarding duplicate edit for context " + context);
return "Rename not supported for selection";
}
// Clean up any previous editor control
Control oldEditor = editor.getEditor();
if (oldEditor != null)
oldEditor.dispose();
if (modifier instanceof DialogModifier) {
performDialogEditing(item, columnIndex, context, (DialogModifier) modifier);
} else if (modifier instanceof CustomModifier) {
startCustomEditing(item, columnIndex, context, (CustomModifier) modifier);
} else if (modifier instanceof EnumerationModifier) {
startEnumerationEditing(item, columnIndex, context, (EnumerationModifier) modifier);
} else {
startTextEditing(item, columnIndex, context, modifier);
}
return null;
}
/**
* @param item
* @param columnIndex
* @param context
* @param modifier
*/
void performDialogEditing(final TreeItem item, final int columnIndex, final NodeContext context,
final DialogModifier modifier) {
final AtomicBoolean disposed = new AtomicBoolean(false);
Consumer callback = result -> {
if (disposed.get())
return;
String error = modifier.isValid(result);
if (error == null) {
modifier.modify(result);
// Item may be disposed if the tree gets reset after a previous editing.
if (!item.isDisposed()) {
item.setText(columnIndex, result);
queueSelectionRefresh(context);
}
}
};
currentlyModifiedNodes.add(context);
try {
String status = modifier.query(tree, item, columnIndex, context, callback);
if (status != null)
ErrorLogger.defaultLog( new Status(IStatus.INFO, Activator.PLUGIN_ID, status) );
} finally {
currentlyModifiedNodes.remove(context);
disposed.set(true);
}
}
private void reconfigureTreeEditor(TreeItem item, int columnIndex, Control control, int widthHint, int heightHint, int insetX, int insetY) {
Point size = control.computeSize(widthHint, heightHint);
editor.horizontalAlignment = SWT.LEFT;
Rectangle itemRect = item.getBounds(columnIndex),
rect = tree.getClientArea();
editor.minimumWidth = Math.max(size.x, itemRect.width) + insetX * 2;
int left = itemRect.x,
right = rect.x + rect.width;
editor.minimumWidth = Math.min(editor.minimumWidth, right - left);
editor.minimumHeight = size.y + insetY * 2;
editor.layout();
}
void reconfigureTreeEditorForText(TreeItem item, int columnIndex, Control control, String text, int heightHint, int insetX, int insetY) {
GC gc = new GC(control);
Point size = gc.textExtent(text);
gc.dispose();
reconfigureTreeEditor(item, columnIndex, control, size.x, SWT.DEFAULT, insetX, insetY);
}
/**
* @param item
* @param columnIndex
* @param context
* @param modifier
*/
void startCustomEditing(final TreeItem item, final int columnIndex, final NodeContext context,
final CustomModifier modifier) {
final Object obj = modifier.createControl(tree, item, columnIndex, context);
if (!(obj instanceof Control))
throw new UnsupportedOperationException("SWT control required, got " + obj + " from CustomModifier.createControl(Object)");
final Control control = (Control) obj;
// final int insetX = 0;
// final int insetY = 0;
// control.addListener(SWT.Resize, new Listener() {
// @Override
// public void handleEvent(Event e) {
// Rectangle rect = control.getBounds();
// control.setBounds(rect.x + insetX, rect.y + insetY, rect.width - insetX * 2, rect.height - insetY * 2);
// }
// });
control.addListener(SWT.Dispose, new Listener() {
@Override
public void handleEvent(Event event) {
currentlyModifiedNodes.remove(context);
queueSelectionRefresh(context);
deactivateEditingContext();
}
});
if (!(control instanceof Shell)) {
editor.setEditor(control, item, columnIndex);
}
control.setFocus();
GraphExplorerImpl.this.reconfigureTreeEditor(item, columnIndex, control, SWT.DEFAULT, SWT.DEFAULT, 0, 0);
activateEditingContext(control);
// Removed in disposeListener above
currentlyModifiedNodes.add(context);
//System.out.println("START CUSTOM EDITING: " + item);
}
/**
* @param item
* @param columnIndex
* @param context
* @param modifier
*/
void startEnumerationEditing(final TreeItem item, final int columnIndex, final NodeContext context, final EnumerationModifier modifier) {
String initialText = modifier.getValue();
if (initialText == null)
throw new AssertionError("Labeler.Modifier.getValue() returned null");
List values = modifier.getValues();
String selectedValue = modifier.getValue();
int selectedIndex = values.indexOf(selectedValue);
if (selectedIndex == -1)
throw new AssertionFailedException(modifier + " EnumerationModifier.getValue returned '" + selectedValue + "' which is not among the possible values returned by EnumerationModifier.getValues(): " + values);
final CCombo combo = new CCombo(tree, SWT.FLAT | SWT.BORDER | SWT.READ_ONLY | SWT.DROP_DOWN);
combo.setVisibleItemCount(10);
//combo.setEditable(false);
for (String value : values) {
combo.add(value);
}
combo.select(selectedIndex);
Listener comboListener = new Listener() {
boolean arrowTraverseUsed = false;
@Override
public void handleEvent(final Event e) {
//System.out.println("FOO: " + e);
switch (e.type) {
case SWT.KeyDown:
if (e.character == SWT.CR) {
// Commit edit directly on ENTER press.
String text = combo.getText();
modifier.modify(text);
// Item may be disposed if the tree gets reset after a previous editing.
if (!item.isDisposed()) {
item.setText(columnIndex, text);
queueSelectionRefresh(context);
}
combo.dispose();
e.doit = false;
} else if (e.keyCode == SWT.ESC) {
// Cancel editing immediately
combo.dispose();
e.doit = false;
}
break;
case SWT.Selection:
{
if (arrowTraverseUsed) {
arrowTraverseUsed = false;
return;
}
String text = combo.getText();
modifier.modify(text);
// Item may be disposed if the tree gets reset after a previous editing.
if (!item.isDisposed()) {
item.setText(columnIndex, text);
queueSelectionRefresh(context);
}
combo.dispose();
break;
}
case SWT.FocusOut: {
String text = combo.getText();
modifier.modify(text);
// Item may be disposed if the tree gets reset after a previous editing.
if (!item.isDisposed()) {
item.setText(columnIndex, text);
queueSelectionRefresh(context);
}
combo.dispose();
break;
}
case SWT.Traverse: {
switch (e.detail) {
case SWT.TRAVERSE_RETURN:
String text = combo.getText();
modifier.modify(text);
if (!item.isDisposed()) {
item.setText(columnIndex, text);
queueSelectionRefresh(context);
}
arrowTraverseUsed = false;
// FALL THROUGH
case SWT.TRAVERSE_ESCAPE:
combo.dispose();
e.doit = false;
break;
case SWT.TRAVERSE_ARROW_NEXT:
case SWT.TRAVERSE_ARROW_PREVIOUS:
arrowTraverseUsed = true;
break;
default:
//System.out.println("unhandled traversal: " + e.detail);
break;
}
break;
}
case SWT.Dispose:
currentlyModifiedNodes.remove(context);
deactivateEditingContext();
break;
}
}
};
combo.addListener(SWT.MouseWheel, VetoingEventHandler.INSTANCE);
combo.addListener(SWT.KeyDown, comboListener);
combo.addListener(SWT.FocusOut, comboListener);
combo.addListener(SWT.Traverse, comboListener);
combo.addListener(SWT.Selection, comboListener);
combo.addListener(SWT.Dispose, comboListener);
editor.setEditor(combo, item, columnIndex);
combo.setFocus();
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(BiFunction f) {
this.selectionTransformation = f;
}
@Override
public void addListener(T listener) {
if(listener instanceof FocusListener) {
focusListeners.add((FocusListener)listener);
} else if(listener instanceof MouseListener) {
mouseListeners.add((MouseListener)listener);
} else if(listener instanceof KeyListener) {
keyListeners.add((KeyListener)listener);
}
}
@Override
public void removeListener(T listener) {
if(listener instanceof FocusListener) {
focusListeners.remove(listener);
} else if(listener instanceof MouseListener) {
mouseListeners.remove(listener);
} else if(listener instanceof KeyListener) {
keyListeners.remove(listener);
}
}
public void addSelectionListener(SelectionListener listener) {
tree.addSelectionListener(listener);
}
public void removeSelectionListener(SelectionListener listener) {
tree.removeSelectionListener(listener);
}
private Set uiContexts;
@Override
public void setUIContexts(Set contexts) {
this.uiContexts = contexts;
}
@Override
public void setRoot(final Object root) {
if(uiContexts != null && uiContexts.size() == 1)
setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root, BuiltinKeys.UI_CONTEXT, uiContexts.iterator().next()));
else
setRootContext0(NodeContextBuilder.buildWithData(BuiltinKeys.INPUT, root));
}
@Override
public void setRootContext(final NodeContext context) {
setRootContext0(context);
}
private void setRootContext0(final NodeContext context) {
Assert.isNotNull(context, "root must not be null");
if (isDisposed() || tree.isDisposed())
return;
Display display = tree.getDisplay();
if (display.getThread() == Thread.currentThread()) {
doSetRoot(context);
} else {
display.asyncExec(new Runnable() {
@Override
public void run() {
doSetRoot(context);
}
});
}
}
private void initializeState() {
if (persistor == null)
return;
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.apply(this, objects);
}
protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
int len = contexts.length;
Object[] objects = new Object[len];
for (int i = 0; i < len; ++i)
objects[i] = filter.filter(contexts[i]);
return objects;
}
@Override
public void setExpanded(final NodeContext context, final boolean expanded) {
assertNotDisposed();
ThreadUtils.asyncExec(thread, new Runnable() {
@Override
public void run() {
if (!isDisposed())
doSetExpanded(context, expanded);
}
});
}
private void doSetExpanded(NodeContext context, boolean expanded) {
//System.out.println("doSetExpanded(" + context + ", " + expanded + ")");
TreeItem item = contextToItem.getRight(context);
if (item != null) {
item.setExpanded(expanded);
}
PrimitiveQueryProcessor> pqp = explorerContext.getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED);
if (pqp instanceof IsExpandedProcessor) {
IsExpandedProcessor iep = (IsExpandedProcessor) pqp;
iep.replaceExpanded(context, expanded);
}
}
@Override
public void setColumnsVisible(boolean visible) {
columnsAreVisible = visible;
if(tree != null) tree.setHeaderVisible(columnsAreVisible);
}
@Override
public void setColumns(final Column[] columns) {
setColumns(columns, null);
}
@Override
public void setColumns(final Column[] columns, Consumer> callback) {
assertNotDisposed();
checkUniqueColumnKeys(columns);
Display d = tree.getDisplay();
if (d.getThread() == Thread.currentThread())
doSetColumns(columns, callback);
else
d.asyncExec(() -> {
if (tree.isDisposed())
return;
doSetColumns(columns, callback);
});
}
private void checkUniqueColumnKeys(Column[] cols) {
Set usedColumnKeys = new HashSet();
List duplicateColumns = new ArrayList();
for (Column c : cols) {
if (!usedColumnKeys.add(c.getKey()))
duplicateColumns.add(c);
}
if (!duplicateColumns.isEmpty()) {
throw new IllegalArgumentException("All columns do not have unique keys: " + cols + ", overlapping: " + duplicateColumns);
}
}
/**
* Only meant to be invoked from the SWT UI thread.
*
* @param cols
*/
private void doSetColumns(Column[] cols, Consumer> callback) {
// Attempt to keep previous column widths.
Map prevWidths = new HashMap();
for (TreeColumn column : tree.getColumns()) {
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;
}
}