/******************************************************************************* * 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; } } }