]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/NodeTree.java
Improved PDF diagram export wizard user experience
[simantics/platform.git] / bundles / org.simantics.modeling.ui / src / org / simantics / modeling / ui / pdf / NodeTree.java
diff --git a/bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/NodeTree.java b/bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/NodeTree.java
new file mode 100644 (file)
index 0000000..a17aafb
--- /dev/null
@@ -0,0 +1,501 @@
+/*******************************************************************************
+ * Copyright (c) 2017 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:
+ *     Semantum Oy - #7297
+ *******************************************************************************/
+package org.simantics.modeling.ui.pdf;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.viewers.CellLabelProvider;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.ICheckStateProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.jface.viewers.ViewerComparator;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.TreeItem;
+import org.simantics.browsing.ui.common.views.DefaultFilterStrategy;
+import org.simantics.browsing.ui.common.views.IFilterStrategy;
+import org.simantics.modeling.requests.CollectionResult;
+import org.simantics.modeling.requests.Node;
+import org.simantics.utils.strings.AlphanumComparator;
+import org.simantics.utils.ui.ISelectionUtils;
+
+/**
+ * A tree of nodes intended for usable listing and selecting diagrams.
+ * 
+ * @author Tuukka Lehtonen
+ * @since 1.30.0
+ */
+public class NodeTree extends Composite {
+
+       /**
+        * This exists to make {@link NodeCheckStateProvider} faster
+        */
+       private static class CheckStateCache {
+               Map<Node, Boolean> isChecked = new HashMap<>();
+               Map<Node, Boolean> isGrayed = new HashMap<>();
+
+               public void invalidate(Node n) {
+                       for (; n != null; n = n.getParent()) {
+                               isChecked.remove(n);
+                               isGrayed.remove(n);
+                       }
+               }
+               public void invalidate() {
+                       isChecked.clear();
+                       isGrayed.clear();
+               }
+       }
+
+       protected Display              display;
+
+       protected LocalResourceManager resourceManager;
+
+       protected Color                noDiagramColor;
+
+       protected IFilterStrategy      filterStrategy     = new DefaultFilterStrategy();
+
+       protected Text                 filter;
+
+       protected Matcher              matcher            = null;
+
+       protected CheckboxTreeViewer   tree;
+
+       /**
+        * The tree paths that were expanded last time no filter was defined. Will
+        * be nullified after the expanded paths have been returned when
+        * {@link #matcher} turns null.
+        */
+       protected TreePath[]           noFilterExpandedPaths;
+
+       protected Set<Node>            selectedNodes;
+
+       protected CheckStateCache      checkStateCache = new CheckStateCache();
+
+       protected Runnable             selectionChangeListener;
+
+       protected CollectionResult     nodes;
+
+       public NodeTree(Composite parent, Set<Node> selectedNodes) {
+               super(parent, 0);
+
+               this.display = getDisplay();
+               this.selectedNodes = selectedNodes;
+
+               resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
+               noDiagramColor = getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
+
+               GridLayoutFactory.fillDefaults().spacing(20, 10).numColumns(3).applyTo(this);
+
+               createFilter(this);
+               createTree(this);
+               createButtons(this);
+       }
+
+       public void setSelectionChangeListener(Runnable r) {
+               this.selectionChangeListener = r;
+       }
+
+       public void setInput(CollectionResult nodes) {
+               this.nodes = nodes;
+               tree.setInput(nodes);
+               resetFilterString(filter.getText());
+       }
+
+       private Runnable resetFilter = () -> resetFilterString(filter.getText());
+
+       private void createFilter(Composite parent) {
+               Label filterLabel = new Label(parent, SWT.NONE);
+               filterLabel.setText("Fi&lter:");
+               GridDataFactory.fillDefaults().span(1, 1).applyTo(filterLabel);
+               filter = new Text(parent, SWT.BORDER);
+               GridDataFactory.fillDefaults().span(2, 1).applyTo(filter);
+               filter.addModifyListener(e -> display.timerExec(500, resetFilter));
+       }
+
+       private void createTree(Composite parent) {
+               tree = new CheckboxTreeViewer(parent, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
+               tree.setUseHashlookup(true);
+               GridDataFactory.fillDefaults().grab(true, true).span(3, 1).applyTo(tree.getControl());
+               tree.getControl().setToolTipText("Selects the diagrams to include in the exported document.");
+               tree.setAutoExpandLevel(2);
+               tree.addCheckStateListener(new CheckStateListener());
+               tree.setContentProvider(new NodeTreeContentProvider());
+               tree.setLabelProvider(new NodeLabelProvider());
+               tree.setCheckStateProvider(new NodeCheckStateProvider());
+               tree.setComparator(new ViewerComparator(AlphanumComparator.CASE_INSENSITIVE_COMPARATOR));
+               tree.setFilters(new ViewerFilter[] { new NodeFilter() });
+       }
+
+       private void createButtons(Composite parent) {
+               Composite bar = new Composite(parent, SWT.NONE);
+               GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(bar);
+               bar.setLayout(new RowLayout());
+               Button selectAll = new Button(bar, SWT.PUSH);
+               selectAll.setText("Select &All");
+               selectAll.setToolTipText("Select All Visible Diagrams");
+               selectAll.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               selectedNodes.addAll(filter.getText().isEmpty() ? nodes.breadthFirstFlatten(CollectionResult.DIAGRAM_RESOURCE_FILTER) : getVisibleNodes());
+                               refreshTree(true);
+                               fireChangeListener();
+                               scheduleFocusTree();
+                       }
+               });
+               Button clearSelection = new Button(bar, SWT.PUSH);
+               clearSelection.setText("&Deselect All");
+               clearSelection.setToolTipText("Deselect All Visible Diagrams");
+               clearSelection.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               if (filter.getText().isEmpty())
+                                       selectedNodes.clear();
+                               else
+                                       selectedNodes.removeAll(getVisibleNodes());
+                               refreshTree(true);
+                               fireChangeListener();
+                               scheduleFocusTree();
+                       }
+               });
+               Button expand = new Button(bar, SWT.PUSH);
+               expand.setText("&Expand");
+               expand.setToolTipText("Fully Expand Selected Nodes or All Nodes");
+               expand.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               IStructuredSelection ss = tree.getStructuredSelection();
+                               if (ss.isEmpty())
+                                       tree.expandAll();
+                               else
+                                       for (Object n : ss.toList())
+                                               tree.expandToLevel(n, TreeViewer.ALL_LEVELS);
+                               scheduleFocusTree();
+                       }
+               });
+               Button collapse = new Button(bar, SWT.PUSH);
+               collapse.setText("&Collapse");
+               collapse.setToolTipText("Collapse Selected Nodes or All Nodes");
+               collapse.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               IStructuredSelection ss = tree.getStructuredSelection();
+                               if (ss.isEmpty())
+                                       tree.collapseAll();
+                               else
+                                       for (Object n : ss.toList())
+                                               tree.collapseToLevel(n, TreeViewer.ALL_LEVELS);
+                               scheduleFocusTree();
+                       }
+               });
+       }
+
+       protected void fireChangeListener() {
+               if (selectionChangeListener != null)
+                       selectionChangeListener.run();
+       }
+
+       protected void scheduleFocusTree() {
+               display.asyncExec(() -> {
+                       if (!tree.getTree().isDisposed() && !tree.getTree().isFocusControl())
+                               tree.getTree().setFocus();
+               });
+       }
+
+       private Collection<Node> getVisibleNodes() {
+               Collection<Node> result = new ArrayList<>();
+
+               Deque<TreeItem> todo = new ArrayDeque<>();
+               for (TreeItem ti : tree.getTree().getItems()) {
+                       todo.add(ti);
+               }
+
+               while (!todo.isEmpty()) {
+                       TreeItem item = todo.removeLast();
+                       Node node = (Node) item.getData();
+                       if (node != null)
+                               result.add(node);
+
+                       for (TreeItem child : item.getItems()) {
+                               todo.add(child);
+                       }
+               }
+
+               return result;
+       }
+
+       private void resetFilterString(String filterString) {
+               TreePath[] restoreExpansions = null;
+               String patternString = filterStrategy.toPatternString(filterString);
+               if (patternString == null) {
+                       if (matcher != null) {
+                               // Filter has been removed
+                               restoreExpansions = noFilterExpandedPaths;
+                               noFilterExpandedPaths = null;
+                       }
+                       matcher = null;
+               } else {
+                       if (matcher == null) {
+                               // Filter has been defined after not being previously defined
+                               noFilterExpandedPaths = tree.getExpandedTreePaths();
+                       }
+                       matcher = Pattern.compile(patternString).matcher("");
+               }
+               refreshTree(false);
+               if (restoreExpansions != null)
+                       tree.setExpandedTreePaths(restoreExpansions);
+               else
+                       tree.expandAll();
+       }
+
+       protected static boolean hasDiagram(Node n) {
+               return n.getDiagramResource() != null;
+       }
+
+       protected static boolean hasDiagramDeep(Node n) {
+               if (hasDiagram(n))
+                       return true;
+               for (Node c : n.getChildren())
+                       if (hasDiagramDeep(c))
+                               return true;
+               return false;
+       }
+
+       protected boolean isSomethingSelected(Node node) {
+               if (selectedNodes.contains(node))
+                       return true;
+
+               Collection<Node> children = node.getChildren();
+               if (!children.isEmpty()) {
+                       for (Node child : children) {
+                               if (!hasDiagramDeep(child))
+                                       continue;
+                               if (isSomethingSelected(child))
+                                       return true;
+                       }
+               }
+               return false;
+       }
+
+       protected boolean isFullySelected(Node node) {
+               if (selectedNodes.contains(node))
+                       return true;
+
+               int selectedCount = 0;
+               boolean allSelected = true;
+               Collection<Node> children = node.getChildren();
+               if (!children.isEmpty()) {
+                       for (Node child : children) {
+                               if (!hasDiagramDeep(child))
+                                       continue;
+                               boolean selected = isFullySelected(child);
+                               allSelected &= selected;
+                               selectedCount += selected ? 1 : 0;
+                               //System.out.println("\tisFullySelected: test child: " + child + " : " + selected + " => " + allSelected);
+                               if (!selected)
+                                       break;
+                       }
+               }
+               //System.out.println("isFullySelected(" + node + "): " + allSelected + ", " + selectedCount);
+               return allSelected && selectedCount > 0;
+       }
+
+       protected boolean isPartiallySelected(Node node) {
+               return !selectedNodes.contains(node) && isSomethingSelected(node) && !isFullySelected(node);
+       }
+
+       protected void refreshTree(boolean invalidateCheckStateCache) {
+               if (invalidateCheckStateCache)
+                       checkStateCache.invalidate();
+               tree.refresh();
+       }
+
+       public void refreshTree() {
+               refreshTree(true);
+       }
+
+       public boolean addOrRemoveSelection(Node node, boolean add) {
+               boolean changed = false;
+               if (hasDiagram(node)) {
+                       if (add)
+                               changed = selectedNodes.add(node);
+                       else
+                               changed = selectedNodes.remove(node);
+                       if (changed)
+                               checkStateCache.invalidate(node);
+               }
+               return changed;
+       }
+
+       public boolean addOrRemoveSelectionRec(Node node, boolean add) {
+               boolean changed = false;
+               changed |= addOrRemoveSelection(node, add);
+               for (Node child : node.getChildren())
+                       changed |= addOrRemoveSelectionRec(child, add);
+               return changed;
+       }
+
+       private static class NodeTreeContentProvider implements ITreeContentProvider {
+               @Override
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+               @Override
+               public void dispose() {
+               }
+               @Override
+               public Object[] getElements(Object inputElement) {
+                       if (inputElement instanceof CollectionResult)
+                               return ((CollectionResult) inputElement).roots.toArray();
+                       return new Object[0];
+               }
+               @Override
+               public boolean hasChildren(Object element) {
+                       Node n = (Node) element;
+                       Collection<Node> children = n.getChildren();
+                       if (children.isEmpty()) 
+                               return false;
+                       for (Node c : children)
+                               if (hasDiagramDeep(c))
+                                       return true;
+                       return false;
+
+               }
+               @Override
+               public Object getParent(Object element) {
+                       return ((Node) element).getParent();
+               }
+               @Override
+               public Object[] getChildren(Object parentElement) {
+                       Node n = (Node) parentElement;
+                       List<Object> result = new ArrayList<>( n.getChildren().size() );
+                       for (Node c : n.getChildren()) 
+                               if (hasDiagramDeep(c)) 
+                                       result.add(c);
+                       return result.toArray();
+               }
+       }
+
+       private class NodeLabelProvider extends CellLabelProvider {
+               @Override
+               public void update(ViewerCell cell) {
+                       Object e = cell.getElement();
+                       if (e instanceof Node) {
+                               Node n = (Node) e;
+                               String name = DiagramPrinter.formDiagramName(n, false);
+                               cell.setText(name);
+
+                               if (n.getDiagramResource() == null)
+                                       cell.setForeground(noDiagramColor);
+                               else
+                                       cell.setForeground(null);
+                       } else {
+                               cell.setText("invalid input: " + e.getClass().getSimpleName());
+                       }
+               }
+       }
+
+       private class CheckStateListener implements ICheckStateListener {
+               @Override
+               public void checkStateChanged(CheckStateChangedEvent event) {
+                       final boolean checked = event.getChecked();
+                       Node checkedNode = (Node) event.getElement();
+
+                       Set<Node> nodes = new HashSet<>();
+                       Set<Node> selection = ISelectionUtils.filterSetSelection(tree.getSelection(), Node.class);
+                       if (selection.contains(checkedNode))
+                               nodes.addAll(selection);
+                       else
+                               tree.setSelection(StructuredSelection.EMPTY);
+                       nodes.add(checkedNode);
+
+                       for (Node node : nodes)
+                               addOrRemoveSelectionRec(node, checked);
+
+                       tree.refresh();
+                       fireChangeListener();
+               }
+       }
+
+       private class NodeCheckStateProvider implements ICheckStateProvider {
+               @Override
+               public boolean isChecked(Object element) {
+                       Node n = (Node) element;
+                       Boolean cache = checkStateCache.isChecked.get(n);
+                       if (cache != null)
+                               return cache;
+                       boolean checked = isSomethingSelected(n);
+                       checkStateCache.isChecked.put(n, checked);
+                       return checked;
+               }
+               @Override
+               public boolean isGrayed(Object element) {
+                       Node n = (Node) element;
+                       Boolean cache = checkStateCache.isGrayed.get(n);
+                       if (cache != null)
+                               return cache;
+                       boolean grayed = n.getDiagramResource() == null && isPartiallySelected(n);
+                       checkStateCache.isGrayed.put(n, grayed);
+                       return grayed;
+               }
+       }
+
+       private class NodeFilter extends ViewerFilter {
+               @Override
+               public boolean select(Viewer viewer, Object parentElement, Object element) {
+                       if (matcher == null)
+                               return true;
+
+                       Node node = (Node) element;
+                       boolean matches = matcher.reset(node.getName().toLowerCase()).matches();
+                       if (matches)
+                               return true;
+
+                       // If any children are in sight, show this element.
+                       for (Node child : node.getChildren())
+                               if (select(viewer, element, child))
+                                       return true;
+
+                       return false;
+               }
+       }
+
+}
\ No newline at end of file