X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.modeling.ui%2Fsrc%2Forg%2Fsimantics%2Fmodeling%2Fui%2Fpdf%2FNodeTree.java;fp=bundles%2Forg.simantics.modeling.ui%2Fsrc%2Forg%2Fsimantics%2Fmodeling%2Fui%2Fpdf%2FNodeTree.java;h=a17aafb4a76828efffc3e73afb494c49a516a5e1;hb=83b58da86c173e771f7083778799b79b2fb152c8;hp=0000000000000000000000000000000000000000;hpb=75809ec96a58c23cc89131637fbc65435ac482af;p=simantics%2Fplatform.git 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 index 000000000..a17aafb4a --- /dev/null +++ b/bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/NodeTree.java @@ -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 isChecked = new HashMap<>(); + Map 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 selectedNodes; + + protected CheckStateCache checkStateCache = new CheckStateCache(); + + protected Runnable selectionChangeListener; + + protected CollectionResult nodes; + + public NodeTree(Composite parent, Set 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 getVisibleNodes() { + Collection result = new ArrayList<>(); + + Deque 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 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 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 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 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 nodes = new HashSet<>(); + Set 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