/******************************************************************************* * Copyright (c) 2007, 2012 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.browsing.ui.swt; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Consumer; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.AssertionFailedException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.action.IStatusLineManager; import org.eclipse.jface.resource.ColorDescriptor; import org.eclipse.jface.resource.DeviceResourceException; import org.eclipse.jface.resource.DeviceResourceManager; import org.eclipse.jface.resource.FontDescriptor; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.resource.ResourceManager; import org.eclipse.jface.viewers.IPostSelectionProvider; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.custom.CCombo; import org.eclipse.swt.custom.TreeEditor; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchSite; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.contexts.IContextActivation; import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.services.IServiceLocator; import org.eclipse.ui.swt.IFocusService; import org.simantics.browsing.ui.BuiltinKeys; import org.simantics.browsing.ui.CheckedState; import org.simantics.browsing.ui.Column; import org.simantics.browsing.ui.Column.Align; import org.simantics.browsing.ui.DataSource; import org.simantics.browsing.ui.ExplorerState; import org.simantics.browsing.ui.GraphExplorer; import org.simantics.browsing.ui.NodeContext; import org.simantics.browsing.ui.NodeContext.CacheKey; import org.simantics.browsing.ui.NodeContext.PrimitiveQueryKey; import org.simantics.browsing.ui.NodeContext.QueryKey; import org.simantics.browsing.ui.NodeContextPath; import org.simantics.browsing.ui.NodeQueryManager; import org.simantics.browsing.ui.NodeQueryProcessor; import org.simantics.browsing.ui.PrimitiveQueryProcessor; import org.simantics.browsing.ui.SelectionDataResolver; import org.simantics.browsing.ui.SelectionFilter; import org.simantics.browsing.ui.StatePersistor; import org.simantics.browsing.ui.common.AdaptableHintContext; import org.simantics.browsing.ui.common.ColumnKeys; import org.simantics.browsing.ui.common.ErrorLogger; import org.simantics.browsing.ui.common.NodeContextBuilder; import org.simantics.browsing.ui.common.NodeContextUtil; import org.simantics.browsing.ui.common.internal.GECache; import org.simantics.browsing.ui.common.internal.GENodeQueryManager; import org.simantics.browsing.ui.common.internal.IGECache; import org.simantics.browsing.ui.common.internal.IGraphExplorerContext; import org.simantics.browsing.ui.common.internal.UIElementReference; import org.simantics.browsing.ui.common.processors.DefaultCheckedStateProcessor; import org.simantics.browsing.ui.common.processors.DefaultComparableChildrenProcessor; import org.simantics.browsing.ui.common.processors.DefaultFinalChildrenProcessor; import org.simantics.browsing.ui.common.processors.DefaultImageDecoratorProcessor; import org.simantics.browsing.ui.common.processors.DefaultImagerFactoriesProcessor; import org.simantics.browsing.ui.common.processors.DefaultImagerProcessor; import org.simantics.browsing.ui.common.processors.DefaultLabelDecoratorProcessor; import org.simantics.browsing.ui.common.processors.DefaultLabelerFactoriesProcessor; import org.simantics.browsing.ui.common.processors.DefaultLabelerProcessor; import org.simantics.browsing.ui.common.processors.DefaultPrunedChildrenProcessor; import org.simantics.browsing.ui.common.processors.DefaultSelectedImageDecoratorFactoriesProcessor; import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelDecoratorFactoriesProcessor; import org.simantics.browsing.ui.common.processors.DefaultSelectedLabelerProcessor; import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointFactoryProcessor; import org.simantics.browsing.ui.common.processors.DefaultSelectedViewpointProcessor; import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionProcessor; import org.simantics.browsing.ui.common.processors.DefaultViewpointContributionsProcessor; import org.simantics.browsing.ui.common.processors.DefaultViewpointProcessor; import org.simantics.browsing.ui.common.processors.IsExpandedProcessor; import org.simantics.browsing.ui.common.processors.NoSelectionRequestProcessor; import org.simantics.browsing.ui.common.processors.ProcessorLifecycle; import org.simantics.browsing.ui.common.state.ExplorerStates; import org.simantics.browsing.ui.content.ImageDecorator; import org.simantics.browsing.ui.content.Imager; import org.simantics.browsing.ui.content.LabelDecorator; import org.simantics.browsing.ui.content.Labeler; import org.simantics.browsing.ui.content.Labeler.CustomModifier; import org.simantics.browsing.ui.content.Labeler.DeniedModifier; import org.simantics.browsing.ui.content.Labeler.DialogModifier; import org.simantics.browsing.ui.content.Labeler.EnumerationModifier; import org.simantics.browsing.ui.content.Labeler.FilteringModifier; import org.simantics.browsing.ui.content.Labeler.LabelerListener; import org.simantics.browsing.ui.content.Labeler.Modifier; import org.simantics.browsing.ui.content.PrunedChildrenResult; import org.simantics.browsing.ui.model.nodetypes.EntityNodeType; import org.simantics.browsing.ui.model.nodetypes.NodeType; import org.simantics.browsing.ui.swt.internal.Threads; import org.simantics.db.layer0.SelectionHints; import org.simantics.utils.ObjectUtils; import org.simantics.utils.datastructures.BijectionMap; import org.simantics.utils.datastructures.disposable.AbstractDisposable; import org.simantics.utils.datastructures.hints.IHintContext; import org.simantics.utils.threads.IThreadWorkQueue; import org.simantics.utils.threads.SWTThread; import org.simantics.utils.threads.ThreadUtils; import org.simantics.utils.ui.ISelectionUtils; import org.simantics.utils.ui.SWTUtils; import org.simantics.utils.ui.jface.BasePostSelectionProvider; import org.simantics.utils.ui.widgets.VetoingEventHandler; import org.simantics.utils.ui.workbench.WorkbenchUtils; import gnu.trove.map.hash.THashMap; import gnu.trove.procedure.TObjectProcedure; import gnu.trove.set.hash.THashSet; /** * @see #getMaxChildren() * @see #setMaxChildren(int) * @see #getMaxChildren(NodeQueryManager, NodeContext) */ class GraphExplorerImpl extends GraphExplorerImplBase implements Listener, GraphExplorer /*, IPostSelectionProvider*/ { private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider { private GraphExplorerImpl ge; GraphExplorerPostSelectionProvider(GraphExplorerImpl ge) { this.ge = ge; } void dispose() { ge = null; } @Override public void setSelection(final ISelection selection) { if(ge == null) return; ge.setSelection(selection, false); } @Override public void removeSelectionChangedListener(ISelectionChangedListener listener) { if(ge == null) return; if(ge.isDisposed()) { if (DEBUG_SELECTION_LISTENERS) System.out.println("GraphExplorerImpl is disposed in removeSelectionChangedListener: " + listener); return; } //assertNotDisposed(); //System.out.println("Remove selection changed listener: " + listener); ge.selectionProvider.removeSelectionChangedListener(listener); } @Override public void addPostSelectionChangedListener(ISelectionChangedListener listener) { if(ge == null) return; if (!ge.thread.currentThreadAccess()) throw new AssertionError(getClass().getSimpleName() + ".addPostSelectionChangedListener called from non SWT-thread: " + Thread.currentThread()); if(ge.isDisposed()) { System.out.println("Client BUG: GraphExplorerImpl is disposed in addPostSelectionChangedListener: " + listener); return; } //System.out.println("Add POST selection changed listener: " + listener); ge.selectionProvider.addPostSelectionChangedListener(listener); } @Override public void removePostSelectionChangedListener(ISelectionChangedListener listener) { if(ge == null) return; if(ge.isDisposed()) { if (DEBUG_SELECTION_LISTENERS) System.out.println("GraphExplorerImpl is disposed in removePostSelectionChangedListener: " + listener); return; } // assertNotDisposed(); //System.out.println("Remove POST selection changed listener: " + listener); ge.selectionProvider.removePostSelectionChangedListener(listener); } @Override public void addSelectionChangedListener(ISelectionChangedListener listener) { if(ge == null) return; if (!ge.thread.currentThreadAccess()) throw new AssertionError(getClass().getSimpleName() + ".addSelectionChangedListener called from non SWT-thread: " + Thread.currentThread()); //System.out.println("Add selection changed listener: " + listener); if (ge.tree.isDisposed() || ge.selectionProvider == null) { System.out.println("Client BUG: GraphExplorerImpl is disposed in addSelectionChangedListener: " + listener); return; } ge.selectionProvider.addSelectionChangedListener(listener); } @Override public ISelection getSelection() { if(ge == null) return StructuredSelection.EMPTY; if (!ge.thread.currentThreadAccess()) throw new AssertionError(getClass().getSimpleName() + ".getSelection called from non SWT-thread: " + Thread.currentThread()); if (ge.tree.isDisposed() || ge.selectionProvider == null) return StructuredSelection.EMPTY; return ge.selectionProvider.getSelection(); } } /** * If this explorer is running with an Eclipse workbench open, this * Workbench UI context will be activated whenever inline editing is started * through {@link #startEditing(TreeItem, int)} and deactivated when inline * editing finishes. * * This context information can be used to for UI handler activity testing. */ private static final String INLINE_EDITING_UI_CONTEXT = "org.simantics.browsing.ui.inlineEditing"; private static final String KEY_DRAG_COLUMN = "dragColumn"; private static final boolean DEBUG_SELECTION_LISTENERS = false; private static final int DEFAULT_CONSECUTIVE_LABEL_REFRESH_DELAY = 200; public static final int DEFAULT_MAX_CHILDREN = 1000; private static final long POST_SELECTION_DELAY = 300; /** * The time in milliseconds that must elapse between consecutive * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)} * invocations in order for this class to construct a new selection. * *

* This is done because selection construction can be very expensive as the * selected set grows larger when the user is pressing shift+arrow keys. * GraphExplorerImpl will naturally listen to all changes in the tree * selection, but as an optimization will not construct new * StructuredSelection instances for every selection change event. A new * selection will be constructed and set only if the selection hasn't * changed for the amount of milliseconds specified by this constant. */ private static final long SELECTION_CHANGE_QUIET_TIME = 150; private final IThreadWorkQueue thread; /** * Local method for checking from whether resources are loaded in * JFaceResources. */ private final LocalResourceManager localResourceManager; /** * Local device resource manager that is safe to use in * {@link ImageLoaderJob} for creating images in a non-UI thread. */ private final ResourceManager resourceManager; /* * Package visibility. * TODO: Get rid of these. */ Tree tree; @SuppressWarnings({ "rawtypes" }) final HashMap, NodeQueryProcessor> processors = new HashMap, NodeQueryProcessor>(); @SuppressWarnings({ "rawtypes" }) final HashMap primitiveProcessors = new HashMap(); @SuppressWarnings({ "rawtypes" }) final HashMap dataSources = new HashMap(); class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext { // This is for query debugging only. int queryIndent = 0; GECache cache = new GECache(); AtomicBoolean propagating = new AtomicBoolean(false); Object propagateList = new Object(); Object propagate = new Object(); List scheduleList = new ArrayList(); final Deque activity = new LinkedList(); int activityInt = 0; /** * Stores the currently running query update runnable. If * null there's nothing scheduled yet in which case * scheduling can commence. Otherwise the update should be skipped. */ AtomicReference currentQueryUpdater = new AtomicReference(); /** * Keeps track of nodes that have already been auto-expanded. After * being inserted into this set, nodes will not be forced to stay in an * expanded state after that. This makes it possible for the user to * close auto-expanded nodes. */ Map autoExpanded = new WeakHashMap(); @Override protected void doDispose() { saveState(); autoExpanded.clear(); } @Override public IGECache getCache() { return cache; } @Override public int queryIndent() { return queryIndent; } @Override public int queryIndent(int offset) { queryIndent += offset; return queryIndent; } @Override @SuppressWarnings("unchecked") public NodeQueryProcessor getProcessor(Object o) { return processors.get(o); } @Override @SuppressWarnings("unchecked") public PrimitiveQueryProcessor getPrimitiveProcessor(Object o) { return primitiveProcessors.get(o); } @SuppressWarnings("unchecked") @Override public DataSource getDataSource(Class clazz) { return dataSources.get(clazz); } @Override public void update(UIElementReference ref) { //System.out.println("GE.update " + ref); TreeItemReference tiref = (TreeItemReference) ref; TreeItem item = tiref.getItem(); // NOTE: must be called regardless of the the item value. // A null item is currently used to indicate a tree root update. GraphExplorerImpl.this.update(item); } @Override public Object getPropagateLock() { return propagate; } @Override public Object getPropagateListLock() { return propagateList; } @Override public boolean isPropagating() { return propagating.get(); } @Override public void setPropagating(boolean b) { this.propagating.set(b); } @Override public List getScheduleList() { return scheduleList; } @Override public void setScheduleList(List list) { this.scheduleList = list; } @Override public Deque getActivity() { return activity; } @Override public void setActivityInt(int i) { this.activityInt = i; } @Override public int getActivityInt() { return activityInt; } @Override public void scheduleQueryUpdate(Runnable r) { if (GraphExplorerImpl.this.isDisposed() || queryUpdateScheduler.isShutdown()) return; //System.out.println("Scheduling query update for runnable " + r); if (currentQueryUpdater.compareAndSet(null, r)) { //System.out.println("Scheduling query update for runnable " + r); queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER); } } Runnable QUERY_UPDATE_SCHEDULER = new Runnable() { @Override public void run() { Runnable r = currentQueryUpdater.getAndSet(null); if (r != null) { //System.out.println("Running query update runnable " + r); r.run(); } } }; } GraphExplorerContext explorerContext = new GraphExplorerContext(); HashSet pendingItems = new HashSet(); boolean updating = false; boolean pendingRoot = false; @SuppressWarnings("deprecation") ModificationContext modificationContext = null; NodeContext rootContext; StatePersistor persistor = null; boolean editable = true; /** * This is a reverse mapping from {@link NodeContext} tree objects back to * their owner TreeItems. * *

* Access this map only in the SWT thread to keep it thread-safe. *

*/ BijectionMap contextToItem = new BijectionMap(); /** * Columns of the UI viewer. Use {@link #setColumns(Column[])} to * initialize. */ Column[] columns = new Column[0]; Map columnKeyToIndex = new HashMap(); boolean refreshingColumnSizes = false; boolean columnsAreVisible = true; /** * An array reused for invoking {@link TreeItem#setImage(Image[])} instead * of constantly allocating new arrays for setting each TreeItems images. * This works because {@link TreeItem#setImage(Image[])} does not take hold * of the array itself, only the contents of the array. * * @see #setImage(NodeContext, TreeItem, Imager, Collection, int) */ Image[] columnImageArray = { null }; /** * Used for collecting Image or ImageDescriptor instances for a single * TreeItem when initially setting images for a TreeItem. * * @see #setImage(NodeContext, TreeItem, Imager, Collection, int) */ Object[] columnDescOrImageArray = { null }; final ExecutorService queryUpdateScheduler = Threads.getExecutor(); final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor(); /** Set to true when the Tree widget is disposed. */ private boolean disposed = false; private final CopyOnWriteArrayList focusListeners = new CopyOnWriteArrayList(); private final CopyOnWriteArrayList mouseListeners = new CopyOnWriteArrayList(); private final CopyOnWriteArrayList keyListeners = new CopyOnWriteArrayList(); /** Selection provider */ private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this); protected BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider(); protected SelectionDataResolver selectionDataResolver; protected SelectionFilter selectionFilter; protected BiFunction selectionTransformation = new BiFunction() { @Override public Object[] apply(GraphExplorer explorer, Object[] objects) { Object[] result = new Object[objects.length]; for (int i = 0; i < objects.length; i++) { IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN); context.setHint(SelectionHints.KEY_MAIN, objects[i]); result[i] = context; } return result; } }; protected FontDescriptor originalFont; protected ColorDescriptor originalForeground; protected ColorDescriptor originalBackground; /** * The set of currently selected TreeItem instances. This set is needed * because we need to know in {@link #setData(Event)} whether the updated * item was a part of the current selection in which case the selection must * be updated. */ private final Map selectedItems = new HashMap(); /** * TODO: specify what this is for */ private final Set selectionRefreshContexts = new HashSet(); /** * If this field is non-null, it means that if {@link #setData(Event)} * encounters a NodeContext equal to this one, it must make the TreeItem * assigned to that NodeContext the topmost item of the tree using * {@link Tree#setTopItem(TreeItem)}. After this the field value is * nullified. * *

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

* Must be invoked during right after construction. * * @param serviceLocator * a specific service locator or null to use the * workbench global service locator */ public void setServiceLocator(IServiceLocator serviceLocator) { if (serviceLocator == null && PlatformUI.isWorkbenchRunning()) serviceLocator = PlatformUI.getWorkbench(); this.serviceLocator = serviceLocator; if (serviceLocator != null) { this.contextService = (IContextService) serviceLocator.getService(IContextService.class); this.focusService = (IFocusService) serviceLocator.getService(IFocusService.class); } } @Override public Object getClicked(Object event) { MouseEvent e = (MouseEvent)event; final Tree tree = (Tree) e.getSource(); Point point = new Point(e.x, e.y); TreeItem item = tree.getItem(point); // No selectable item at point? if (item == null) return null; Object data = item.getData(); return data; } }