X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.browsing.ui.swt%2Fsrc%2Forg%2Fsimantics%2Fbrowsing%2Fui%2Fswt%2FGraphExplorerImpl2.java;fp=bundles%2Forg.simantics.browsing.ui.swt%2Fsrc%2Forg%2Fsimantics%2Fbrowsing%2Fui%2Fswt%2FGraphExplorerImpl2.java;h=f3fca15c4946c7c3c2666eb8305fd6ef66c2ba6b;hb=969bd23cab98a79ca9101af33334000879fb60c5;hp=0000000000000000000000000000000000000000;hpb=866dba5cd5a3929bbeae85991796acb212338a08;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl2.java b/bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl2.java new file mode 100644 index 000000000..f3fca15c4 --- /dev/null +++ b/bundles/org.simantics.browsing.ui.swt/src/org/simantics/browsing/ui/swt/GraphExplorerImpl2.java @@ -0,0 +1,2932 @@ +/******************************************************************************* + * 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.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.Platform; +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.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.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.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.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.BinaryFunction; +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.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 BinaryFunction selectionTransformation = new BinaryFunction() { + + @Override + public Object[] call(GraphExplorer explorer, Object[] objects) { + Object[] result = new Object[objects.length]; + for (int i = 0; i < objects.length; i++) { + IHintContext context = new AdaptableHintContext(SelectionHints.KEY_MAIN); + context.setHint(SelectionHints.KEY_MAIN, objects[i]); + result[i] = context; + } + return result; + } + + }; + + 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; + + ExplorerState state = persistor.deserialize( + Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(), + getRoot()); + + + Object processor = getPrimitiveProcessor(BuiltinKeys.IS_EXPANDED); + if (processor instanceof DefaultIsExpandedProcessor) { + DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor; + for(NodeContext expanded : state.expandedNodes) { + isExpandedProcessor.setExpanded(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) { + prevWidths.put(c.getColumn().getText(), 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); + 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.call(this, objects); + } + + protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) { + int len = contexts.length; + Object[] objects = new Object[len]; + for (int i = 0; i < len; ++i) + objects[i] = filter.filter(contexts[i]); + return objects; + } + + @Override + public void setSelectionTransformation( + BinaryFunction 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("rawtypes") + @Override + public Object getAdapter(Class adapter) { + if (adapter == NodeContext.class) + return 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() { + @SuppressWarnings("rawtypes") + @Override + public Object 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(); + } + } +}