--- /dev/null
+/*******************************************************************************
+ * 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<er:");
+ 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