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.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
-import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.resource.ResourceManager;
+import org.eclipse.jface.util.OpenStrategy;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
-import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.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.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.db.layer0.SelectionHints;
import org.simantics.utils.ObjectUtils;
import org.simantics.utils.datastructures.BijectionMap;
-import org.simantics.utils.datastructures.BinaryFunction;
import org.simantics.utils.datastructures.disposable.AbstractDisposable;
import org.simantics.utils.datastructures.hints.IHintContext;
import org.simantics.utils.threads.IThreadWorkQueue;
import org.simantics.utils.threads.SWTThread;
import org.simantics.utils.threads.ThreadUtils;
import org.simantics.utils.ui.ISelectionUtils;
+import org.simantics.utils.ui.SWTUtils;
import org.simantics.utils.ui.jface.BasePostSelectionProvider;
import org.simantics.utils.ui.widgets.VetoingEventHandler;
import org.simantics.utils.ui.workbench.WorkbenchUtils;
public static final int DEFAULT_MAX_CHILDREN = 1000;
- private static final long POST_SELECTION_DELAY = 300;
-
- /**
- * The time in milliseconds that must elapse between consecutive
- * {@link Tree} {@link SelectionListener#widgetSelected(SelectionEvent)}
- * invocations in order for this class to construct a new selection.
- *
- * <p>
- * This is done because selection construction can be very expensive as the
- * selected set grows larger when the user is pressing shift+arrow keys.
- * GraphExplorerImpl will naturally listen to all changes in the tree
- * selection, but as an optimization will not construct new
- * StructuredSelection instances for every selection change event. A new
- * selection will be constructed and set only if the selection hasn't
- * changed for the amount of milliseconds specified by this constant.
- */
- private static final long SELECTION_CHANGE_QUIET_TIME = 150;
-
private final IThreadWorkQueue thread;
/**
Tree tree;
@SuppressWarnings({ "rawtypes" })
- final HashMap<CacheKey<?>, NodeQueryProcessor> processors = new HashMap<CacheKey<?>, NodeQueryProcessor>();
+ final HashMap<CacheKey<?>, NodeQueryProcessor> processors = new HashMap<>();
@SuppressWarnings({ "rawtypes" })
- final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors = new HashMap<Object, PrimitiveQueryProcessor>();
+ final HashMap<Object, PrimitiveQueryProcessor> primitiveProcessors = new HashMap<>();
@SuppressWarnings({ "rawtypes" })
- final HashMap<Class, DataSource> dataSources = new HashMap<Class, DataSource>();
+ final HashMap<Class, DataSource> dataSources = new HashMap<>();
class GraphExplorerContext extends AbstractDisposable implements IGraphExplorerContext {
// This is for query debugging only.
* <code>null</code> there's nothing scheduled yet in which case
* scheduling can commence. Otherwise the update should be skipped.
*/
- AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<Runnable>();
+ AtomicReference<Runnable> currentQueryUpdater = new AtomicReference<>();
/**
* Keeps track of nodes that have already been auto-expanded. After
* expanded state after that. This makes it possible for the user to
* close auto-expanded nodes.
*/
- Map<NodeContext, Boolean> autoExpanded = new WeakHashMap<NodeContext, Boolean>();
+ Map<NodeContext, Boolean> autoExpanded = new WeakHashMap<>();
@Override
GraphExplorerContext explorerContext = new GraphExplorerContext();
- HashSet<TreeItem> pendingItems = new HashSet<TreeItem>();
+ HashSet<TreeItem> pendingItems = new HashSet<>();
boolean updating = false;
boolean pendingRoot = false;
* Access this map only in the SWT thread to keep it thread-safe.
* </p>
*/
- BijectionMap<NodeContext, TreeItem> contextToItem = new BijectionMap<NodeContext, TreeItem>();
+ BijectionMap<NodeContext, TreeItem> contextToItem = new BijectionMap<>();
/**
* Columns of the UI viewer. Use {@link #setColumns(Column[])} to
* initialize.
*/
Column[] columns = new Column[0];
- Map<String, Integer> columnKeyToIndex = new HashMap<String, Integer>();
+ Map<String, Integer> columnKeyToIndex = new HashMap<>();
boolean refreshingColumnSizes = false;
boolean columnsAreVisible = true;
/** Set to true when the Tree widget is disposed. */
private boolean disposed = false;
- private final CopyOnWriteArrayList<FocusListener> focusListeners = new CopyOnWriteArrayList<FocusListener>();
- private final CopyOnWriteArrayList<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();
- private final CopyOnWriteArrayList<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
+ private final CopyOnWriteArrayList<FocusListener> focusListeners = new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
/** Selection provider */
- private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
+ private GraphExplorerPostSelectionProvider postSelectionProvider = new GraphExplorerPostSelectionProvider(this);
protected BasePostSelectionProvider selectionProvider = new BasePostSelectionProvider();
protected SelectionDataResolver selectionDataResolver;
protected SelectionFilter selectionFilter;
- protected BinaryFunction<Object[], GraphExplorer, Object[]> selectionTransformation = new BinaryFunction<Object[], GraphExplorer, Object[]>() {
+ protected BiFunction<GraphExplorer, Object[], Object[]> selectionTransformation = new BiFunction<GraphExplorer, Object[], Object[]>() {
@Override
- public Object[] call(GraphExplorer explorer, Object[] objects) {
+ 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);
* item was a part of the current selection in which case the selection must
* be updated.
*/
- private final Map<TreeItem, NodeContext> selectedItems = new HashMap<TreeItem, NodeContext>();
+ private final Map<TreeItem, NodeContext> selectedItems = new HashMap<>();
/**
* TODO: specify what this is for
*/
- private final Set<NodeContext> selectionRefreshContexts = new HashSet<NodeContext>();
+ private final Set<NodeContext> selectionRefreshContexts = new HashSet<>();
/**
* If this field is non-null, it means that if {@link #setData(Event)}
*
* @see #setPendingImages(IProgressMonitor)
*/
- Map<TreeItem, ImageTask> imageTasks = new THashMap<TreeItem, ImageTask>();
+ Map<TreeItem, ImageTask> imageTasks = new THashMap<>();
/**
* A state flag indicating whether the vertical scroll bar was visible for
updateCounter = 0;
uiUpdateScheduler.schedule(this, 50, TimeUnit.MILLISECONDS);
} else {
- tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this, GraphExplorerImpl.this.explorerContext));
+ tree.getDisplay().asyncExec(new UpdateRunner(GraphExplorerImpl.this));
}
}
* modified. These are used internally to prevent duplicate edits from being
* initiated which should always be a sensible thing to do.
*/
- private Set<NodeContext> currentlyModifiedNodes = new THashSet<NodeContext>();
+ private Set<NodeContext> currentlyModifiedNodes = new THashSet<>();
private final TreeEditor editor;
private Color invalidModificationColor = null;
// Keep track of the previous single selection to help
// decide whether to start editing a tree node on mouse
// downs or not.
- tree.addListener(SWT.Selection, new Listener() {
- @Override
- public void handleEvent(Event event) {
- TreeItem[] selection = tree.getSelection();
- if (selection.length == 1) {
- //for (TreeItem item : selection)
- // System.out.println("selection: " + item);
- previousSingleSelection = selection[0];
- } else {
- previousSingleSelection = null;
- }
+ tree.addListener(SWT.Selection, event -> {
+ TreeItem[] selection = tree.getSelection();
+ if (selection.length == 1) {
+ //for (TreeItem item : selection)
+ // System.out.println("selection: " + item);
+ previousSingleSelection = selection[0];
+ } else {
+ previousSingleSelection = null;
}
});
};
tree.addListener(SWT.MouseDown, mouseEditListener);
tree.addListener(SWT.DragDetect, mouseEditListener);
- tree.addListener(SWT.DragDetect, new Listener() {
- @Override
- public void handleEvent(Event event) {
- Point test = new Point(event.x, event.y);
- TreeItem item = tree.getItem(test);
- if(item != null) {
- for(int i=0;i<tree.getColumnCount();i++) {
- Rectangle rect = item.getBounds(i);
- if(rect.contains(test)) {
- tree.setData(KEY_DRAG_COLUMN, i);
- return;
- }
+ tree.addListener(SWT.DragDetect, event -> {
+ Point test = new Point(event.x, event.y);
+ TreeItem item = tree.getItem(test);
+ if(item != null) {
+ for(int i=0;i<tree.getColumnCount();i++) {
+ Rectangle rect = item.getBounds(i);
+ if(rect.contains(test)) {
+ tree.setData(KEY_DRAG_COLUMN, i);
+ return;
}
}
- tree.setData(KEY_DRAG_COLUMN, -1);
}
+ tree.setData(KEY_DRAG_COLUMN, -1);
});
- tree.addListener(SWT.MouseMove, new Listener() {
- @Override
- public void handleEvent(Event event) {
- Point test = new Point(event.x, event.y);
- TreeItem item = tree.getItem(test);
- if(item != null) {
- for(int i=0;i<tree.getColumnCount();i++) {
- Rectangle rect = item.getBounds(i);
- if(rect.contains(test)) {
- transientState.setActiveColumn(i);
- return;
- }
+ tree.addListener(SWT.MouseMove, event -> {
+ Point test = new Point(event.x, event.y);
+ TreeItem item = tree.getItem(test);
+ if(item != null) {
+ for(int i=0;i<tree.getColumnCount();i++) {
+ Rectangle rect = item.getBounds(i);
+ if(rect.contains(test)) {
+ transientState.setActiveColumn(i);
+ return;
}
}
- transientState.setActiveColumn(null);
}
+ transientState.setActiveColumn(null);
});
// Add focus/mouse/key listeners for supporting the respective
}
});
- // Add a tree selection listener for keeping the selection of
- // GraphExplorer's ISelectionProvider up-to-date.
- tree.addSelectionListener(new SelectionListener() {
- @Override
- public void widgetDefaultSelected(SelectionEvent e) {
- widgetSelected(e);
- }
- @Override
- public void widgetSelected(SelectionEvent e) {
- widgetSelectionChanged(false);
- }
- });
+ OpenStrategy os = new OpenStrategy(tree);
+ os.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
+ resetSelectionFromWidget();
+ }));
+ os.addPostSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
+ //System.out.println("OPENSTRATEGY: post selection changed: " + e);
+ resetSelectionFromWidgetAndFirePostSelection(true);
+ }));
// This listener takes care of updating the set of currently selected
// TreeItem instances. This set is needed because we need to know in
}
/**
- * Mod count for delaying post selection changed events.
- */
- int postSelectionModCount = 0;
-
- /**
- * Last tree selection modification time for implementing a quiet
- * time for selection changes.
+ * @return the new selection if it was different from the old selection in
+ * {@link #selectionProvider}
*/
- long lastSelectionModTime = System.currentTimeMillis() - 10000;
-
- /**
- * Current target time for the selection to be set. Calculated
- * according to the set quiet time and last selection modification
- * time.
- */
- long selectionSetTargetTime = 0;
+ private ISelection resetSelectionFromWidget() {
+ ISelection widgetSelection = getWidgetSelection();
+// System.out.println("resetSelection()");
+// System.out.println(" provider selection: " + selectionProvider.getSelection());
+// System.out.println(" widget selection: " + widgetSelection);
+ boolean equals = selectionProvider.selectionEquals(widgetSelection);
+ selectionProvider.setSelectionWithoutFiring(widgetSelection);
+ return equals ? null : widgetSelection;
+ }
/**
- * <code>true</code> if delayed selection runnable is current scheduled or
- * running.
+ * @return the new selection if it was different from the old selection in
+ * {@link #selectionProvider}
*/
- boolean delayedSelectionScheduled = false;
-
- Runnable SELECTION_DELAY = new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed())
- return;
- long now = System.currentTimeMillis();
- long waitTimeLeft = selectionSetTargetTime - now;
- if (waitTimeLeft > 0) {
- // Not enough quiet time, reschedule.
- delayedSelectionScheduled = true;
- tree.getDisplay().timerExec((int) waitTimeLeft, this);
- } else {
- // Time to perform selection, stop rescheduling.
- delayedSelectionScheduled = false;
- resetSelection();
- }
- }
- };
-
- private void widgetSelectionChanged(boolean forceSelectionChange) {
- long modTime = System.currentTimeMillis();
- long delta = modTime - lastSelectionModTime;
- lastSelectionModTime = modTime;
- if (!forceSelectionChange && delta < SELECTION_CHANGE_QUIET_TIME) {
- long msToWait = SELECTION_CHANGE_QUIET_TIME - delta;
- selectionSetTargetTime = modTime + msToWait;
- if (!delayedSelectionScheduled) {
- delayedSelectionScheduled = true;
- tree.getDisplay().timerExec((int) msToWait, SELECTION_DELAY);
- }
- // Make sure that post selection change events do not fire.
- ++postSelectionModCount;
- return;
+ private boolean resetSelectionFromWidgetAndFirePostSelection(boolean force) {
+ ISelection s = resetSelectionFromWidget();
+ boolean fire = s != null || force;
+ if (fire) {
+ //System.out.println("FIRING POST-SELECTION: " + selectionProvider.getSelection());
+ selectionProvider.firePostSelection(selectionProvider.getSelection());
}
-
- // Immediate selection reconstruction.
- resetSelection();
+ return fire;
}
- private void resetSelection() {
- final ISelection selection = getWidgetSelection();
-
- //System.out.println("resetSelection(" + postSelectionModCount + ")");
- //System.out.println(" provider selection: " + selectionProvider.getSelection());
- //System.out.println(" widget selection: " + selection);
-
- selectionProvider.setAndFireNonEqualSelection(selection);
-
- // Implement deferred firing of post selection events
- final int count = ++postSelectionModCount;
- //System.out.println("[" + System.currentTimeMillis() + "] scheduling postSelectionChanged " + count + ": " + selection);
- ThreadUtils.getNonBlockingWorkExecutor().schedule(new Runnable() {
- @Override
- public void run() {
- int newCount = postSelectionModCount;
- // Don't publish selection yet, there's another change incoming.
- //System.out.println("[" + System.currentTimeMillis() + "] checking post selection publish: " + count + " vs. " + newCount + ": " + selection);
- if (newCount != count)
- return;
- //System.out.println("[" + System.currentTimeMillis() + "] " + count + " count equals, firing post selection listeners: " + selection);
-
- if (tree.isDisposed())
- return;
-
- //System.out.println("scheduling fire post selection changed: " + selection);
- tree.getDisplay().asyncExec(new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed() || selectionProvider == null)
- return;
- //System.out.println("firing post selection changed: " + selection);
- selectionProvider.firePostSelection(selection);
- }
- });
- }
- }, POST_SELECTION_DELAY, TimeUnit.MILLISECONDS);
- }
-
protected void setDefaultProcessors() {
// Add a simple IMAGER query processor that always returns null.
// With this processor no images will ever be shown.
}
@Override
- public void setSelectionTransformation(BinaryFunction<Object[], GraphExplorer, Object[]> f) {
+ public void setSelectionTransformation(BiFunction<GraphExplorer, Object[], Object[]> f) {
this.selectionTransformation = f;
}
});
}
}
-
+
private void initializeState() {
if (persistor == null)
return;
+ ExplorerStates.scheduleRead(getRoot(), persistor)
+ .thenAccept(state -> SWTUtils.asyncExec(tree, () -> restoreState(state)));
+ }
- ExplorerState state = persistor.deserialize(
- Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
- getRoot());
-
+ private void restoreState(ExplorerState state) {
// topNodeToSet will be processed by #setData when it encounters a
// NodeContext that matches this one.
// topNodePath = state.topNodePath;
if (processor instanceof DefaultIsExpandedProcessor) {
DefaultIsExpandedProcessor isExpandedProcessor = (DefaultIsExpandedProcessor)processor;
for(NodeContext expanded : state.expandedNodes) {
- isExpandedProcessor.setExpanded(expanded, true);
+ isExpandedProcessor.replaceExpanded(expanded, true);
}
}
}
}
persistor.serialize(
- Platform.getStateLocation(Activator.getDefault().getBundle()).toFile(),
+ ExplorerStates.explorerStateLocation(),
getRoot(),
new ExplorerState(topNodePath, topNodePathChildIndex, expandedNodes, columnWidths));
}
// System.out.println("MODCOUNT: " + modCount + " vs. " + count);
if (modCount != count)
return;
- widgetSelectionChanged(true);
+ resetSelectionFromWidgetAndFirePostSelection(false);
}
});
}
}
protected Object[] transformSelection(Object[] objects) {
- return selectionTransformation.call(this, objects);
+ return selectionTransformation.apply(this, objects);
}
protected static Object[] filter(SelectionFilter filter, NodeContext[] contexts) {
*/
private void doSetColumns(Column[] cols, Consumer<Map<Column, Object>> callback) {
// Attempt to keep previous column widths.
- Map<String, Integer> prevWidths = new HashMap<String, Integer>();
+ Map<String, Integer> prevWidths = new HashMap<>();
for (TreeColumn column : tree.getColumns()) {
- prevWidths.put(column.getText(), column.getWidth());
- column.dispose();
+ Column c = (Column) column.getData();
+ if (c != null) {
+ prevWidths.put(c.getKey(), column.getWidth());
+ column.dispose();
+ }
}
- HashMap<String, Integer> keyToIndex = new HashMap<String, Integer>();
+ HashMap<String, Integer> keyToIndex = new HashMap<>();
for (int i = 0; i < cols.length; ++i) {
keyToIndex.put(cols[i].getKey(), i);
}
this.columnImageArray = new Image[cols.length];
this.columnDescOrImageArray = new Object[cols.length];
- Map<Column, Object> map = new HashMap<Column, Object>();
+ Map<Column, Object> map = new HashMap<>();
tree.setHeaderVisible(columnsAreVisible);
for (Column column : columns) {
int cw = column.getWidth();
// Try to keep previous widths
- Integer w = prevWidths.get(column);
+ Integer w = prevWidths.get(column.getKey());
if (w != null)
c.setWidth(w);
else if (cw != Column.DEFAULT_CONTROL_WIDTH)
if(callback != null) callback.accept(map);
// Make sure the explorer fits the columns properly after initialization.
- tree.getDisplay().asyncExec(new Runnable() {
- @Override
- public void run() {
- if (tree.isDisposed())
- return;
+ SWTUtils.asyncExec(tree, () -> {
+ if (!tree.isDisposed())
refreshColumnSizes();
- }
});
}