]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/NodeTree.java
Merge "Default property editing restores assertions"
[simantics/platform.git] / bundles / org.simantics.modeling.ui / src / org / simantics / modeling / ui / pdf / NodeTree.java
1 /*******************************************************************************
2  * Copyright (c) 2017 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     Semantum Oy - #7297
11  *******************************************************************************/
12 package org.simantics.modeling.ui.pdf;
13
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;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
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;
61
62 /**
63  * A tree of nodes intended for usable listing and selecting diagrams.
64  * 
65  * @author Tuukka Lehtonen
66  * @since 1.30.0
67  */
68 public class NodeTree extends Composite {
69
70         /**
71          * This exists to make {@link NodeCheckStateProvider} faster
72          */
73         private static class CheckStateCache {
74                 Map<Node, Boolean> isChecked = new HashMap<>();
75                 Map<Node, Boolean> isGrayed = new HashMap<>();
76
77                 public void invalidate(Node n) {
78                         for (; n != null; n = n.getParent()) {
79                                 isChecked.remove(n);
80                                 isGrayed.remove(n);
81                         }
82                 }
83                 public void invalidate() {
84                         isChecked.clear();
85                         isGrayed.clear();
86                 }
87         }
88
89         protected Display              display;
90
91         protected LocalResourceManager resourceManager;
92
93         protected Color                noDiagramColor;
94
95         protected IFilterStrategy      filterStrategy     = new DefaultFilterStrategy();
96
97         protected Text                 filter;
98
99         protected Matcher              matcher            = null;
100
101         protected CheckboxTreeViewer   tree;
102
103         /**
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.
107          */
108         protected TreePath[]           noFilterExpandedPaths;
109
110         protected Set<Node>            selectedNodes;
111
112         protected CheckStateCache      checkStateCache = new CheckStateCache();
113
114         protected Runnable             selectionChangeListener;
115
116         protected CollectionResult     nodes;
117
118         public NodeTree(Composite parent, Set<Node> selectedNodes) {
119                 super(parent, 0);
120
121                 this.display = getDisplay();
122                 this.selectedNodes = selectedNodes;
123
124                 resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
125                 noDiagramColor = getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
126
127                 GridLayoutFactory.fillDefaults().spacing(20, 10).numColumns(3).applyTo(this);
128
129                 createFilter(this);
130                 createTree(this);
131                 createButtons(this);
132         }
133
134         public void setSelectionChangeListener(Runnable r) {
135                 this.selectionChangeListener = r;
136         }
137
138         public void setInput(CollectionResult nodes) {
139                 this.nodes = nodes;
140                 tree.setInput(nodes);
141                 resetFilterString(filter.getText());
142         }
143
144         private Runnable resetFilter = () -> resetFilterString(filter.getText());
145
146         private void createFilter(Composite parent) {
147                 Label filterLabel = new Label(parent, SWT.NONE);
148                 filterLabel.setText("Fi&lter:");
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));
153         }
154
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() });
167         }
168
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() {
177                         @Override
178                         public void widgetSelected(SelectionEvent e) {
179                                 selectedNodes.addAll(filter.getText().isEmpty() ? nodes.breadthFirstFlatten(CollectionResult.DIAGRAM_RESOURCE_FILTER) : getVisibleNodes());
180                                 refreshTree(true);
181                                 fireChangeListener();
182                                 scheduleFocusTree();
183                         }
184                 });
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() {
189                         @Override
190                         public void widgetSelected(SelectionEvent e) {
191                                 if (filter.getText().isEmpty())
192                                         selectedNodes.clear();
193                                 else
194                                         selectedNodes.removeAll(getVisibleNodes());
195                                 refreshTree(true);
196                                 fireChangeListener();
197                                 scheduleFocusTree();
198                         }
199                 });
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() {
204                         @Override
205                         public void widgetSelected(SelectionEvent e) {
206                                 IStructuredSelection ss = tree.getStructuredSelection();
207                                 if (ss.isEmpty())
208                                         tree.expandAll();
209                                 else
210                                         for (Object n : ss.toList())
211                                                 tree.expandToLevel(n, TreeViewer.ALL_LEVELS);
212                                 scheduleFocusTree();
213                         }
214                 });
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() {
219                         @Override
220                         public void widgetSelected(SelectionEvent e) {
221                                 IStructuredSelection ss = tree.getStructuredSelection();
222                                 if (ss.isEmpty())
223                                         tree.collapseAll();
224                                 else
225                                         for (Object n : ss.toList())
226                                                 tree.collapseToLevel(n, TreeViewer.ALL_LEVELS);
227                                 scheduleFocusTree();
228                         }
229                 });
230         }
231
232         protected void fireChangeListener() {
233                 if (selectionChangeListener != null)
234                         selectionChangeListener.run();
235         }
236
237         protected void scheduleFocusTree() {
238                 display.asyncExec(() -> {
239                         if (!tree.getTree().isDisposed() && !tree.getTree().isFocusControl())
240                                 tree.getTree().setFocus();
241                 });
242         }
243
244         private Collection<Node> getVisibleNodes() {
245                 Collection<Node> result = new ArrayList<>();
246
247                 Deque<TreeItem> todo = new ArrayDeque<>();
248                 for (TreeItem ti : tree.getTree().getItems()) {
249                         todo.add(ti);
250                 }
251
252                 while (!todo.isEmpty()) {
253                         TreeItem item = todo.removeLast();
254                         Node node = (Node) item.getData();
255                         if (node != null)
256                                 result.add(node);
257
258                         for (TreeItem child : item.getItems()) {
259                                 todo.add(child);
260                         }
261                 }
262
263                 return result;
264         }
265
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;
274                         }
275                         matcher = null;
276                 } else {
277                         if (matcher == null) {
278                                 // Filter has been defined after not being previously defined
279                                 noFilterExpandedPaths = tree.getExpandedTreePaths();
280                         }
281                         matcher = Pattern.compile(patternString).matcher("");
282                 }
283                 refreshTree(false);
284                 if (restoreExpansions != null)
285                         tree.setExpandedTreePaths(restoreExpansions);
286                 else
287                         tree.expandAll();
288         }
289
290         protected static boolean hasDiagram(Node n) {
291                 return n.getDiagramResource() != null;
292         }
293
294         protected static boolean hasDiagramDeep(Node n) {
295                 if (hasDiagram(n))
296                         return true;
297                 for (Node c : n.getChildren())
298                         if (hasDiagramDeep(c))
299                                 return true;
300                 return false;
301         }
302
303         protected boolean isSomethingSelected(Node node) {
304                 if (selectedNodes.contains(node))
305                         return true;
306
307                 Collection<Node> children = node.getChildren();
308                 if (!children.isEmpty()) {
309                         for (Node child : children) {
310                                 if (!hasDiagramDeep(child))
311                                         continue;
312                                 if (isSomethingSelected(child))
313                                         return true;
314                         }
315                 }
316                 return false;
317         }
318
319         protected boolean isFullySelected(Node node) {
320                 if (selectedNodes.contains(node))
321                         return true;
322
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))
329                                         continue;
330                                 boolean selected = isFullySelected(child);
331                                 allSelected &= selected;
332                                 selectedCount += selected ? 1 : 0;
333                                 //System.out.println("\tisFullySelected: test child: " + child + " : " + selected + " => " + allSelected);
334                                 if (!selected)
335                                         break;
336                         }
337                 }
338                 //System.out.println("isFullySelected(" + node + "): " + allSelected + ", " + selectedCount);
339                 return allSelected && selectedCount > 0;
340         }
341
342         protected boolean isPartiallySelected(Node node) {
343                 return !selectedNodes.contains(node) && isSomethingSelected(node) && !isFullySelected(node);
344         }
345
346         protected void refreshTree(boolean invalidateCheckStateCache) {
347                 if (invalidateCheckStateCache)
348                         checkStateCache.invalidate();
349                 tree.refresh();
350         }
351
352         public void refreshTree() {
353                 refreshTree(true);
354         }
355
356         public boolean addOrRemoveSelection(Node node, boolean add) {
357                 boolean changed = false;
358                 if (hasDiagram(node)) {
359                         if (add)
360                                 changed = selectedNodes.add(node);
361                         else
362                                 changed = selectedNodes.remove(node);
363                         if (changed)
364                                 checkStateCache.invalidate(node);
365                 }
366                 return changed;
367         }
368
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);
374                 return changed;
375         }
376
377         private static class NodeTreeContentProvider implements ITreeContentProvider {
378                 @Override
379                 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
380                 }
381                 @Override
382                 public void dispose() {
383                 }
384                 @Override
385                 public Object[] getElements(Object inputElement) {
386                         if (inputElement instanceof CollectionResult)
387                                 return ((CollectionResult) inputElement).roots.toArray();
388                         return new Object[0];
389                 }
390                 @Override
391                 public boolean hasChildren(Object element) {
392                         Node n = (Node) element;
393                         Collection<Node> children = n.getChildren();
394                         if (children.isEmpty()) 
395                                 return false;
396                         for (Node c : children)
397                                 if (hasDiagramDeep(c))
398                                         return true;
399                         return false;
400
401                 }
402                 @Override
403                 public Object getParent(Object element) {
404                         return ((Node) element).getParent();
405                 }
406                 @Override
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)) 
412                                         result.add(c);
413                         return result.toArray();
414                 }
415         }
416
417         private class NodeLabelProvider extends CellLabelProvider {
418                 @Override
419                 public void update(ViewerCell cell) {
420                         Object e = cell.getElement();
421                         if (e instanceof Node) {
422                                 Node n = (Node) e;
423                                 String name = DiagramPrinter.formDiagramName(n, false);
424                                 cell.setText(name);
425
426                                 if (n.getDiagramResource() == null)
427                                         cell.setForeground(noDiagramColor);
428                                 else
429                                         cell.setForeground(null);
430                         } else {
431                                 cell.setText("invalid input: " + e.getClass().getSimpleName());
432                         }
433                 }
434         }
435
436         private class CheckStateListener implements ICheckStateListener {
437                 @Override
438                 public void checkStateChanged(CheckStateChangedEvent event) {
439                         final boolean checked = event.getChecked();
440                         Node checkedNode = (Node) event.getElement();
441
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);
446                         else
447                                 tree.setSelection(StructuredSelection.EMPTY);
448                         nodes.add(checkedNode);
449
450                         for (Node node : nodes)
451                                 addOrRemoveSelectionRec(node, checked);
452
453                         tree.refresh();
454                         fireChangeListener();
455                 }
456         }
457
458         private class NodeCheckStateProvider implements ICheckStateProvider {
459                 @Override
460                 public boolean isChecked(Object element) {
461                         Node n = (Node) element;
462                         Boolean cache = checkStateCache.isChecked.get(n);
463                         if (cache != null)
464                                 return cache;
465                         boolean checked = isSomethingSelected(n);
466                         checkStateCache.isChecked.put(n, checked);
467                         return checked;
468                 }
469                 @Override
470                 public boolean isGrayed(Object element) {
471                         Node n = (Node) element;
472                         Boolean cache = checkStateCache.isGrayed.get(n);
473                         if (cache != null)
474                                 return cache;
475                         boolean grayed = n.getDiagramResource() == null && isPartiallySelected(n);
476                         checkStateCache.isGrayed.put(n, grayed);
477                         return grayed;
478                 }
479         }
480
481         private class NodeFilter extends ViewerFilter {
482                 @Override
483                 public boolean select(Viewer viewer, Object parentElement, Object element) {
484                         if (matcher == null)
485                                 return true;
486
487                         Node node = (Node) element;
488                         boolean matches = matcher.reset(node.getName().toLowerCase()).matches();
489                         if (matches)
490                                 return true;
491
492                         // If any children are in sight, show this element.
493                         for (Node child : node.getChildren())
494                                 if (select(viewer, element, child))
495                                         return true;
496
497                         return false;
498                 }
499         }
500
501 }