1 /*******************************************************************************
2 * Copyright (c) 2017 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
11 *******************************************************************************/
12 package org.simantics.modeling.ui.pdf;
14 import java.util.ArrayDeque;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Deque;
18 import java.util.HashMap;
19 import java.util.HashSet;
20 import java.util.List;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
26 import org.eclipse.jface.layout.GridDataFactory;
27 import org.eclipse.jface.layout.GridLayoutFactory;
28 import org.eclipse.jface.resource.JFaceResources;
29 import org.eclipse.jface.resource.LocalResourceManager;
30 import org.eclipse.jface.viewers.CellLabelProvider;
31 import org.eclipse.jface.viewers.CheckStateChangedEvent;
32 import org.eclipse.jface.viewers.CheckboxTreeViewer;
33 import org.eclipse.jface.viewers.ICheckStateListener;
34 import org.eclipse.jface.viewers.ICheckStateProvider;
35 import org.eclipse.jface.viewers.IStructuredSelection;
36 import org.eclipse.jface.viewers.ITreeContentProvider;
37 import org.eclipse.jface.viewers.StructuredSelection;
38 import org.eclipse.jface.viewers.TreePath;
39 import org.eclipse.jface.viewers.TreeViewer;
40 import org.eclipse.jface.viewers.Viewer;
41 import org.eclipse.jface.viewers.ViewerCell;
42 import org.eclipse.jface.viewers.ViewerComparator;
43 import org.eclipse.jface.viewers.ViewerFilter;
44 import org.eclipse.swt.SWT;
45 import org.eclipse.swt.events.SelectionAdapter;
46 import org.eclipse.swt.events.SelectionEvent;
47 import org.eclipse.swt.graphics.Color;
48 import org.eclipse.swt.layout.RowLayout;
49 import org.eclipse.swt.widgets.Button;
50 import org.eclipse.swt.widgets.Composite;
51 import org.eclipse.swt.widgets.Display;
52 import org.eclipse.swt.widgets.Label;
53 import org.eclipse.swt.widgets.Text;
54 import org.eclipse.swt.widgets.TreeItem;
55 import org.simantics.browsing.ui.common.views.DefaultFilterStrategy;
56 import org.simantics.browsing.ui.common.views.IFilterStrategy;
57 import org.simantics.modeling.requests.CollectionResult;
58 import org.simantics.modeling.requests.Node;
59 import org.simantics.utils.strings.AlphanumComparator;
60 import org.simantics.utils.ui.ISelectionUtils;
63 * A tree of nodes intended for usable listing and selecting diagrams.
65 * @author Tuukka Lehtonen
68 public class NodeTree extends Composite {
71 * This exists to make {@link NodeCheckStateProvider} faster
73 private static class CheckStateCache {
74 Map<Node, Boolean> isChecked = new HashMap<>();
75 Map<Node, Boolean> isGrayed = new HashMap<>();
77 public void invalidate(Node n) {
78 for (; n != null; n = n.getParent()) {
83 public void invalidate() {
89 protected Display display;
91 protected LocalResourceManager resourceManager;
93 protected Color noDiagramColor;
95 protected IFilterStrategy filterStrategy = new DefaultFilterStrategy();
97 protected Text filter;
99 protected Matcher matcher = null;
101 protected CheckboxTreeViewer tree;
104 * The tree paths that were expanded last time no filter was defined. Will
105 * be nullified after the expanded paths have been returned when
106 * {@link #matcher} turns null.
108 protected TreePath[] noFilterExpandedPaths;
110 protected Set<Node> selectedNodes;
112 protected CheckStateCache checkStateCache = new CheckStateCache();
114 protected Runnable selectionChangeListener;
116 protected CollectionResult nodes;
118 public NodeTree(Composite parent, Set<Node> selectedNodes) {
121 this.display = getDisplay();
122 this.selectedNodes = selectedNodes;
124 resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
125 noDiagramColor = getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
127 GridLayoutFactory.fillDefaults().spacing(20, 10).numColumns(3).applyTo(this);
134 public void setSelectionChangeListener(Runnable r) {
135 this.selectionChangeListener = r;
138 public void setInput(CollectionResult nodes) {
140 tree.setInput(nodes);
141 resetFilterString(filter.getText());
144 private Runnable resetFilter = () -> resetFilterString(filter.getText());
146 private void createFilter(Composite parent) {
147 Label filterLabel = new Label(parent, SWT.NONE);
148 filterLabel.setText("Fi<er:");
149 GridDataFactory.fillDefaults().span(1, 1).applyTo(filterLabel);
150 filter = new Text(parent, SWT.BORDER);
151 GridDataFactory.fillDefaults().span(2, 1).applyTo(filter);
152 filter.addModifyListener(e -> display.timerExec(500, resetFilter));
155 private void createTree(Composite parent) {
156 tree = new CheckboxTreeViewer(parent, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
157 tree.setUseHashlookup(true);
158 GridDataFactory.fillDefaults().grab(true, true).span(3, 1).applyTo(tree.getControl());
159 tree.getControl().setToolTipText("Selects the diagrams to include in the exported document.");
160 tree.setAutoExpandLevel(2);
161 tree.addCheckStateListener(new CheckStateListener());
162 tree.setContentProvider(new NodeTreeContentProvider());
163 tree.setLabelProvider(new NodeLabelProvider());
164 tree.setCheckStateProvider(new NodeCheckStateProvider());
165 tree.setComparator(new ViewerComparator(AlphanumComparator.CASE_INSENSITIVE_COMPARATOR));
166 tree.setFilters(new ViewerFilter[] { new NodeFilter() });
169 private void createButtons(Composite parent) {
170 Composite bar = new Composite(parent, SWT.NONE);
171 GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo(bar);
172 bar.setLayout(new RowLayout());
173 Button selectAll = new Button(bar, SWT.PUSH);
174 selectAll.setText("Select &All");
175 selectAll.setToolTipText("Select All Visible Diagrams");
176 selectAll.addSelectionListener(new SelectionAdapter() {
178 public void widgetSelected(SelectionEvent e) {
179 selectedNodes.addAll(filter.getText().isEmpty() ? nodes.breadthFirstFlatten(CollectionResult.DIAGRAM_RESOURCE_FILTER) : getVisibleNodes());
181 fireChangeListener();
185 Button clearSelection = new Button(bar, SWT.PUSH);
186 clearSelection.setText("&Deselect All");
187 clearSelection.setToolTipText("Deselect All Visible Diagrams");
188 clearSelection.addSelectionListener(new SelectionAdapter() {
190 public void widgetSelected(SelectionEvent e) {
191 if (filter.getText().isEmpty())
192 selectedNodes.clear();
194 selectedNodes.removeAll(getVisibleNodes());
196 fireChangeListener();
200 Button expand = new Button(bar, SWT.PUSH);
201 expand.setText("&Expand");
202 expand.setToolTipText("Fully Expand Selected Nodes or All Nodes");
203 expand.addSelectionListener(new SelectionAdapter() {
205 public void widgetSelected(SelectionEvent e) {
206 IStructuredSelection ss = tree.getStructuredSelection();
210 for (Object n : ss.toList())
211 tree.expandToLevel(n, TreeViewer.ALL_LEVELS);
215 Button collapse = new Button(bar, SWT.PUSH);
216 collapse.setText("&Collapse");
217 collapse.setToolTipText("Collapse Selected Nodes or All Nodes");
218 collapse.addSelectionListener(new SelectionAdapter() {
220 public void widgetSelected(SelectionEvent e) {
221 IStructuredSelection ss = tree.getStructuredSelection();
225 for (Object n : ss.toList())
226 tree.collapseToLevel(n, TreeViewer.ALL_LEVELS);
232 protected void fireChangeListener() {
233 if (selectionChangeListener != null)
234 selectionChangeListener.run();
237 protected void scheduleFocusTree() {
238 display.asyncExec(() -> {
239 if (!tree.getTree().isDisposed() && !tree.getTree().isFocusControl())
240 tree.getTree().setFocus();
244 private Collection<Node> getVisibleNodes() {
245 Collection<Node> result = new ArrayList<>();
247 Deque<TreeItem> todo = new ArrayDeque<>();
248 for (TreeItem ti : tree.getTree().getItems()) {
252 while (!todo.isEmpty()) {
253 TreeItem item = todo.removeLast();
254 Node node = (Node) item.getData();
258 for (TreeItem child : item.getItems()) {
266 private void resetFilterString(String filterString) {
267 TreePath[] restoreExpansions = null;
268 String patternString = filterStrategy.toPatternString(filterString);
269 if (patternString == null) {
270 if (matcher != null) {
271 // Filter has been removed
272 restoreExpansions = noFilterExpandedPaths;
273 noFilterExpandedPaths = null;
277 if (matcher == null) {
278 // Filter has been defined after not being previously defined
279 noFilterExpandedPaths = tree.getExpandedTreePaths();
281 matcher = Pattern.compile(patternString).matcher("");
284 if (restoreExpansions != null)
285 tree.setExpandedTreePaths(restoreExpansions);
290 protected static boolean hasDiagram(Node n) {
291 return n.getDiagramResource() != null;
294 protected static boolean hasDiagramDeep(Node n) {
297 for (Node c : n.getChildren())
298 if (hasDiagramDeep(c))
303 protected boolean isSomethingSelected(Node node) {
304 if (selectedNodes.contains(node))
307 Collection<Node> children = node.getChildren();
308 if (!children.isEmpty()) {
309 for (Node child : children) {
310 if (!hasDiagramDeep(child))
312 if (isSomethingSelected(child))
319 protected boolean isFullySelected(Node node) {
320 if (selectedNodes.contains(node))
323 int selectedCount = 0;
324 boolean allSelected = true;
325 Collection<Node> children = node.getChildren();
326 if (!children.isEmpty()) {
327 for (Node child : children) {
328 if (!hasDiagramDeep(child))
330 boolean selected = isFullySelected(child);
331 allSelected &= selected;
332 selectedCount += selected ? 1 : 0;
333 //System.out.println("\tisFullySelected: test child: " + child + " : " + selected + " => " + allSelected);
338 //System.out.println("isFullySelected(" + node + "): " + allSelected + ", " + selectedCount);
339 return allSelected && selectedCount > 0;
342 protected boolean isPartiallySelected(Node node) {
343 return !selectedNodes.contains(node) && isSomethingSelected(node) && !isFullySelected(node);
346 protected void refreshTree(boolean invalidateCheckStateCache) {
347 if (invalidateCheckStateCache)
348 checkStateCache.invalidate();
352 public void refreshTree() {
356 public boolean addOrRemoveSelection(Node node, boolean add) {
357 boolean changed = false;
358 if (hasDiagram(node)) {
360 changed = selectedNodes.add(node);
362 changed = selectedNodes.remove(node);
364 checkStateCache.invalidate(node);
369 public boolean addOrRemoveSelectionRec(Node node, boolean add) {
370 boolean changed = false;
371 changed |= addOrRemoveSelection(node, add);
372 for (Node child : node.getChildren())
373 changed |= addOrRemoveSelectionRec(child, add);
377 private static class NodeTreeContentProvider implements ITreeContentProvider {
379 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
382 public void dispose() {
385 public Object[] getElements(Object inputElement) {
386 if (inputElement instanceof CollectionResult)
387 return ((CollectionResult) inputElement).roots.toArray();
388 return new Object[0];
391 public boolean hasChildren(Object element) {
392 Node n = (Node) element;
393 Collection<Node> children = n.getChildren();
394 if (children.isEmpty())
396 for (Node c : children)
397 if (hasDiagramDeep(c))
403 public Object getParent(Object element) {
404 return ((Node) element).getParent();
407 public Object[] getChildren(Object parentElement) {
408 Node n = (Node) parentElement;
409 List<Object> result = new ArrayList<>( n.getChildren().size() );
410 for (Node c : n.getChildren())
411 if (hasDiagramDeep(c))
413 return result.toArray();
417 private class NodeLabelProvider extends CellLabelProvider {
419 public void update(ViewerCell cell) {
420 Object e = cell.getElement();
421 if (e instanceof Node) {
423 String name = DiagramPrinter.formDiagramName(n, false);
426 if (n.getDiagramResource() == null)
427 cell.setForeground(noDiagramColor);
429 cell.setForeground(null);
431 cell.setText("invalid input: " + e.getClass().getSimpleName());
436 private class CheckStateListener implements ICheckStateListener {
438 public void checkStateChanged(CheckStateChangedEvent event) {
439 final boolean checked = event.getChecked();
440 Node checkedNode = (Node) event.getElement();
442 Set<Node> nodes = new HashSet<>();
443 Set<Node> selection = ISelectionUtils.filterSetSelection(tree.getSelection(), Node.class);
444 if (selection.contains(checkedNode))
445 nodes.addAll(selection);
447 tree.setSelection(StructuredSelection.EMPTY);
448 nodes.add(checkedNode);
450 for (Node node : nodes)
451 addOrRemoveSelectionRec(node, checked);
454 fireChangeListener();
458 private class NodeCheckStateProvider implements ICheckStateProvider {
460 public boolean isChecked(Object element) {
461 Node n = (Node) element;
462 Boolean cache = checkStateCache.isChecked.get(n);
465 boolean checked = isSomethingSelected(n);
466 checkStateCache.isChecked.put(n, checked);
470 public boolean isGrayed(Object element) {
471 Node n = (Node) element;
472 Boolean cache = checkStateCache.isGrayed.get(n);
475 boolean grayed = n.getDiagramResource() == null && isPartiallySelected(n);
476 checkStateCache.isGrayed.put(n, grayed);
481 private class NodeFilter extends ViewerFilter {
483 public boolean select(Viewer viewer, Object parentElement, Object element) {
487 Node node = (Node) element;
488 boolean matches = matcher.reset(node.getName().toLowerCase()).matches();
492 // If any children are in sight, show this element.
493 for (Node child : node.getChildren())
494 if (select(viewer, element, child))