/******************************************************************************* * Copyright (c) 2013 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.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.ScheduledExecutorService; import java.util.concurrent.Semaphore; 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.IAdaptable; 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.layout.GridDataFactory; import org.eclipse.jface.layout.TreeColumnLayout; 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.viewers.CellEditor; import org.eclipse.jface.viewers.CellLabelProvider; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent; import org.eclipse.jface.viewers.ColumnViewerEditorActivationListener; import org.eclipse.jface.viewers.ColumnViewerEditorDeactivationEvent; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.ComboBoxCellEditor; import org.eclipse.jface.viewers.DialogCellEditor; import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.ICellEditorValidator; 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.ITreeContentProvider; import org.eclipse.jface.viewers.ITreeViewerListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TextCellEditor; import org.eclipse.jface.viewers.TreeExpansionEvent; import org.eclipse.jface.viewers.TreeSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; 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.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.layout.FillLayout; 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.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; 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.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.NodeQueryManager; import org.simantics.browsing.ui.NodeQueryProcessor; import org.simantics.browsing.ui.PrimitiveQueryProcessor; import org.simantics.browsing.ui.PrimitiveQueryUpdater; 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.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.AbstractPrimitiveQueryProcessor; 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.DialogModifier; import org.simantics.browsing.ui.content.Labeler.EnumerationModifier; import org.simantics.browsing.ui.content.Labeler.Modifier; import org.simantics.browsing.ui.swt.internal.Threads; import org.simantics.db.layer0.SelectionHints; import org.simantics.ui.SimanticsUI; import org.simantics.utils.datastructures.BijectionMap; import org.simantics.utils.datastructures.MapList; 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.AdaptionUtils; import org.simantics.utils.ui.ISelectionUtils; import org.simantics.utils.ui.SWTUtils; import org.simantics.utils.ui.jface.BasePostSelectionProvider; import gnu.trove.map.hash.THashMap; import gnu.trove.map.hash.TObjectIntHashMap; /** * TreeView based GraphExplorer * * * @author Marko Luukkainen */ public class GraphExplorerImpl2 extends GraphExplorerImplBase implements GraphExplorer { private static final boolean DEBUG_SELECTION_LISTENERS = false; private static final boolean DEBUG = false; private TreeViewer viewer; private LocalResourceManager localResourceManager; private DeviceResourceManager resourceManager; private IThreadWorkQueue thread; @SuppressWarnings({ "rawtypes" }) final HashMap, NodeQueryProcessor> processors = new HashMap, NodeQueryProcessor>(); @SuppressWarnings({ "rawtypes" }) final HashMap primitiveProcessors = new HashMap(); @SuppressWarnings({ "rawtypes" }) final HashMap dataSources = new HashMap(); private FontDescriptor originalFont; protected ColorDescriptor originalForeground; protected ColorDescriptor originalBackground; private Color invalidModificationColor; private Column[] columns; private Map columnKeyToIndex; private boolean columnsAreVisible = true; private NodeContext rootContext; private TreeNode rootNode; private StatePersistor persistor = null; private boolean editable = true; private boolean disposed = false; private final CopyOnWriteArrayList focusListeners = new CopyOnWriteArrayList(); private final CopyOnWriteArrayList mouseListeners = new CopyOnWriteArrayList(); private final CopyOnWriteArrayList keyListeners = new CopyOnWriteArrayList(); private int autoExpandLevel = 0; private IServiceLocator serviceLocator; private IContextService contextService = null; private IFocusService focusService = null; private IContextActivation editingContext = null; GeViewerContext explorerContext = new GeViewerContext(this); private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this); private BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider(); private SelectionDataResolver selectionDataResolver; private SelectionFilter selectionFilter; private Set collapsedNodes = new HashSet(); private MapList contextToNodeMap = new MapList(); private ModificationContext modificationContext = null; private boolean filterSelectionEdit = true; private TreeColumnLayout treeColumnLayout; private boolean expand; private boolean verticalBarVisible = false; private 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; } }; 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(); public GraphExplorerImpl2(Composite parent) { this(parent, SWT.BORDER | SWT.MULTI ); } public GraphExplorerImpl2(Composite parent, int style) { 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); boolean useLayout = true; // FIXME: hack, GraphExplorerComposite uses its own TreeColumnLayout. if (useLayout && !(parent.getLayout() instanceof TreeColumnLayout)) { Composite rootTreeComposite = new Composite(parent, SWT.NONE); treeColumnLayout = new TreeColumnLayout(); rootTreeComposite.setLayout(treeColumnLayout); viewer = new TreeViewer(rootTreeComposite,style|SWT.H_SCROLL|SWT.V_SCROLL); GridDataFactory.fillDefaults().grab(true, true).span(3,1).applyTo(rootTreeComposite); } else { viewer = new TreeViewer(parent,style | SWT.H_SCROLL | SWT.V_SCROLL); } viewer.getColumnViewerEditor().addEditorActivationListener(new ColumnViewerEditorActivationListener() { @Override public void beforeEditorDeactivated(ColumnViewerEditorDeactivationEvent event) { } @Override public void beforeEditorActivated(ColumnViewerEditorActivationEvent event) { // cancel editor activation for double click events. // TODO: this may not work similarly to GraphExplorerImpl if ((event.time - focusGainedAt) < 250L) { event.cancel = true; } } @Override public void afterEditorDeactivated(ColumnViewerEditorDeactivationEvent event) { } @Override public void afterEditorActivated(ColumnViewerEditorActivationEvent event) { } }); viewer.setUseHashlookup(true); viewer.setContentProvider(new GeViewerContentProvider()); originalFont = JFaceResources.getDefaultFontDescriptor(); viewer.getTree().setFont((Font) localResourceManager.get(originalFont)); setBasicListeners(); setDefaultProcessors(); viewer.getTree().addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { doDispose(); } }); // Add listener to tree for delayed tree population. Listener listener = new Listener() { @Override public void handleEvent(Event event) { switch (event.type) { case SWT.Activate: case SWT.Show: case SWT.Paint: { visible = true; activate(); break; } case SWT.Deactivate: case SWT.Hide: visible = false; } } }; viewer.getTree().addListener(SWT.Activate, listener); viewer.getTree().addListener(SWT.Deactivate, listener); viewer.getTree().addListener(SWT.Show, listener); viewer.getTree().addListener(SWT.Hide, listener); viewer.getTree().addListener(SWT.Paint,listener); viewer.addTreeListener(new ITreeViewerListener() { @Override public void treeExpanded(TreeExpansionEvent event) { } @Override public void treeCollapsed(TreeExpansionEvent event) { collapsedNodes.add((TreeNode)event.getElement()); } }); setColumns( new Column[] { new Column(ColumnKeys.SINGLE) }); } private long focusGainedAt = 0L; private boolean visible = false; private Collection selectedNodes = new ArrayList(); protected void setBasicListeners() { Tree tree = viewer.getTree(); tree.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { focusGainedAt = ((long) e.time) & 0xFFFFFFFFL; for (FocusListener listener : focusListeners) listener.focusGained(e); } @Override public void focusLost(FocusEvent e) { for (FocusListener listener : focusListeners) listener.focusLost(e); } }); tree.addMouseListener(new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) { for (MouseListener listener : mouseListeners) { listener.mouseDoubleClick(e); } } @Override public void mouseDown(MouseEvent e) { for (MouseListener listener : mouseListeners) { listener.mouseDown(e); } } @Override public void mouseUp(MouseEvent e) { for (MouseListener listener : mouseListeners) { listener.mouseUp(e); } } }); tree.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { for (KeyListener listener : keyListeners) { listener.keyPressed(e); } } @Override public void keyReleased(KeyEvent e) { for (KeyListener listener : keyListeners) { listener.keyReleased(e); } } }); viewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { //System.out.println("GraphExplorerImpl2.fireSelection"); selectedNodes = AdaptionUtils.adaptToCollection(event.getSelection(), TreeNode.class); Collection selectedContexts = AdaptionUtils.adaptToCollection(event.getSelection(), NodeContext.class); selectionProvider.setAndFireSelection(constructSelection(selectedContexts.toArray(new NodeContext[selectedContexts.size()]))); } }); viewer.addPostSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { //System.out.println("GraphExplorerImpl2.firePostSelection"); Collection selectedContexts = AdaptionUtils.adaptToCollection(event.getSelection(), NodeContext.class); selectionProvider.firePostSelection(constructSelection(selectedContexts.toArray(new NodeContext[selectedContexts.size()]))); } }); } private NodeContext pendingRoot; private void activate() { if (pendingRoot != null && !expand) { doSetRoot(pendingRoot); pendingRoot = null; } } /** * Invoke only from SWT thread to reset the root of the graph explorer tree. * * @param root */ private void doSetRoot(NodeContext root) { Display display = viewer.getTree().getDisplay(); if (display.getThread() != Thread.currentThread()) { throw new RuntimeException("Invoke from SWT thread only"); } // System.out.println("doSetRoot " + root); if (isDisposed()) return; if (viewer.getTree().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. if (rootNode != null) { rootNode.dispose(); } GeViewerContext oldContext = explorerContext; GeViewerContext newContext = new GeViewerContext(this); this.explorerContext = newContext; oldContext.safeDispose(); // Need to empty these or otherwise they won't be emptied until the // explorer is disposed which would mean that many unwanted references // will be held by this map. clearPrimitiveProcessors(); this.rootContext = root.getConstant(BuiltinKeys.IS_ROOT) != null ? root : NodeContextUtil.withConstant(root, BuiltinKeys.IS_ROOT, Boolean.TRUE); explorerContext.getCache().incRef(this.rootContext); initializeState(); select(rootContext); //refreshColumnSizes(); rootNode = new TreeNode(rootContext); if (DEBUG) System.out.println("setRoot " + rootNode); viewer.setInput(rootNode); // Delay content reading. // This is required for cases when GEImpl2 is attached to selection view. Reading content // instantly could stagnate SWT thread under rapid changes in selection. By delaying the // content reading we give the system a change to dispose the GEImpl2 before the content is read. display.asyncExec(new Runnable() { @Override public void run() { if (rootNode != null) { rootNode.updateChildren(); } } }); } private void initializeState() { if (persistor == null) return; ExplorerStates.scheduleRead(getRoot(), persistor) .thenAccept(state -> SWTUtils.asyncExec(viewer.getTree(), () -> restoreState(state))); } private void restoreState(ExplorerState state) { Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED); if (processor instanceof DefaultIsExpandedProcessor) { DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor; for(NodeContext expanded : state.expandedNodes) { isExpandedProcessor.replaceExpanded(expanded, true); } } } @Override public NodeContext getRoot() { return rootContext; } @Override public IThreadWorkQueue getThread() { return thread; } @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()); List nodes = contextToNodeMap.getValuesUnsafe(context); for (int i = 0; i < nodes.size(); i++) { if (nodes.get(i).getParent() != null) return nodes.get(i).getParent().getContext(); } return null; } @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; } 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 TreeNodeIsExpandedProcessor()); setPrimitiveProcessor(new DefaultShowMaxChildrenProcessor()); } @Override public Column[] getColumns() { return Arrays.copyOf(columns, columns.length); } @Override public void setColumnsVisible(boolean visible) { columnsAreVisible = visible; if(viewer.getTree() != null) viewer.getTree().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 = viewer.getTree().getDisplay(); if (d.getThread() == Thread.currentThread()) { doSetColumns(columns, callback); viewer.refresh(true); }else d.asyncExec(new Runnable() { @Override public void run() { if (viewer == null) return; if (viewer.getTree().isDisposed()) return; doSetColumns(columns, callback); viewer.refresh(true); viewer.getTree().getParent().layout(); } }); } 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); } } private List treeViewerColumns = new ArrayList(); private CellLabelProvider cellLabelProvider = new GeViewerLabelProvider(); private List editingSupports = new ArrayList(); private void doSetColumns(Column[] cols, Consumer> callback) { // Attempt to keep previous column widths. Map prevWidths = new HashMap<>(); for (TreeViewerColumn c : treeViewerColumns) { Column col = (Column) (c.getColumn().getData()); if (col != null) prevWidths.put(col.getKey(), c.getColumn().getWidth()); c.getColumn().dispose(); } treeViewerColumns.clear(); 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; Map map = new HashMap(); // FIXME : temporary workaround for ModelBrowser. viewer.getTree().setHeaderVisible(columns.length == 1 ? false : columnsAreVisible); int columnIndex = 0; for (Column column : columns) { TreeViewerColumn tvc = new TreeViewerColumn(viewer, toSWT(column.getAlignment())); treeViewerColumns.add(tvc); tvc.setLabelProvider(cellLabelProvider); EditingSupport support = null; if (editingSupports.size() > columnIndex) support = editingSupports.get(columnIndex); else { support = new GeEditingSupport(viewer, columnIndex); editingSupports.add(support); } tvc.setEditingSupport(support); TreeColumn c = tvc.getColumn(); 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 if (columns.length == 1) { // FIXME : how to handle single column properly? c.setWidth(1000); } else { // Go for some kind of default settings then... if (ColumnKeys.PROPERTY.equals(column.getKey())) c.setWidth(150); else c.setWidth(50); } if (treeColumnLayout != null) { treeColumnLayout.setColumnData(c, new ColumnWeightData(column.getWeight(), true)); } // if (!column.hasGrab() && !FILLER.equals(column.getKey())) { // c.addListener(SWT.Resize, resizeListener); // c.setResizable(true); // } else { // //c.setResizable(false); // } columnIndex++; } if(callback != null) callback.accept(map); } 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 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? } 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 setSelectionTransformation( BiFunction f) { this.selectionTransformation = f; } public ISelection getWidgetSelection() { return viewer.getSelection(); } @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) { viewer.getTree().addSelectionListener(listener); } public void removeSelectionListener(SelectionListener listener) { viewer.getTree().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 setRoot(NodeContext context) { if (!visible) { pendingRoot = context; Display.getDefault().asyncExec(new Runnable() { @Override public void run() { if (viewer != null && !viewer.getTree().isDisposed()) viewer.getTree().redraw(); } }); return; } doSetRoot(context); } private void setRootContext0(final NodeContext context) { Assert.isNotNull(context, "root must not be null"); if (isDisposed() || viewer.getTree().isDisposed()) return; Display display = viewer.getTree().getDisplay(); if (display.getThread() == Thread.currentThread()) { setRoot(context); } else { display.asyncExec(new Runnable() { @Override public void run() { setRoot(context); } }); } } @Override public void setFocus() { viewer.getTree().setFocus(); } @SuppressWarnings("unchecked") @Override public T getControl() { return (T)viewer.getTree(); } @Override public boolean isDisposed() { return disposed; } protected void assertNotDisposed() { if (isDisposed()) throw new IllegalStateException("disposed"); } @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 = viewer.getTree().getDisplay(); viewer.getTree().setBackground(editable ? null : display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); } private void doDispose() { if (disposed) return; disposed = true; // TODO: Since GENodeQueryManager is cached in QueryChache and it refers to this class // we have to remove all references here to reduce memory consumption. // // Proper fix would be to remove references between QueryCache and GENodeQueryManagers. explorerContext.dispose(); explorerContext = null; processors.clear(); detachPrimitiveProcessors(); primitiveProcessors.clear(); dataSources.clear(); pendingItems.clear(); rootContext = null; mouseListeners.clear(); selectionProvider.clearListeners(); selectionProvider = null; selectionDataResolver = null; selectedNodes.clear(); selectedNodes = null; selectionTransformation = null; originalFont = null; localResourceManager.dispose(); localResourceManager = null; // Must shutdown image loader job before disposing its ResourceManager imageLoaderJob.dispose(); imageLoaderJob.cancel(); try { imageLoaderJob.join(); imageLoaderJob = null; } catch (InterruptedException e) { ErrorLogger.defaultLogError(e); } resourceManager.dispose(); resourceManager = null; collapsedNodes.clear(); collapsedNodes = null; if (rootNode != null) { rootNode.dispose(); rootNode = null; } contextToNodeMap.clear(); // should be empty at this point. contextToNodeMap = null; if (postSelectionProvider != null) { postSelectionProvider.dispose(); postSelectionProvider = null; } imageTasks = null; modificationContext = null; focusService = null; contextService = null; serviceLocator = null; columns = null; columnKeyToIndex.clear(); columnKeyToIndex = null; viewer = null; } @Override public boolean select(NodeContext context) { assertNotDisposed(); if (context == null || context.equals(rootContext) || contextToNodeMap.getValuesUnsafe(context).size() == 0) { viewer.setSelection(new StructuredSelection()); selectionProvider.setAndFireNonEqualSelection(TreeSelection.EMPTY); return true; } viewer.setSelection(new StructuredSelection(contextToNodeMap.getValuesUnsafe(context).get(0))); return false; } @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); } private boolean selectPathInternal(NodeContext[] contexts, int position) { NodeContext head = contexts[position]; if(position == contexts.length-1) { return select(head); } setExpanded(head, true); if(!waitVisible(contexts[position+1])) return false; return selectPathInternal(contexts, position+1); } private boolean waitVisible(NodeContext context) { long start = System.nanoTime(); while(!isVisible(context)) { Display.getCurrent().readAndDispatch(); long duration = System.nanoTime() - start; if(duration > 10e9) return false; } return true; } @Override public boolean isVisible(NodeContext context) { if (contextToNodeMap.getValuesUnsafe(context).size() == 0) return false; Object elements[] = viewer.getVisibleExpandedElements(); return org.simantics.utils.datastructures.Arrays.contains(elements, contextToNodeMap.getValuesUnsafe(context).get(0)); } @Override public TransientExplorerState getTransientState() { if (!thread.currentThreadAccess()) throw new AssertionError(getClass().getSimpleName() + ".getActiveColumn called from non SWT-thread: " + Thread.currentThread()); return transientState; } @Override public T query(NodeContext context, CacheKey key) { return this.explorerContext.cache.get(context, key); } /** * 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); } } 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(); } } } @Override public void setExpanded(NodeContext context, boolean expanded) { viewer.setExpandedState(context, expanded); } @Override public void setAutoExpandLevel(int level) { this.autoExpandLevel = level; viewer.setAutoExpandLevel(level); } int maxChildren = GraphExplorerImpl.DEFAULT_MAX_CHILDREN; @Override public int getMaxChildren() { return maxChildren; } @Override public void setMaxChildren(int maxChildren) { this.maxChildren = 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 NodeQueryProcessor getProcessor(QueryKey key) { return explorerContext.getProcessor(key); } @Override public PrimitiveQueryProcessor getPrimitiveProcessor(PrimitiveQueryKey key) { return explorerContext.getPrimitiveProcessor(key); } private HashSet pendingItems = new HashSet(); private boolean updating = false; private int updateCounter = 0; final ScheduledExecutorService uiUpdateScheduler = ThreadUtils.getNonBlockingWorkExecutor(); private class UpdateItem { TreeNode element; int columnIndex; public UpdateItem(TreeNode element) { this(element,-1); } public UpdateItem(TreeNode element, int columnIndex) { this.element = element; this.columnIndex = columnIndex; if (element != null && element.isDisposed()) { throw new IllegalArgumentException("Node is disposed. " + element); } } public void update(TreeViewer viewer) { if (element != null) { if (element.isDisposed()) { return; } if (((TreeNode)element).updateChildren()) { viewer.refresh(element,true); } else { if (columnIndex >= 0) { viewer.update(element, new String[]{columns[columnIndex].getKey()}); } else { viewer.refresh(element,true); } } if (!element.isDisposed() && autoExpandLevel > 1 && !collapsedNodes.contains(element) && ((TreeNode)element).distanceToRoot() <= autoExpandLevel) { expand = true; viewer.setExpandedState(element, true); expand = false; } } else { if (rootNode.updateChildren()) viewer.refresh(rootNode,true); } } @Override public boolean equals(Object obj) { if (obj == null) return false; if (obj.getClass() != getClass()) return false; UpdateItem other = (UpdateItem)obj; if (columnIndex != other.columnIndex) return false; if (element != null) return element.equals(other.element); return other.element == null; } @Override public int hashCode() { if (element != null) return element.hashCode() + columnIndex; return 0; } } private void update(final TreeNode element, final int columnIndex) { if (DEBUG)System.out.println("update " + element + " " + columnIndex); if (viewer.getTree().isDisposed()) return; synchronized (pendingItems) { pendingItems.add(new UpdateItem(element, columnIndex)); if (updating) return; updateCounter++; scheduleUpdater(); } } private void update(final TreeNode element) { if (DEBUG)System.out.println("update " + element); if (viewer.getTree().isDisposed()) return; if (element != null && element.isDisposed()) return; synchronized (pendingItems) { pendingItems.add(new UpdateItem(element)); if (updating) return; updateCounter++; scheduleUpdater(); } } boolean scheduleUpdater() { if (viewer.getTree().isDisposed()) return false; if (!pendingItems.isEmpty()) { 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 (viewer == null || viewer.getTree().isDisposed()) return; if (updateCounter > 0) { updateCounter = 0; uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS); } else { viewer.getTree().getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl2.this, GraphExplorerImpl2.this.explorerContext)); } } }, delay, TimeUnit.MILLISECONDS); updating = true; return true; } return false; } @Override public String startEditing(NodeContext context, String columnKey) { assertNotDisposed(); if (!thread.currentThreadAccess()) throw new IllegalStateException("not in SWT display thread " + thread.getThread()); if(columnKey.startsWith("#")) { columnKey = columnKey.substring(1); } Integer columnIndex = columnKeyToIndex.get(columnKey); if (columnIndex == null) return "Rename not supported for selection"; viewer.editElement(context, columnIndex); if(viewer.isCellEditorActive()) return null; return "Rename not supported for selection"; } @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); } 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 { Collection coll = AdaptionUtils.adaptToCollection(selection, NodeContext.class); Collection nodes = new ArrayList(); for (NodeContext c : coll) { List match = contextToNodeMap.getValuesUnsafe(c); if(match.size() > 0) nodes.add(match.get(0)); } final ISelection sel = new StructuredSelection(nodes.toArray()); if (coll.size() == 0) return; // Schedule viewer and selection update if necessary. if (viewer.getTree().isDisposed()) return; Display d = viewer.getTree().getDisplay(); if (d.getThread() == Thread.currentThread()) { viewer.setSelection(sel); } else { d.asyncExec(new Runnable() { @Override public void run() { if (viewer.getTree().isDisposed()) return; viewer.setSelection(sel); } }); } } } @Override public void setModificationContext(ModificationContext modificationContext) { this.modificationContext = modificationContext; } final ExecutorService queryUpdateScheduler = Threads.getExecutor(); private static class GeViewerContext extends AbstractDisposable implements IGraphExplorerContext { // This is for query debugging only. private GraphExplorerImpl2 ge; int queryIndent = 0; GECache2 cache = new GECache2(); 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; 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(); public GeViewerContext(GraphExplorerImpl2 ge) { this.ge = ge; } @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) { if (ge == null) return null; return ge.processors.get(o); } @Override @SuppressWarnings("unchecked") public PrimitiveQueryProcessor getPrimitiveProcessor(Object o) { return ge.primitiveProcessors.get(o); } @SuppressWarnings("unchecked") @Override public DataSource getDataSource(Class clazz) { return ge.dataSources.get(clazz); } @Override public void update(UIElementReference ref) { if (ref instanceof ViewerCellReference) { ViewerCellReference tiref = (ViewerCellReference) ref; Object element = tiref.getElement(); int columnIndex = tiref.getColumn(); // NOTE: must be called regardless of the the item value. // A null item is currently used to indicate a tree root update. ge.update((TreeNode)element,columnIndex); } else if (ref instanceof ViewerRowReference) { ViewerRowReference rref = (ViewerRowReference)ref; Object element = rref.getElement(); ge.update((TreeNode)element); } else { throw new IllegalArgumentException("Ui Reference is unknkown " + ref); } } @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 (ge == null) return; if (ge.isDisposed()) return; if (currentQueryUpdater.compareAndSet(null, r)) { ge.queryUpdateScheduler.execute(QUERY_UPDATE_SCHEDULER); } } Runnable QUERY_UPDATE_SCHEDULER = new Runnable() { @Override public void run() { Runnable r = currentQueryUpdater.getAndSet(null); if (r != null) { r.run(); } } }; @Override public void dispose() { cache.dispose(); cache = new DummyCache(); scheduleList.clear(); autoExpanded.clear(); autoExpanded = null; ge = null; } } private static class GeViewerContentProvider implements ITreeContentProvider { @Override public Object[] getElements(Object inputElement) { return getChildren(inputElement); } @Override public Object[] getChildren(Object element) { TreeNode node = (TreeNode)element; return node.getChildren().toArray(); } @Override public Object getParent(Object element) { TreeNode node = (TreeNode)element; return node.getParent(); } @Override public boolean hasChildren(Object element) { return getChildren(element).length > 0; } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } @Override public void dispose() { } } private class GeViewerLabelProvider extends CellLabelProvider { private TreeNode node; private Labeler labeler; private Imager imager; Collection labelDecorators; Collection imageDecorators; Map labels; Map runtimeLabels; @Override public void update(ViewerCell cell) { TreeNode node = (TreeNode)cell.getElement(); NodeContext ctx = node.getContext(); int columnIndex = cell.getColumnIndex(); String columnKey = columns[columnIndex].getKey(); // using columnIndex 0 to refresh data. // Note that this does not work if ViewerCellReferences are used. (At the moment there is no code that would use them). if (node != this.node || columnIndex == 0) { this.node = node; GENodeQueryManager manager = node.getManager(); labeler = manager.query(ctx, BuiltinKeys.SELECTED_LABELER); imager = manager.query(ctx, BuiltinKeys.SELECTED_IMAGER); labelDecorators = manager.query(ctx, BuiltinKeys.LABEL_DECORATORS); imageDecorators = manager.query(ctx, BuiltinKeys.IMAGE_DECORATORS); if (labeler != null) { labels = labeler.getLabels(); runtimeLabels = labeler.getRuntimeLabels(); } else { labels = null; runtimeLabels = null; } } //if (DEBUG) System.out.println("GeViewerLabelProvider.update " + context + " " + columnIndex); setText(cell, columnKey); setImage(cell, columnKey); } void setImage(ViewerCell cell, String columnKey) { if (imager != null) { Object descOrImage = null; boolean hasUncachedImages = false; ImageDescriptor desc = imager.getImage(columnKey); if (desc != null) { int index = 0; // Attempt to decorate the label if (!imageDecorators.isEmpty()) { for (ImageDecorator id : imageDecorators) { ImageDescriptor ds = id.decorateImage(desc, columnKey, index); 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); descOrImage = img != null ? img : desc; hasUncachedImages |= img == null; } if (!hasUncachedImages) { cell.setImage((Image) descOrImage); } else { // Schedule loading to another thread to refrain from // blocking // the UI with database operations. queueImageTask(node, new ImageTask(node, descOrImage)); } } else { cell.setImage(null); } } private void queueImageTask(TreeNode node, ImageTask task) { synchronized (imageTasks) { imageTasks.put(node, task); } imageLoaderJob.scheduleIfNecessary(100); } void setText(ViewerCell cell, String key) { if (labeler != null) { String s = null; if (runtimeLabels != null) s = runtimeLabels.get(key); if (s == null) s = labels.get(key); //if (DEBUG) System.out.println(cell.getElement() + " " + cell.getColumnIndex() + " label:" + s + " key:" + key + " " + labels.size() + " " + (runtimeLabels == null ? "-1" : runtimeLabels.size())); if (s != null) { FontDescriptor font = originalFont; ColorDescriptor bg = originalBackground; ColorDescriptor fg = originalForeground; // Attempt to decorate the label if (!labelDecorators.isEmpty()) { int index = 0; for (LabelDecorator ld : labelDecorators) { String ds = ld.decorateLabel(s, key, index); if (ds != null) s = ds; FontDescriptor dfont = ld.decorateFont(font, key, index); if (dfont != null) font = dfont; ColorDescriptor dbg = ld.decorateBackground(bg, key, index); if (dbg != null) bg = dbg; ColorDescriptor dfg = ld.decorateForeground(fg, key, index); if (dfg != null) fg = dfg; } } if (font != originalFont) { // System.out.println("set font: " + index + ": " + // font); cell.setFont((Font) localResourceManager.get(font)); } else { cell.setFont((Font) (originalFont != null ? localResourceManager.get(originalFont) : null)); } if (bg != originalBackground) cell.setBackground((Color) localResourceManager.get(bg)); else cell.setBackground((Color) (originalBackground != null ? localResourceManager.get(originalBackground) : null)); if (fg != originalForeground) cell.setForeground((Color) localResourceManager.get(fg)); else cell.setForeground((Color) (originalForeground != null ? localResourceManager.get(originalForeground) : null)); cell.setText(s); } } else { cell.setText(Labeler.NO_LABEL); } } } private class GeEditingSupport extends EditingSupport { private Object lastElement; private Modifier lastModifier; private int columnIndex; public GeEditingSupport(ColumnViewer viewer, int columnIndex) { super(viewer); this.columnIndex = columnIndex; } @Override protected boolean canEdit(Object element) { if (filterSelectionEdit && !selectedNodes.contains(element)) { // When item is clicked, canEdit is called before the selection is updated. // This allows filtering edit attempts when the item is selected. return false; } lastElement = null; // clear cached element + modifier. Modifier modifier = getModifier((TreeNode)element); if (modifier == null) return false; return true; } @Override protected CellEditor getCellEditor(Object element) { TreeNode node = (TreeNode) element; Modifier modifier = getModifier((TreeNode)element); NodeContext context = node.getContext(); if (modifier instanceof DialogModifier) { return performDialogEditing(context, (DialogModifier) modifier); } else if (modifier instanceof CustomModifier) { return startCustomEditing(node, (CustomModifier) modifier); } else if (modifier instanceof EnumerationModifier) { return startEnumerationEditing((EnumerationModifier) modifier); } else { return startTextEditing(modifier); } } @Override protected Object getValue(Object element) { Modifier modifier = getModifier((TreeNode)element); return modifier.getValue(); } @Override protected void setValue(Object element, Object value) { Modifier modifier = getModifier((TreeNode)element); // CustomModifiers have internal set value mechanism. if (!(modifier instanceof CustomModifier)) modifier.modify((String)value); } CellEditor startTextEditing( Modifier modifier) { TextCellEditor editor = new ValidatedTextEditor(viewer.getTree());//new TextCellEditor(viewer.getTree()); editor.setValidator(new ModifierValidator(modifier)); return editor; } CellEditor startEnumerationEditing(EnumerationModifier modifier) { if (SimanticsUI.isLinuxGTK()) { // ComboBoxCellEditor2 does not work when GEImpl2 is embedded into dialog (It does work in SelectionView) // CBCE2 does not work because it receives a focus lost event when the combo/popup is opened. return new EnumModifierEditor(viewer.getTree(),modifier); } else { return new EnumModifierEditor2(viewer.getTree(),modifier); } } CellEditor performDialogEditing(final NodeContext context, final DialogModifier modifier) { DialogCellEditor editor = new DialogCellEditor(viewer.getTree()) { String res = null; @Override protected Object openDialogBox(Control cellEditorWindow) { final Semaphore sem = new Semaphore(1); Consumer callback = result -> { res = result; sem.release(); }; String status = modifier.query(cellEditorWindow, null, columnIndex, context, callback); if (status != null) return null; try { sem.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } return res; } }; editor.setValidator(new ModifierValidator(modifier)); return editor; } CellEditor startCustomEditing(TreeNode node, CustomModifier modifier) { CustomModifierEditor editor = new CustomModifierEditor(viewer.getTree(), modifier, node, columnIndex); return editor; } private Modifier getModifier(TreeNode element) { if (element == lastElement) return lastModifier; lastModifier = GraphExplorerImpl2.this.getModifier(element, columnIndex); lastElement = element; return lastModifier; } } private Modifier getModifier(TreeNode element, int columnIndex) { GENodeQueryManager manager = element.getManager(); final NodeContext context = element.getContext(); Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER); if (labeler == null) return null; Column column = columns[columnIndex]; return labeler.getModifier(modificationContext, column.getKey()); } static class ImageTask { TreeNode node; Object descsOrImage; public ImageTask(TreeNode node, Object descsOrImage) { this.node = node; this.descsOrImage = descsOrImage; } } /** * The job that is used for off-loading image loading tasks (see * {@link ImageTask} to a worker thread from the main UI thread. */ ImageLoaderJob imageLoaderJob; // Map imageTasks = new THashMap(); Map imageTasks = new THashMap(); /** * Invoked in a job worker thread. * * @param monitor */ @Override protected IStatus setPendingImages(IProgressMonitor monitor) { ImageTask[] tasks = null; synchronized (imageTasks) { tasks = imageTasks.values().toArray(new ImageTask[imageTasks.size()]); imageTasks.clear(); } MultiStatus status = null; // Load missing images for (ImageTask task : tasks) { Object desc = task.descsOrImage; if (desc instanceof ImageDescriptor) { try { desc = resourceManager.get((ImageDescriptor) desc); task.descsOrImage = 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() { setImages(_tasks); } }); return status != null ? status : Status.OK_STATUS; } void setImages(ImageTask[] tasks) { for (ImageTask task : tasks) if (task != null) setImage(task); } void setImage(ImageTask task) { if (!task.node.isDisposed()) update(task.node, 0); } private static class GraphExplorerPostSelectionProvider implements IPostSelectionProvider { private GraphExplorerImpl2 ge; GraphExplorerPostSelectionProvider(GraphExplorerImpl2 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; } 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; } 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; } 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()); if (ge.viewer.getTree().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.viewer.getTree().isDisposed() || ge.selectionProvider == null) return StructuredSelection.EMPTY; return ge.selectionProvider.getSelection(); } } static class ModifierValidator implements ICellEditorValidator { private Modifier modifier; public ModifierValidator(Modifier modifier) { this.modifier = modifier; } @Override public String isValid(Object value) { return modifier.isValid((String)value); } } static class UpdateRunner implements Runnable { final GraphExplorerImpl2 ge; UpdateRunner(GraphExplorerImpl2 ge, IGraphExplorerContext geContext) { this.ge = ge; } public void run() { try { doRun(); } catch (Throwable t) { t.printStackTrace(); } } public void doRun() { if (ge.isDisposed()) return; HashSet items; ScrollBar verticalBar = ge.viewer.getTree().getVerticalBar(); synchronized (ge.pendingItems) { items = ge.pendingItems; ge.pendingItems = new HashSet(); } if (DEBUG) System.out.println("UpdateRunner.doRun() " + items.size()); ge.viewer.getTree().setRedraw(false); for (UpdateItem item : items) { item.update(ge.viewer); } // check if vertical scroll bar has become visible and refresh layout. boolean currentlyVerticalBarVisible = verticalBar.isVisible(); if (ge.verticalBarVisible != currentlyVerticalBarVisible) { ge.verticalBarVisible = currentlyVerticalBarVisible; ge.viewer.getTree().getParent().layout(); } ge.viewer.getTree().setRedraw(true); synchronized (ge.pendingItems) { if (!ge.scheduleUpdater()) { ge.updating = false; } } if (DEBUG) { if (!ge.updating) { ge.printTree(ge.rootNode, 0); } } } } private class ValidatedTextEditor extends TextCellEditor { public ValidatedTextEditor(Composite parent) { super(parent); } protected void editOccured(org.eclipse.swt.events.ModifyEvent e) { String value = text.getText(); if (value == null) { value = "";//$NON-NLS-1$ } Object typedValue = value; boolean oldValidState = isValueValid(); boolean newValidState = isCorrect(typedValue); if (!newValidState) { text.setBackground(invalidModificationColor); text.setToolTipText(getErrorMessage()); } else { text.setBackground(null); text.setToolTipText(null); } valueChanged(oldValidState, newValidState); }; } private class EnumModifierEditor2 extends ComboBoxCellEditor2 { List values; public EnumModifierEditor2(Composite parent, EnumerationModifier modifier) { super(parent,modifier.getValues().toArray(new String[modifier.getValues().size()]),SWT.READ_ONLY); values = modifier.getValues(); setValidator(new ModifierValidator(modifier)); } @Override protected void doSetValue(Object value) { super.doSetValue((Integer)values.indexOf(value)); } @Override protected Object doGetValue() { return values.get((Integer)super.doGetValue()); } }; private class EnumModifierEditor extends ComboBoxCellEditor { List values; public EnumModifierEditor(Composite parent, EnumerationModifier modifier) { super(parent,modifier.getValues().toArray(new String[modifier.getValues().size()]),SWT.READ_ONLY); values = modifier.getValues(); setValidator(new ModifierValidator(modifier)); } @Override protected void doSetValue(Object value) { super.doSetValue((Integer)values.indexOf(value)); } @Override protected Object doGetValue() { return values.get((Integer)super.doGetValue()); } }; private class CustomModifierEditor extends CellEditor implements ICellEditorValidator, DisposeListener { private CustomModifier modifier; private TreeNode node; private NodeContext context; private int columnIndex; private Composite control; private Control origControl; public CustomModifierEditor(Composite parent, CustomModifier modifier, TreeNode node, int columnIndex) { this.modifier = modifier; this.node = node; this.context = node.getContext(); this.columnIndex = columnIndex; setValidator(this); create(parent); } @Override protected Control createControl(Composite parent) { control = new Composite(parent, SWT.NONE); control.setLayout(new FillLayout()); origControl = (Control) modifier.createControl(control, null, columnIndex, context); return control; } @Override protected Object doGetValue() { return modifier.getValue(); } @Override protected void doSetValue(Object value) { //CustomModifier handles value setting internally. } private void reCreate() { modifier = (CustomModifier)getModifier(node, columnIndex); if (control != null && !control.isDisposed()) { if (!origControl.isDisposed()) origControl.dispose(); origControl = (Control)modifier.createControl(control, null, columnIndex, context); origControl.addDisposeListener(this); } } protected void doSetFocus() { if (control != null && !control.isDisposed()) control.setFocus(); }; @Override public void widgetDisposed(DisposeEvent e) { if (e.widget == origControl) { reCreate(); } } @Override public String isValid(Object value) { return modifier.isValid((String)value); } } private class TreeNode implements IAdaptable { private NodeContext context; private TreeNode parent; private List children = new ArrayList(); private GENodeQueryManager manager; private TreeNode(NodeContext context) { if (context == null) throw new NullPointerException(); this.context = context; contextToNodeMap.add(context, this); manager = new GENodeQueryManager(explorerContext, null, null, ViewerRowReference.create(this)); } public List getChildren() { synchronized (children) { return children; } } public TreeNode getParent() { return parent; } public NodeContext getContext() { return context; } public GENodeQueryManager getManager() { return manager; } public TreeNode addChild(NodeContext context) { TreeNode child = new TreeNode(context); child.parent = this; children.add(child); if (DEBUG) System.out.println("Add " + this + " -> " + child); return child; } public TreeNode addChild(int index, NodeContext context) { TreeNode child = new TreeNode(context); child.parent = this; children.add(index,child); if (DEBUG) System.out.println("Add " + this + " -> " + child + " at " + index); return child; } public TreeNode setChild(int index, NodeContext context) { TreeNode child = new TreeNode(context); child.parent = this; children.set(index,child); if (DEBUG) System.out.println("Set " + this + " -> " + child + " at " + index); return child; } public int distanceToRoot() { int i = 0; TreeNode n = getParent(); while (n != null) { n = n.getParent(); i++; } return i; } public void dispose() { if (parent != null) parent.children.remove(this); dispose2(); } public void dispose2() { if (DEBUG) System.out.println("dispose " + this); parent = null; for (TreeNode n : children) { n.dispose2(); } clearCache(); children.clear(); contextToNodeMap.remove(context, this); context = null; manager.dispose(); manager = null; } private void clearCache() { if (explorerContext != null) { GECache2 cache = explorerContext.cache; if (cache != null) { cache.dispose(context); } } } public boolean updateChildren() { if (context == null) throw new IllegalStateException("Node is disposed."); NodeContext[] childContexts = manager.query(context, BuiltinKeys.FINAL_CHILDREN); if (DEBUG) System.out.println("updateChildren " + childContexts.length + " " + this); boolean modified = false; synchronized (children) { int oldCount = children.size(); BijectionMap indexes = new BijectionMap(); Set mapped = new HashSet(); boolean reorder = false; // locate matching pairs form old and new children for (int i = 0; i < oldCount; i++) { NodeContext oldCtx = children.get(i).context; for (int j = 0; j oldChildren = new ArrayList(oldCount); oldChildren.addAll(children); if (childContexts.length >= oldCount) { for (int i = 0; i < oldCount; i++) { Integer oldIndex = indexes.getLeft(i); if (oldIndex == null) { setChild(i, childContexts[i]); } else { TreeNode n = oldChildren.get(oldIndex); children.set(i, n); } } for (int i = oldCount; i < childContexts.length; i++) { addChild(childContexts[i]); } } else { for (int i = 0; i < childContexts.length; i++) { Integer oldIndex = indexes.getLeft(i); if (oldIndex == null) { setChild(i, childContexts[i]); } else { TreeNode n = oldChildren.get(oldIndex); children.set(i, n); } } for (int i = oldCount -1; i >= childContexts.length; i--) { children.remove(i); } } for (int i = 0; i < oldChildren.size(); i++) { if (!indexes.containsLeft(i)) { oldChildren.get(i).dispose2(); } } } } return modified; } public boolean isDisposed() { return context == null; } @SuppressWarnings("unchecked") @Override public T getAdapter(Class adapter) { if (adapter == NodeContext.class) return (T) context; return context.getAdapter(adapter); } // @Override // public String toString() { // String s = ""; // if (manager != null) { // // s+= super.toString() + " "; // try { // Labeler labeler = manager.query(context, BuiltinKeys.SELECTED_LABELER); // Map labels = labeler.getLabels(); // for (Entry l : labels.entrySet()) { // s+= l.getKey() + " : " + l.getValue() + " "; // } // } catch (Exception e) { // // } // } else { // s = super.toString(); // } // if (context != null) // s += " context " + context.hashCode(); // return s; // // } } private static class TreeNodeIsExpandedProcessor extends AbstractPrimitiveQueryProcessor implements IsExpandedProcessor, ProcessorLifecycle { /** * The set of currently expanded node contexts. */ private final HashSet expanded = new HashSet(); private final HashMap expandedQueries = new HashMap(); private Tree tree; public TreeNodeIsExpandedProcessor() { } @Override public Object getIdentifier() { return BuiltinKeys.IS_EXPANDED; } @Override public String toString() { return "IsExpandedProcessor"; } @Override public Boolean query(PrimitiveQueryUpdater updater, NodeContext context, PrimitiveQueryKey key) { boolean isExpanded = expanded.contains(context); expandedQueries.put(context, updater); return Boolean.valueOf(isExpanded); } @Override public Collection getExpanded() { return new HashSet(expanded); } @Override public boolean getExpanded(NodeContext context) { return this.expanded.contains(context); } @Override public boolean setExpanded(NodeContext context, boolean expanded) { return _setExpanded(context, expanded); } @Override public boolean replaceExpanded(NodeContext context, boolean expanded) { return nodeStatusChanged(context, expanded); } private boolean _setExpanded(NodeContext context, boolean expanded) { if (expanded) { return this.expanded.add(context); } else { return this.expanded.remove(context); } } Listener treeListener = new Listener() { @Override public void handleEvent(Event event) { TreeNode node = (TreeNode) event.item.getData(); NodeContext context = node.getContext(); switch (event.type) { case SWT.Expand: nodeStatusChanged(context, true); break; case SWT.Collapse: nodeStatusChanged(context, false); break; } } }; protected boolean nodeStatusChanged(NodeContext context, boolean expanded) { boolean result = _setExpanded(context, expanded); PrimitiveQueryUpdater updater = expandedQueries.get(context); if (updater != null) updater.scheduleReplace(context, BuiltinKeys.IS_EXPANDED, expanded); return result; } @Override public void attached(GraphExplorer explorer) { Object control = explorer.getControl(); if (control instanceof Tree) { this.tree = (Tree) control; tree.addListener(SWT.Expand, treeListener); tree.addListener(SWT.Collapse, treeListener); } else { System.out.println("WARNING: " + getClass().getSimpleName() + " attached to unsupported control: " + control); } } @Override public void clear() { expanded.clear(); expandedQueries.clear(); } @Override public void detached(GraphExplorer explorer) { clear(); if (tree != null) { tree.removeListener(SWT.Expand, treeListener); tree.removeListener(SWT.Collapse, treeListener); tree = null; } } } private void printTree(TreeNode node, int depth) { String s = ""; for (int i = 0; i < depth; i++) { s += " "; } s += node; System.out.println(s); int d = depth+1; for (TreeNode n : node.getChildren()) { printTree(n, d); } } /** * Copy-paste of org.simantics.browsing.ui.common.internal.GECache.GECacheKey (internal class that cannot be used) */ final private static class GECacheKey { private NodeContext context; private CacheKey key; GECacheKey(NodeContext context, CacheKey key) { this.context = context; this.key = key; if (context == null || key == null) throw new IllegalArgumentException("Null context or key is not accepted"); } GECacheKey(GECacheKey other) { this.context = other.context; this.key = other.key; if (context == null || key == null) throw new IllegalArgumentException("Null context or key is not accepted"); } void setValues(NodeContext context, CacheKey key) { this.context = context; this.key = key; if (context == null || key == null) throw new IllegalArgumentException("Null context or key is not accepted"); } @Override public int hashCode() { return context.hashCode() | key.hashCode(); } @Override public boolean equals(Object object) { if (this == object) return true; else if (object == null) return false; GECacheKey i = (GECacheKey) object; return key.equals(i.key) && context.equals(i.context); } }; /** * Copy-paste of org.simantics.browsing.ui.common.internal.GECache with added capability of purging all NodeContext related data. */ private static class GECache2 implements IGECache { final HashMap entries = new HashMap(); final HashMap> treeReferences = new HashMap>(); final HashMap> keyRefs = new HashMap>(); /** * This single instance is used for all get operations from the cache. This * should work since the GE cache is meant to be single-threaded within the * current UI thread, what ever that thread is. For put operations which * store the key, this is not used. */ NodeContext getNC = new NodeContext() { @Override public T getAdapter(Class adapter) { return null; } @Override public T getConstant(ConstantKey key) { return null; } @Override public Set> getKeys() { return Collections.emptySet(); } }; CacheKey getCK = new CacheKey() { @Override public Object processorIdenfitier() { return this; } }; GECacheKey getKey = new GECacheKey(getNC, getCK); private void addKey(GECacheKey key) { Set refs = keyRefs.get(key.context); if (refs != null) { refs.add(key); } else { refs = new HashSet(); refs.add(key); keyRefs.put(key.context, refs); } } private void removeKey(GECacheKey key) { Set refs = keyRefs.get(key.context); if (refs != null) { refs.remove(key); } } public IGECacheEntry put(NodeContext context, CacheKey key, T value) { // if (DEBUG) System.out.println("Add entry " + context + " " + key); IGECacheEntry entry = new GECacheEntry(context, key, value); GECacheKey gekey = new GECacheKey(context, key); entries.put(gekey, entry); addKey(gekey); return entry; } @SuppressWarnings("unchecked") public T get(NodeContext context, CacheKey key) { getKey.setValues(context, key); IGECacheEntry entry = entries.get(getKey); if (entry == null) return null; return (T) entry.getValue(); } @Override public IGECacheEntry getEntry(NodeContext context, CacheKey key) { assert(context != null); assert(key != null); getKey.setValues(context, key); return entries.get(getKey); } @Override public void remove(NodeContext context, CacheKey key) { // if (DEBUG) System.out.println("Remove entry " + context + " " + key); getKey.setValues(context, key); entries.remove(getKey); removeKey(getKey); } @Override public Set getTreeReference(NodeContext context, CacheKey key) { assert(context != null); assert(key != null); getKey.setValues(context, key); return treeReferences.get(getKey); } @Override public void putTreeReference(NodeContext context, CacheKey key, UIElementReference reference) { assert(context != null); assert(key != null); //if (DEBUG) System.out.println("Add tree reference " + context + " " + key); getKey.setValues(context, key); Set refs = treeReferences.get(getKey); if (refs != null) { refs.add(reference); } else { refs = new HashSet(4); refs.add(reference); GECacheKey gekey = new GECacheKey(getKey); treeReferences.put(gekey, refs); addKey(gekey); } } @Override public Set removeTreeReference(NodeContext context, CacheKey key) { assert(context != null); assert(key != null); //if (DEBUG) System.out.println("Remove tree reference " + context + " " + key); getKey.setValues(context, key); removeKey(getKey); return treeReferences.remove(getKey); } @Override public boolean isShown(NodeContext context) { return references.get(context) > 0; } private TObjectIntHashMap references = new TObjectIntHashMap(); @Override public void incRef(NodeContext context) { int exist = references.get(context); references.put(context, exist+1); } @Override public void decRef(NodeContext context) { int exist = references.get(context); references.put(context, exist-1); if(exist == 1) { references.remove(context); } } public void dispose() { references.clear(); entries.clear(); treeReferences.clear(); keyRefs.clear(); } public void dispose(NodeContext context) { Set keys = keyRefs.remove(context); if (keys != null) { for (GECacheKey key : keys) { entries.remove(key); treeReferences.remove(key); } } } } /** * Non-functional cache to replace actual cache when GEContext is disposed. * * @author mlmarko * */ private static class DummyCache extends GECache2 { @Override public IGECacheEntry getEntry(NodeContext context, CacheKey key) { return null; } @Override public IGECacheEntry put(NodeContext context, CacheKey key, T value) { return null; } @Override public void putTreeReference(NodeContext context, CacheKey key, UIElementReference reference) { } @Override public T get(NodeContext context, CacheKey key) { return null; } @Override public Set getTreeReference( NodeContext context, CacheKey key) { return null; } @Override public void remove(NodeContext context, CacheKey key) { } @Override public Set removeTreeReference( NodeContext context, CacheKey key) { return null; } @Override public boolean isShown(NodeContext context) { return false; } @Override public void incRef(NodeContext context) { } @Override public void decRef(NodeContext context) { } @Override public void dispose() { super.dispose(); } } @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; } }