]> gerrit.simantics Code Review - simantics/platform.git/commitdiff
Improved PDF diagram export wizard user experience 26/626/4
authorTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Wed, 14 Jun 2017 16:45:36 +0000 (19:45 +0300)
committerTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Wed, 14 Jun 2017 17:24:08 +0000 (20:24 +0300)
* Page number addition is now an option in the wizard page
* The diagram selection tree no longer automatically expands itself
  fully when any node is (un)checked. Automatic expansion is only done
  up to the second node level only.
* The diagram filter now works with a 500ms quiet time delay which means
  writing in the filter shouldn't slow down the UI like it used to.
* Initial selection is more properly taken into account when
  initializing the wizard page. The containing model/index root is
  always sought based on the selection and the diagrams contained by the
  selected resource are then initially selected for that model in the
  diagram list.
* The Select/Deselect Visible buttons have been removed and Expand and
  Collapse buttons have been added instead for expanding and collapsing
  the selected tree nodes.
* The Select All and Deselect All buttons now work differently. If any
  filter is applied to the current tree view, only the diagrams shown in
  the tree are selected/deselected when either button is pressed. If no
  filter is applied, all diagrams will be selected/deselected.

refs #7297

Change-Id: I078a80528ba91c337f9921422d08a75c95cad45f

bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/NodeTree.java [new file with mode: 0644]
bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/PDFDiagramExportWizard.java
bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/PDFExportPage.java
bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/PDFExportPlan.java
bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/pdf/Preferences.java
bundles/org.simantics.modeling/src/org/simantics/modeling/requests/CollectionRequest.java
bundles/org.simantics.modeling/src/org/simantics/modeling/requests/CollectionResult.java
bundles/org.simantics.modeling/src/org/simantics/modeling/requests/Node.java
bundles/org.simantics.modeling/src/org/simantics/modeling/requests/Nodes.java

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 (file)
index 0000000..a17aafb
--- /dev/null
@@ -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<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&lter:");
+               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
index f0c9cddb32514610ec850b0bb9b30919e5657516..461e98c10b1d10d4895230ec1b9e9fe7df85e9b7 100644 (file)
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * Copyright (c) 2007, 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
@@ -8,6 +8,7 @@
  *
  * Contributors:
  *     VTT Technical Research Centre of Finland - initial API and implementation
+ *     Semantum Oy - initial selection handling improvements
  *******************************************************************************/
 package org.simantics.modeling.ui.pdf;
 
@@ -17,16 +18,15 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
 
-import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.preferences.InstanceScope;
 import org.eclipse.jface.dialogs.MessageDialog;
-import org.eclipse.jface.operation.IRunnableWithProgress;
 import org.eclipse.jface.preference.IPersistentPreferenceStore;
 import org.eclipse.jface.preference.IPreferenceStore;
 import org.eclipse.jface.viewers.IFilter;
@@ -36,15 +36,16 @@ import org.eclipse.ui.IExportWizard;
 import org.eclipse.ui.IMemento;
 import org.eclipse.ui.IWorkbench;
 import org.eclipse.ui.preferences.ScopedPreferenceStore;
+import org.simantics.NameLabelMode;
+import org.simantics.NameLabelUtil;
 import org.simantics.Simantics;
-import org.simantics.browsing.ui.graph.impl.request.GetName;
 import org.simantics.db.ReadGraph;
 import org.simantics.db.Resource;
 import org.simantics.db.common.NamedResource;
 import org.simantics.db.common.request.ObjectsWithType;
+import org.simantics.db.common.request.PossibleIndexRoot;
 import org.simantics.db.common.request.ReadRequest;
 import org.simantics.db.exception.DatabaseException;
-import org.simantics.db.layer0.request.ActiveModels;
 import org.simantics.db.management.ISessionContext;
 import org.simantics.layer0.Layer0;
 import org.simantics.modeling.requests.Node;
@@ -56,12 +57,17 @@ import org.simantics.simulation.ontology.SimulationResource;
 import org.simantics.ui.SimanticsUI;
 import org.simantics.ui.utils.ResourceAdaptionUtils;
 import org.simantics.utils.FileUtils;
+import org.simantics.utils.strings.AlphanumComparator;
 import org.simantics.utils.ui.ErrorLogger;
 import org.simantics.utils.ui.ExceptionUtils;
 import org.simantics.utils.ui.workbench.StringMemento;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(PDFDiagramExportWizard.class);
+
     private static final int    MAX_RECENT_EXPORT_PATHS = 10;
 
     private static final String TAG_PATH  = "path";
@@ -70,9 +76,9 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
 
     Deque<String>               recentExportPaths;
     boolean                     zoomToFit;
-    boolean                                            attachTG, attachWiki;
+    boolean                     attachTG, attachWiki, addPageNumbers;
 
-    PDFExportPlan              exportPlan;
+    PDFExportPlan               exportPlan;
 
     private boolean readPreferences() {
         IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, Activator.PLUGIN_ID);
@@ -82,6 +88,7 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
         zoomToFit = store.getBoolean(Preferences.DIAGRAM_EXPORT_PDF_ZOOM_TO_FIT);
         attachTG =  store.getBoolean(Preferences.DIAGRAM_EXPORT_PDF_ATTACH_TG);
         attachWiki =  store.getBoolean(Preferences.DIAGRAM_EXPORT_PDF_ATTACH_WIKI);
+        addPageNumbers =  store.getBoolean(Preferences.DIAGRAM_EXPORT_PDF_ADD_PAGE_NUMBERS);
 
         return true;
     }
@@ -93,13 +100,14 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
         store.putValue(Preferences.DIAGRAM_EXPORT_PDF_ZOOM_TO_FIT, String.valueOf(zoomToFit));
         store.putValue(Preferences.DIAGRAM_EXPORT_PDF_ATTACH_TG, String.valueOf(attachTG));
         store.putValue(Preferences.DIAGRAM_EXPORT_PDF_ATTACH_WIKI, String.valueOf(attachWiki));
+        store.putValue(Preferences.DIAGRAM_EXPORT_PDF_ADD_PAGE_NUMBERS, String.valueOf(addPageNumbers));
 
         if (store.needsSaving())
             store.save();
     }
 
     private Deque<String> decodePaths(String recentPathsPref) {
-        Deque<String> result = new LinkedList<String>();
+        Deque<String> result = new LinkedList<>();
         try {
             StringMemento sm = new StringMemento(recentPathsPref);
             for (IMemento m : sm.getChildren(TAG_PATH)) {
@@ -137,7 +145,7 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
     }
 
     private NamedResource toNamedResource(ReadGraph graph, Resource r) throws DatabaseException {
-        String name = graph.syncRequest(new GetName(r));
+        String name = NameLabelUtil.modalName(graph, r, NameLabelMode.NAME_AND_LABEL);
         return new NamedResource(name, r);
     }
 
@@ -154,54 +162,45 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
 
         exportPlan = new PDFExportPlan(ctx, recentExportPaths);
         exportPlan.project = project;
-        final Object selectedObject = selection.getFirstElement();
+        exportPlan.initialSelection = selection;
         exportPlan.fitContentToPageMargins = zoomToFit;
         exportPlan.attachTG = attachTG;
         exportPlan.attachWiki = attachWiki;
+        exportPlan.addPageNumbers = addPageNumbers;
         
         // Get all model names
         try {
             exportPlan.sessionContext.getSession().syncRequest(new ReadRequest() {
                 @Override
                 public void run(ReadGraph graph) throws DatabaseException {
-                    Resource selection = ResourceAdaptionUtils.toSingleResource(selectedObject);
-                    if (selection != null) {
-                        //exportModel.selection = new NamedResource(name + " (input selection)", selection);
-                        exportPlan.selection = toNamedResource(graph, selection);
-                        exportPlan.selectableModels.add(exportPlan.selection);
-                    } else {
-                        for (Resource activeModel : graph.syncRequest(new ActiveModels(exportPlan.project.get()))) {
-                            selection = activeModel;
-                            exportPlan.selection = toNamedResource(graph, activeModel);
-                            exportPlan.selectableModels.add( exportPlan.selection );
-                            break;
-                        }
-                    }
+                    Set<Resource> processed = new HashSet<>();
+                    List<NamedResource> models = new ArrayList<>();
 
-                    List<NamedResource> models = new ArrayList<NamedResource>();
-                    
                     Collection<Resource> ontologies = Simantics.applySCL("Simantics/SharedOntologies", "traverseSharedOntologies", graph, graph.getRootLibrary());
-                    for (Resource model : ontologies) {
-                        if (model.equals(selection))
-                            continue;
-                        models.add( toNamedResource(graph, model) );
+                    for (Resource root : ontologies) {
+                        if (processed.add(root))
+                            models.add( toNamedResource(graph, root) );
                     }
-                    
+
                     for (Resource model : graph.syncRequest(new ObjectsWithType(exportPlan.project.get(),
                             Layer0.getInstance(graph).ConsistsOf, SimulationResource.getInstance(graph).Model))) {
-                        if (model.equals(selection))
-                            continue;
-                        models.add( toNamedResource(graph, model) );
+                        if (processed.add(model))
+                            models.add( toNamedResource(graph, model) );
                     }
-                    Collections.sort(models);
+                    Collections.sort(models, AlphanumComparator.CASE_INSENSITIVE_COMPARATOR);
                     exportPlan.selectableModels.addAll(models);
-                    if (selection == null && !exportPlan.selectableModels.isEmpty()) {
+
+                    Resource selected = ResourceAdaptionUtils.toSingleResource(selection.getFirstElement());
+                    Resource indexRoot = selected != null ? graph.sync(new PossibleIndexRoot(selected)) : null;
+                    if (indexRoot != null)
+                        exportPlan.initialModelSelection = exportPlan.selection = toNamedResource(graph, indexRoot);
+
+                    if (exportPlan.selection == null && !exportPlan.selectableModels.isEmpty())
                         exportPlan.selection = exportPlan.selectableModels.get(0);
-                    }
                 }
             });
         } catch (DatabaseException e) {
-            e.printStackTrace();
+            LOGGER.error("Failed to initialize diagram PDF export wizard input data.", e);
         }
     }
 
@@ -224,7 +223,7 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
             recentExportPaths.addFirst(exportPlan.exportLocation.getAbsolutePath());
 
             // Remove duplicates
-            Set<String> dups = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+            Set<String> dups = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
             for (Iterator<String> it = recentExportPaths.iterator(); it.hasNext();) {
                 String path = it.next();
                 if (!dups.add(path)) {
@@ -238,6 +237,7 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
             zoomToFit = exportPlan.fitContentToPageMargins;
             attachTG = exportPlan.attachTG;
             attachWiki = exportPlan.attachWiki;
+            addPageNumbers = exportPlan.addPageNumbers;
 
             writePreferences();
         } catch (IOException e) {
@@ -256,16 +256,13 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
 
         long start = System.currentTimeMillis();
         try {
-            getContainer().run(true, true, new IRunnableWithProgress() {
-                @Override
-                public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
-                    try {
-                        DiagramPrinter.printToPdf(monitor, exportPlan, exportPlan.exportLocation.toString(), exportPlan.selectedNodes);
-                    } catch (PdfException e) {
-                        throw new InvocationTargetException(e);
-                    } finally {
-                        monitor.done();
-                    }
+            getContainer().run(true, true, monitor -> {
+                try {
+                    DiagramPrinter.printToPdf(monitor, exportPlan, exportPlan.exportLocation.toString(), exportPlan.selectedNodes);
+                } catch (PdfException e) {
+                    throw new InvocationTargetException(e);
+                } finally {
+                    monitor.done();
                 }
             });
         } catch (InvocationTargetException e) {
@@ -276,7 +273,7 @@ public class PDFDiagramExportWizard extends Wizard implements IExportWizard {
             return false;
         }
         long end = System.currentTimeMillis();
-        System.out.println("PDF export took " + ((end - start) * 1e-3) + " seconds.");
+        LOGGER.info("PDF export took " + ((end - start) * 1e-3) + " seconds.");
 
         return true;
     }
index 7ef2efb9406f283b578f6bfdcabc7669d91e997a..fb2f8b0c640adf08c4dcfb845ca46ced5bfb880b 100644 (file)
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * Copyright (c) 2007, 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
  *
  * Contributors:
  *     VTT Technical Research Centre of Finland - initial API and implementation
+ *     Semantum Oy - #7297
  *******************************************************************************/
 package org.simantics.modeling.ui.pdf;
 
 import java.io.File;
 import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Deque;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import org.eclipse.core.runtime.IPath;
-import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.SubMonitor;
 import org.eclipse.jface.layout.GridDataFactory;
-import org.eclipse.jface.operation.IRunnableWithProgress;
-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.ITreeContentProvider;
-import org.eclipse.jface.viewers.StructuredSelection;
-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.jface.wizard.WizardPage;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CCombo;
-import org.eclipse.swt.events.DisposeEvent;
-import org.eclipse.swt.events.DisposeListener;
 import org.eclipse.swt.events.ModifyEvent;
 import org.eclipse.swt.events.ModifyListener;
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
 import org.eclipse.swt.events.SelectionListener;
-import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
-import org.eclipse.swt.layout.RowLayout;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Combo;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.FileDialog;
 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.databoard.Bindings;
 import org.simantics.db.ReadGraph;
 import org.simantics.db.Resource;
 import org.simantics.db.common.NamedResource;
 import org.simantics.db.common.request.ReadRequest;
 import org.simantics.db.exception.DatabaseException;
+import org.simantics.modeling.requests.CollectionResult;
 import org.simantics.modeling.requests.Node;
+import org.simantics.modeling.requests.Nodes;
+import org.simantics.ui.utils.ResourceAdaptionUtils;
 import org.simantics.utils.FileUtils;
-import org.simantics.utils.strings.AlphanumComparator;
-import org.simantics.utils.ui.ISelectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class PDFExportPage extends WizardPage {
 
-       protected Display              display;
+    private static final Logger    LOGGER = LoggerFactory.getLogger(PDFExportPage.class);
 
-    protected PDFExportPlan       exportModel;
+    protected Display              display;
 
-    protected IFilterStrategy      filterStrategy     = new DefaultFilterStrategy();
+    protected PDFExportPlan        exportModel;
 
     protected Combo                modelSelector;
     protected SelectionListener    modelSelectorListener;
 
-    protected Text                 filter;
-
-    protected Matcher              matcher            = null;
-
-    protected CheckboxTreeViewer   tree;
+    protected NodeTree             nodeTree;
 
     protected CCombo               exportLocation;
     protected ModifyListener       exportLocationListener;
 
     protected Set<Node>            selectedNodes;
-
-    protected LocalResourceManager resourceManager;
-    protected Color                noDiagramColor;
     
-    protected Label                               toFileLabel;
+    protected Label                toFileLabel;
 
     protected boolean              exportLocationTouchedByUser = false;
 
-    ICheckStateProvider checkStateProvider = new ICheckStateProvider() {
-        @Override
-        public boolean isChecked(Object element) {
-            Node node = (Node) element;
-
-            // Primarily checked if any children are selected.
-            Collection<Node> children = node.getChildren();
-            if (!children.isEmpty()) {
-                for (Node child : node.getChildren())
-                    if (isChecked(child))
-                        return true;
-
-                // No children are checked, not checked.
-                return false;
-            }
-
-            // Otherwise checked only if selected.
-            return selectedNodes.contains(node);
-        }
-        @Override
-        public boolean isGrayed(Object element) {
-            Node node = (Node) element;
-
-            // Grayed if there are children but not all of them are selected.
-            Collection<Node> children = node.getChildren();
-            if (!children.isEmpty()) {
-                for (Node child : children)
-                    if (!selectedNodes.contains(child))
-                        return true;
-            }
-
-            // Grayed if the node itself contains no diagram.
-            if (node.getDiagramResource() == null)
-                return true;
-
-            // Otherwise never grayed.
-            return false;
-        }
-    };
-
     protected PDFExportPage(PDFExportPlan model) {
         super("Export Diagrams to PDF", "Define Exported Items", null);
         this.exportModel = model;
@@ -165,14 +91,6 @@ public class PDFExportPage extends WizardPage {
             layout.numColumns = 3;
             container.setLayout(layout);
         }
-        resourceManager = new LocalResourceManager(JFaceResources.getResources());
-        container.addDisposeListener(new DisposeListener() {
-            @Override
-            public void widgetDisposed(DisposeEvent e) {
-                resourceManager.dispose();
-            }
-        });
-        noDiagramColor = container.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY);
 
         Label modelSelectorLabel = new Label(container, SWT.NONE);
         modelSelectorLabel.setText("Model Selector:");
@@ -196,198 +114,9 @@ public class PDFExportPage extends WizardPage {
 
         modelSelector.addSelectionListener(modelSelectorListener);
 
-//        Label label = new Label(container, SWT.NONE);
-//        label.setText("Diagrams to Export:");
-//        GridDataFactory.fillDefaults().span(3, 1).applyTo(label);
-
-        Label filterLabel = new Label(container, SWT.NONE);
-        filterLabel.setText("Fi&lter:");
-        GridDataFactory.fillDefaults().span(1, 1).applyTo(filterLabel);
-        filter = new Text(container, SWT.BORDER);
-        GridDataFactory.fillDefaults().span(2, 1).applyTo(filter);
-        filter.addModifyListener(new ModifyListener() {
-            @Override
-            public void modifyText(ModifyEvent e) {
-                resetFilterString(filter.getText());
-            }
-        });
-
-        tree = new CheckboxTreeViewer(container, 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 diagram to include in the exported document.");
-            tree.setAutoExpandLevel(TreeViewer.ALL_LEVELS);
-            tree.addCheckStateListener(new ICheckStateListener(){
-                void addOrRemoveSelection(Node node, boolean add) {
-                    if (add)
-                        selectedNodes.add(node);
-                    else
-                        selectedNodes.remove(node);
-                }
-                void addOrRemoveSelectionRec(Node node, boolean add) {
-                    addOrRemoveSelection(node, add);
-                    for (Node child : node.getChildren())
-                        addOrRemoveSelectionRec(child, add);
-                }
-                @Override
-                public void checkStateChanged(CheckStateChangedEvent event) {
-                    final boolean checked = event.getChecked();
-                    Node checkedNode = (Node) event.getElement();
-
-                    Set<Node> nodes = new HashSet<Node>();
-                    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.setSubtreeChecked(node, checked);
-//                         The checked node is always either checked or not checked, never grayed.
-//                        tree.setGrayed(node, checkStateProvider.isGrayed(node));
-
-//                        Node parent = node.getParent();
-//                        if (parent != null) {
-//                            tree.setChecked(parent, checkStateProvider.isChecked(parent));
-//                            tree.setGrayed(parent, checkStateProvider.isGrayed(parent));
-//                        }
-                    }
-
-                    refreshAndExpandTree();
-                    validatePage();
-                }
-            });
-
-            tree.setContentProvider(new ITreeContentProvider(){
-                @Override
-                public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
-                }
-                @Override
-                public void dispose() {
-                }
-                @Override
-                public Object[] getElements(Object inputElement) {
-                    return exportModel.nodes.roots.toArray();
-                }
-                @Override
-                public boolean hasChildren(Object element) {
-                    Node n = (Node) element;
-                    if (n.getChildren().isEmpty()) return false;
-                       for (Node c : n.getChildren()) if (hasDiagram(c)) return true; 
-                    return false;
-                    
-                }
-                @Override
-                public Object getParent(Object element) {
-                    Node n = (Node) element;
-                    return n.getParent();
-                }
-                @Override
-                public Object[] getChildren(Object parentElement) {
-                    Node n = (Node) parentElement;
-                       List<Object> result = new ArrayList<Object>( n.getChildren().size() );
-                       for (Node c : n.getChildren()) 
-                               if (hasDiagram(c)) 
-                                       result.add(c);
-                    return result.toArray();
-                }
-                
-                boolean hasDiagram(Node n)
-                {                      
-                       if (n.getDiagramResource()!=null) return true;
-                       for (Node c : n.getChildren()) if (hasDiagram(c)) return true;
-                       return false;
-                }
-            });
-            tree.setLabelProvider(new 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());
-                    }
-                }
-            });
-            tree.setComparator(new ViewerComparator(AlphanumComparator.CASE_INSENSITIVE_COMPARATOR));
-            tree.setFilters(new ViewerFilter[] {
-                    new ViewerFilter() {
-                        @Override
-                        public boolean select(Viewer viewer, Object parentElement, Object element) {
-                            if (matcher == null)
-                                return true;
-
-                            Node node = (Node) element;
-                            // If any children are in sight, show this element.
-                            for (Node child : node.getChildren()) {
-                                if (select(viewer, element, child))
-                                    return true;
-                            }
-
-                            return matcher.reset(node.getName().toLowerCase()).matches();
-                        }
-                    }
-            });
-            tree.setCheckStateProvider(checkStateProvider);
-        }
-
-        Composite bar = new Composite(container, 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.addSelectionListener(new SelectionAdapter() {
-            @Override
-            public void widgetSelected(SelectionEvent e) {
-                selectedNodes.addAll(exportModel.nodes.breadthFirstFlatten());
-                for (Node root : exportModel.nodes.roots)
-                    tree.setSubtreeChecked(root, true);
-                validatePage();
-            }
-        });
-        Button clearSelection = new Button(bar, SWT.PUSH);
-        clearSelection.setText("&Clear Selection");
-        clearSelection.addSelectionListener(new SelectionAdapter() {
-            @Override
-            public void widgetSelected(SelectionEvent e) {
-                selectedNodes.clear();
-                for (Node root : exportModel.nodes.roots)
-                    tree.setSubtreeChecked(root, false);
-                validatePage();
-            }
-        });
-        Button selectVisible = new Button(bar, SWT.PUSH);
-        selectVisible.setText("&Select Visible");
-        selectVisible.addSelectionListener(new SelectionAdapter() {
-            @Override
-            public void widgetSelected(SelectionEvent e) {
-                selectedNodes.addAll(getVisibleNodes());
-                refreshAndExpandTree();
-                validatePage();
-            }
-        });
-        Button deselectVisible = new Button(bar, SWT.PUSH);
-        deselectVisible.setText("&Deselect Visible");
-        deselectVisible.addSelectionListener(new SelectionAdapter() {
-            @Override
-            public void widgetSelected(SelectionEvent e) {
-                selectedNodes.removeAll(getVisibleNodes());
-                refreshAndExpandTree();
-                validatePage();
-            }
-        });
+        nodeTree = new NodeTree(container, selectedNodes);
+        GridDataFactory.fillDefaults().grab(true, true).span(3, 1).applyTo(nodeTree);
+        nodeTree.setSelectionChangeListener(this::validatePage);
 
         toFileLabel = new Label(container, SWT.NONE);
         toFileLabel.setText("&To file:");
@@ -474,7 +203,7 @@ public class PDFExportPage extends WizardPage {
             }
         });
         */
-        
+
         final Button attachWikiButton = new Button(container, SWT.CHECK);
         GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo( attachWikiButton );
         attachWikiButton.setText("Attach &Wiki page");
@@ -485,7 +214,18 @@ public class PDFExportPage extends WizardPage {
                 exportModel.attachWiki = attachWikiButton.getSelection();
             }
         });
-        
+
+        final Button addPageNumbers = new Button(container, SWT.CHECK);
+        GridDataFactory.fillDefaults().grab(true, false).span(3, 1).applyTo( addPageNumbers );
+        addPageNumbers.setText("Add page &numbers");
+        addPageNumbers.setSelection(exportModel.addPageNumbers);
+        addPageNumbers.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                exportModel.addPageNumbers = addPageNumbers.getSelection();
+            }
+        });
+
         setControl(container);
         validatePage();
 
@@ -493,21 +233,14 @@ public class PDFExportPage extends WizardPage {
     }
 
     private void scheduleInitializeData(final NamedResource modelSelection) {
-        display.asyncExec(new Runnable() {
-            @Override
-            public void run() {
-                if (filter.isDisposed())
-                    return;
-
-                try {
+        display.asyncExec(() -> {
+            try {
+                if (!nodeTree.isDisposed())
                     initializeData(modelSelection);
-                } catch (DatabaseException e) {
-                    e.printStackTrace();
-                } catch (InvocationTargetException e) {
-                    e.getTargetException().printStackTrace();
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                }
+            } catch (DatabaseException | InterruptedException e) {
+                LOGGER.error("Input data initialization failed.", e);
+            } catch (InvocationTargetException e) {
+                LOGGER.error("Input data initialization failed.", e.getTargetException());
             }
         });
     }
@@ -527,43 +260,9 @@ public class PDFExportPage extends WizardPage {
         exportLocation.addModifyListener(exportLocationListener);
     }
 
-    private Collection<Node> getVisibleNodes() {
-        Collection<Node> result = new ArrayList<Node>();
-
-        Deque<TreeItem> todo = new ArrayDeque<TreeItem>();
-        for (TreeItem ti : tree.getTree().getItems()) {
-            todo.add(ti);
-        }
-
-        while (!todo.isEmpty()) {
-            TreeItem item = todo.removeLast();
-            Node node = (Node) item.getData();
-            result.add(node);
-
-            for (TreeItem child : item.getItems()) {
-                todo.add(child);
-            }
-        }
-
-        return result;
-    }
-
-    private void resetFilterString(String filterString) {
-        String patternString = filterStrategy.toPatternString(filterString);
-        if (patternString == null) {
-            matcher = null;
-        } else {
-            matcher = Pattern.compile(patternString).matcher("");
-        }
-        refreshAndExpandTree();
-    }
-
-    private void refreshAndExpandTree() {
-        tree.refresh();
-        tree.expandAll();
-    }
-
     private void initializeData(final NamedResource modelSelection) throws DatabaseException, InvocationTargetException, InterruptedException {
+        Set<Node> toBeSelected = new HashSet<>();
+
         if (modelSelection != null) {
             // Process input selection to find the model/state selected by default.
 
@@ -573,29 +272,54 @@ public class PDFExportPage extends WizardPage {
             // !PROFILE
             long time = System.nanoTime();
 
-            getWizard().getContainer().run(true, true, new IRunnableWithProgress() {
-                @Override
-                public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
-                    try {
-                        final SubMonitor mon = SubMonitor.convert(monitor, "Searching for exportable diagrams...", 100);
-                        exportModel.sessionContext.getSession().syncRequest(new ReadRequest() {
-                            @Override
-                            public void run(ReadGraph graph) throws DatabaseException {
-                                exportModel.nodes = DiagramPrinter.browse(mon.newChild(100), graph, new Resource[] { modelSelection.getResource() });
+            getWizard().getContainer().run(true, true, monitor -> {
+                try {
+                    SubMonitor mon = SubMonitor.convert(monitor, "Searching for exportable diagrams...", 100);
+                    exportModel.sessionContext.getSession().syncRequest(new ReadRequest() {
+                        @Override
+                        public void run(ReadGraph graph) throws DatabaseException {
+                            CollectionResult coll = exportModel.nodes = DiagramPrinter.browse(mon.newChild(100), graph, new Resource[] { modelSelection.getResource() });
+
+                            // Decide initial selection based on exportModel.initialSelection
+                            if (modelSelection.equals(exportModel.initialModelSelection)) {
+                                Set<Resource> selectedResources = new HashSet<>();
+                                for (Object o : exportModel.initialSelection.toList()) {
+                                    Resource r = ResourceAdaptionUtils.toSingleResource(o);
+                                    if (r != null)
+                                        selectedResources.add(r);
+                                }
+                                coll.walkTree(node -> {
+                                    if (node.getDiagramResource() != null) {
+                                        if (Nodes.parentIsInSet(toBeSelected, node))
+                                            toBeSelected.add(node);
+                                        else
+                                            for (Resource r : node.getDefiningResources())
+                                                if (selectedResources.contains(r))
+                                                    toBeSelected.add(node);
+                                    }
+                                    return true;
+                                });
                             }
-                        });
-                    } catch (DatabaseException e) {
-                        throw new InvocationTargetException(e);
-                    } finally {
-                        monitor.done();
-                    }
+
+                            // Filter out any excess nodes from the tree.
+                            exportModel.nodes = coll = coll.withRoots(Nodes.depthFirstFilter(Nodes.DIAGRAM_RESOURCE_PREDICATE, coll.roots));
+
+                            // Select all if initial selection doesn't dictate anything.
+                            if (toBeSelected.isEmpty())
+                                toBeSelected.addAll(coll.breadthFirstFlatten(CollectionResult.DIAGRAM_RESOURCE_FILTER));
+                        }
+                    });
+                } catch (DatabaseException e) {
+                    throw new InvocationTargetException(e);
+                } finally {
+                    monitor.done();
                 }
             });
 
             // !PROFILE
             long endTime = System.nanoTime();
             if (exportModel.nodes != null)
-                System.out.println("Found " + exportModel.nodes.diagrams.size() + " diagrams in " + ((endTime - time)*1e-9) + " seconds.");
+                LOGGER.info("Found " + exportModel.nodes.diagrams.size() + " diagrams in " + ((endTime - time)*1e-9) + " seconds.");
         }
 
         // Browsing was canceled by user.
@@ -604,15 +328,10 @@ public class PDFExportPage extends WizardPage {
 
         // Setup selected states, select everything by default.
         selectedNodes.clear();
-        selectedNodes.addAll(exportModel.nodes.breadthFirstFlatten());
-
-        tree.setInput(this);
+        selectedNodes.addAll(toBeSelected);
 
-        for (Node root : exportModel.nodes.roots) {
-            tree.setSubtreeChecked(root, true);
-        }
-
-        resetFilterString(filter.getText());
+        // Fully refresh node tree
+        nodeTree.setInput(exportModel.nodes);
 
         modelSelector.removeSelectionListener(modelSelectorListener);
         int selectedIndex = -1;
@@ -632,8 +351,16 @@ public class PDFExportPage extends WizardPage {
     }
 
     void validatePage() {
+        int diagramCount = 0;
+        Node singleDiagram = null;
+        for (Node n : selectedNodes)
+            if (n.getDiagramResource() != null) {
+                ++diagramCount;
+                singleDiagram = n;
+            }
+
         //System.out.println("VALIDATE PAGE: " + exportLocationTouchedByUser);
-        if (selectedNodes.size() == 0) {
+        if (diagramCount == 0) {
             setMessage("Select the diagrams to export.");
             setErrorMessage(null);
             setPageComplete(false);
@@ -645,8 +372,8 @@ public class PDFExportPage extends WizardPage {
             // Generate file name automatically if user hasn't touched the name manually.
             NamedResource nr = getSelectedModel();
             if (nr != null) {
-                if (selectedNodes.size() == 1) {
-                    generatedName = nr.getName() + "-" + selectedNodes.iterator().next().getName();
+                if (diagramCount == 1 && singleDiagram != null) {
+                    generatedName = nr.getName() + "-" + singleDiagram.getName();
                 } else {
                     generatedName = nr.getName();
                 }
@@ -701,11 +428,6 @@ public class PDFExportPage extends WizardPage {
         }
         exportModel.exportLocation = file;
 
-        int diagramCount = 0;
-        for (Node n : selectedNodes)
-            if (n.getDiagramResource() != null)
-                ++diagramCount;
-
         String msg = diagramCount + " diagrams selected for export.";
 
         setMessage(msg);
index a41715f3bc81e64d4eabb0c44323c523541b7c6c..73e5b09d2b4700c3d6a11e50153e224d020d7f9f 100644 (file)
@@ -19,6 +19,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import org.eclipse.jface.viewers.IStructuredSelection;
 import org.simantics.db.common.NamedResource;
 import org.simantics.db.management.ISessionContext;
 import org.simantics.export.core.pdf.PageNumbering;
@@ -31,6 +32,8 @@ public class PDFExportPlan {
     // Input
     public ISessionContext           sessionContext;
     public IProject                  project;
+    public IStructuredSelection      initialSelection;
+    public NamedResource             initialModelSelection;
     public List<NamedResource>       selectableModels = new ArrayList<NamedResource>();
     public NamedResource             selection;
     public Collection<String>        recentLocations;
index f248ca2fa5aad1067cda302c147f26e5b98244f9..97265df58bddd3f074426f70d07b3a3695912255 100644 (file)
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * Copyright (c) 2007, 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
@@ -8,6 +8,7 @@
  *
  * Contributors:
  *     VTT Technical Research Centre of Finland - initial API and implementation
+ *     Semantum Oy - page numbering additions
  *******************************************************************************/
 package org.simantics.modeling.ui.pdf;
 
@@ -21,6 +22,6 @@ public interface Preferences {
     String DIAGRAM_EXPORT_PDF_ZOOM_TO_FIT = "diagram.export.pdf.zoomToFit";
     String DIAGRAM_EXPORT_PDF_ATTACH_TG = "diagram.export.pdf.attachTG";
     String DIAGRAM_EXPORT_PDF_ATTACH_WIKI = "diagram.export.pdf.attachWiki";
-
+    String DIAGRAM_EXPORT_PDF_ADD_PAGE_NUMBERS = "diagram.export.pdf.addPageNumbers";
 
 }
index 70aa8c88b100b7547fccc29ddb8ca2c7d9c6ef01..183717325bf0353a6d2a459d6b8577f914e3625e 100644 (file)
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * Copyright (c) 2007, 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
@@ -27,7 +27,6 @@ import org.simantics.db.Resource;
 import org.simantics.db.common.request.ReadRequest;
 import org.simantics.db.common.request.ResourceRead;
 import org.simantics.db.common.utils.NameUtils;
-import org.simantics.db.exception.AdaptionException;
 import org.simantics.db.exception.DatabaseException;
 import org.simantics.db.request.Read;
 import org.simantics.diagram.query.DiagramRequests;
@@ -69,22 +68,6 @@ public class CollectionRequest implements Read<CollectionResult> {
         return g.syncRequest(new GetName(r));
     }
 
-//    Collection<String> getPartOfGroups(Resource diagram) throws DatabaseException {
-//        Resource r = diagram;
-//        Deque<String> result = new ArrayDeque<String>();
-//        loop:
-//            while (true) {
-//                for (Resource partOf : g.getObjects(r, b.PartOf)) {
-//                    if (g.isInstanceOf(partOf, dr.DiagramLibrary)) {
-//                        result.addFirst(safeGetName(partOf));
-//                        r = partOf;
-//                        continue loop;
-//                    }
-//                }
-//                return result;
-//            }
-//    }
-
     @Override
     public CollectionResult perform(ReadGraph g) throws DatabaseException {
         this.g = g;
@@ -96,53 +79,22 @@ public class CollectionRequest implements Read<CollectionResult> {
         SIMU = SimulationResource.getInstance(g);
 
         final CollectionResult result = new CollectionResult();
-        final Deque<Node> roots = new ArrayDeque<Node>();
+        final Deque<Node> roots = new ArrayDeque<>();
 
         // 1. Based on input, look for the proper nodes to start browsing for diagrams.
         for (Resource r : input) {
-            /*if (g.isInstanceOf(r, SIMU.Model)) {
-                // Complete models
-                Resource composite = g.getPossibleObject(r, SIMU.HasConfiguration);
-                Resource diagram = composite != null ? g.getPossibleObject(composite, mr.CompositeToDiagram) : null;
-
-                if (DEBUG)
-                    System.out.println("Model root");
-
-                if (composite != null) {                       
-                       Node node = new Node(null, safeGetName(r), diagram, composite, r);
-                    roots.add(node);
-                    result.roots.add(roots.peekLast());
-                }
-            } else*/ if (g.isInstanceOf(r, l0.IndexRoot)) {
-//             for(Resource type : ModelingUtils.searchByTypeShallow(g, r, sr.ComponentType)) {
-//                     Resource composite = g.getPossibleObject(type, sr.IsDefinedBy);
-//                    Resource diagram = composite != null ? g.getPossibleObject(composite, mr.CompositeToDiagram) : null;
-//                     if(composite != null) {
-//                     Node node = new Node(null, safeGetName(r), diagram, composite, r);
-//                        roots.add(node);
-//                        result.roots.add(roots.peekLast());
-//                     }
-//             }
-               
-               Node node = new Node(null, safeGetName(r), null, r);
-               roots.add(node);
-               result.roots.add(roots.peekLast());
-               
+            if (g.isInstanceOf(r, l0.IndexRoot)) {
+                Node node = new Node(null, safeGetName(r), null, r);
+                roots.add(node);
+                result.roots.add(roots.peekLast());
             } else if (g.isInstanceOf(r, sr.Composite)) {
                 // The contents of components
                 String name = null;
                 Resource model = g.getPossibleObject(r, SIMU.IsConfigurationOf);
-                //Resource componentType = g.getPossibleObject(r, sr.Defines);
                 if (model != null) {
                     name = safeGetName(model);
-
                     if (DEBUG)
                         System.out.println("Configuration root: " + name);
-
-//                } else if (componentType != null) {
-//                    Resource singleInstance = componentType != null ? g.getPossibleObject(componentType, b.HasSingleInstance) : null;
-//                    name = singleInstance != null ? safeGetName(singleInstance) : safeGetName(componentType);
-//                    System.out.println("Composite of component type root: " + name);
                 } else {
                     name = safeGetName(r);
                     if (DEBUG)
@@ -153,19 +105,10 @@ public class CollectionRequest implements Read<CollectionResult> {
                 diagram = (diagram != null && g.isInstanceOf(diagram, dr.Composite)) ? diagram : null;
 
                 {
-                       Node node = new Node(null, name, diagram, r);
-                       roots.add(node);
-                       result.roots.add(roots.peekLast());
+                    Node node = new Node(null, name, diagram, r);
+                    roots.add(node);
+                    result.roots.add(roots.peekLast());
                 }
-//            } else if (g.isInstanceOf(r, sr.Component)) {
-//                // Complete components themselves
-//                Resource componentType = g.getSingleType(r, sr.Component);
-//                Resource composite = g.getPossibleObject(componentType, sr.IsDefinedBy);
-//                Resource diagram = (composite != null && g.isInstanceOf(composite, sr.Composite)) ? g.getPossibleObject(composite, mr.CompositeToDiagram) : null;
-//                String name = safeGetName(r);
-//                System.out.println("Component root: " + name);
-//                roots.add(new Node(null, name, diagram, composite, r));
-//                result.roots.add(roots.peekLast());
             } else if (g.isInheritedFrom(r, dr.DefinedElement)) {
                 // Symbols
                 Resource composite = g.getPossibleObject(r, sr.IsDefinedBy);
@@ -176,21 +119,13 @@ public class CollectionRequest implements Read<CollectionResult> {
                     name += " Symbol";
                     if (DEBUG)
                         System.out.println("Symbol root: " + name);
-                    
-                    {                 
-                       Node node = new Node(null, name, composite, r);
-                           roots.add(node);
-                           result.roots.add(roots.peekLast());
+
+                    {
+                        Node node = new Node(null, name, composite, r);
+                        roots.add(node);
+                        result.roots.add(roots.peekLast());
                     }
                 }
-//            } else if (g.isInheritedFrom(r, sr.Component)) {
-//                // Reusable component types
-//                Resource composite = g.getPossibleObject(r, sr.IsDefinedBy);
-//                Resource diagram = (composite != null && g.isInstanceOf(composite, sr.Composite)) ? g.getPossibleObject(composite, mr.CompositeToDiagram) : null;
-//                String name = safeGetName(r);
-//                System.out.println("Component type root: " + name);
-//                roots.add(new Node(null, name, diagram, r, composite));
-//                result.roots.add(roots.peekLast());
             }
         }
 
@@ -206,35 +141,41 @@ public class CollectionRequest implements Read<CollectionResult> {
 
             private void loadComposites(ReadGraph graph, final Node node) throws DatabaseException {
                 Resource diagram = node.getDiagramResource();
-                //System.out.println("loadComposites(" + diagram + ", " + node + ")");
-                if (diagram != null) {
+                if (DEBUG)
+                    System.out.println("loadComposites(" + diagram + ", " + node + ")");
+                if (diagram != null)
                     result.addDiagram(diagram, node);
-//                    node.setPartOfGroups(getPartOfGroups(diagram));
-                }
                 mon.setWorkRemaining(1000);
 
                 for(Resource r : graph.getObjects(node.getDefiningResources().resources[0], l0.ConsistsOf)) {
-                       if(graph.isInstanceOf(r, sr.Composite)) {
-                               String compositeName = graph.syncRequest(new GetName(r));
-                               Resource definingDiagram = graph.getPossibleObject(r, mr.CompositeToDiagram);
-                       Node n = new Node(node, compositeName, definingDiagram, r);
+                    if(graph.isInstanceOf(r, sr.Composite)) {
+                        String compositeName = graph.syncRequest(new GetName(r));
+                        Resource definingDiagram = graph.getPossibleObject(r, mr.CompositeToDiagram);
+                        Node n = new Node(node, compositeName, definingDiagram, r);
+                        if (DEBUG)
+                            System.out.println("Found composite: " + n);
                         loadComposites(graph, n);
                         mon.worked(1);
-                       } else if (graph.isInstanceOf(r, l0.Library)) {
-                               String compositeName = graph.syncRequest(new GetName(r));
-                       Node n = new Node(node, compositeName, null, r);
+                    } else if (graph.isInstanceOf(r, l0.Library)) {
+                        String compositeName = graph.syncRequest(new GetName(r));
+                        Node n = new Node(node, compositeName, null, r);
+                        if (DEBUG)
+                            System.out.println("Found library: " + n);
                         loadComposites(graph, n);
                         mon.worked(1);
-                       } else if (graph.isInheritedFrom(r, sr.Component)) {
-                               String name = safeGetName(r);
-                               Node n = new Node(node, name, null, r);
-                               loadComposites(graph, n);
-                               mon.worked(1);
-                  }
+                    } else if (graph.isInheritedFrom(r, sr.Component)) {
+                        Resource definedBy = graph.getPossibleObject(r, sr.IsDefinedBy);
+                        if (definedBy == null)
+                            continue;
+                        String name = safeGetName(r);
+                        Node n = new Node(node, name, null, r);
+                        if (DEBUG)
+                            System.out.println("Found component: " + n);
+                        loadComposites(graph, n);
+                        mon.worked(1);
+                    }
                 }
-                
             }
-
         });
 
         ILog log = Platform.getLog(Platform.getBundle(Plugin.PLUGIN_ID));
@@ -263,9 +204,8 @@ public class CollectionRequest implements Read<CollectionResult> {
 
         return result;
     }
-    
-    static class GetName extends ResourceRead<String> {
 
+    static class GetName extends ResourceRead<String> {
         public GetName(Resource resource) {
             super(resource);
         }
@@ -274,11 +214,10 @@ public class CollectionRequest implements Read<CollectionResult> {
         public String perform(ReadGraph graph) throws DatabaseException {
             try {
                 return NameLabelUtil.modalName(graph, resource);
-            } catch (AdaptionException e) {
+            } catch (DatabaseException e) {
                 return NameUtils.getSafeName(graph, resource);
             }
         }
-
     }
 
 }
\ No newline at end of file
index ff06e43d0946bc10a6bb15a6155232e17631718c..c182c59c80120b7c5eff2814b7a073a07898fc44 100644 (file)
@@ -29,6 +29,9 @@ import org.simantics.scl.runtime.function.Function1;
  */
 public class CollectionResult {
 
+    public static final IFilter DIAGRAM_RESOURCE_FILTER = o -> Nodes.DIAGRAM_RESOURCE_PREDICATE.test((Node) o);
+    public static final IFilter DIAGRAM_RESOURCE_AND_RVI_FILTER = o -> Nodes.DIAGRAM_RESOURCE_AND_RVI_PREDICATE.test((Node) o);
+
     public class DiagramFilter implements IFilter {
 
         private final IFilter proxy;
@@ -44,10 +47,28 @@ public class CollectionResult {
 
     }
 
-    final public Set<Node>           roots       = new ConcurrentSkipListSet<Node>();
-    final private Set<Node>          diagramSet  = new ConcurrentSkipListSet<Node>();
-    final public List<Node>          diagramList = new Vector<Node>();
-    final public Map<Resource, Node> diagrams    = new ConcurrentHashMap<Resource, Node>();
+    public final Set<Node>           roots;
+    private final Set<Node>          diagramSet;
+    public final List<Node>          diagramList;
+    public final Map<Resource, Node> diagrams;
+
+    public CollectionResult() {
+        this.roots       = new ConcurrentSkipListSet<Node>();
+        this.diagramSet  = new ConcurrentSkipListSet<Node>();
+        this.diagramList = new Vector<Node>();
+        this.diagrams    = new ConcurrentHashMap<Resource, Node>();
+    }
+
+    private CollectionResult(Set<Node> roots, Set<Node> diagramSet, List<Node> diagramList, Map<Resource, Node> diagrams) {
+        this.roots = roots;
+        this.diagramSet = diagramSet;
+        this.diagramList = diagramList;
+        this.diagrams = diagrams;
+    }
+
+    public CollectionResult withRoots(Set<Node> roots) {
+        return new CollectionResult(roots, diagramSet, diagramList, diagrams);
+    }
 
     public void addDiagram(Resource r, Node n) {
         diagramList.add(n);
index 3d52b2604df683a08b8f610420ec2f17d023e6cf..4a43e0840a907cecc3bfd0bfbc847113775e8161 100644 (file)
@@ -27,15 +27,12 @@ import org.simantics.utils.strings.AlphanumComparator;
  */
 public class Node implements Comparable<Node> {
 
-    public static final Comparator<Node> CASE_INSENSITIVE_COMPARATOR = new Comparator<Node>() {
-        @Override
-        public int compare(Node o1, Node o2) {
-            return AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(o1.getName(), o2.getName());
-        }
-    };
+    public static final Comparator<Node> CASE_INSENSITIVE_COMPARATOR =
+            (o1, o2) -> AlphanumComparator.CASE_INSENSITIVE_COMPARATOR.compare(o1.getName(), o2.getName());
 
     private final Node       parent;
     private final List<Node> children = new ArrayList<Node>();
+    private final List<Node> unmodifiableChildren = Collections.unmodifiableList(children);
 
     /**
      * May be <code>null</code> if there is no diagram for this node.
@@ -44,10 +41,8 @@ public class Node implements Comparable<Node> {
     private final ResourceArray    definingResource; // i.e. Composite
     private final String           name;
 
-//    private String[]         partOfGroups = {};
-
     private PageDesc         pageDesc;
-    private String           RVI;
+    private String           rvi;
 
     /**
      * @param parent
@@ -67,6 +62,13 @@ public class Node implements Comparable<Node> {
             parent.addChild(this);
     }
 
+    public Node cloneWithoutChildren(Node parent) {
+        Node clone = new Node(parent, name, diagram, definingResource.resources);
+        clone.setRVI(rvi);
+        clone.setPageDesc(pageDesc);
+        return clone;
+    }
+
     public Node getParent() {
         return parent;
     }
@@ -100,27 +102,19 @@ public class Node implements Comparable<Node> {
     }
 
     public Collection<Node> getChildren() {
-        return Collections.unmodifiableCollection(children);
+        return unmodifiableChildren;
     }
 
-//    public void setPartOfGroups(Collection<String> groups) {
-//        this.partOfGroups = groups.toArray(new String[groups.size()]);
-//    }
-//
-//    public String[] getPartOfGroups() {
-//        return partOfGroups;
-//    }
-
     public void setPageDesc(PageDesc pageDesc) {
         this.pageDesc = pageDesc;
     }
     
-    public void setRVI(String RVI) {
-       this.RVI = RVI;
+    public void setRVI(String rvi) {
+       this.rvi = rvi;
     }
     
     public String getRVI() {
-       return RVI;
+       return rvi;
     }
 
     public PageDesc getPageDesc() {
index 01c89aaf19cee51f5f0d45c6e5de3e448310a15b..5d1da87970420ab81ca22de772d679025201ea53 100644 (file)
@@ -7,6 +7,9 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Deque;
 import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
 
 import org.eclipse.jface.viewers.IFilter;
 import org.simantics.scl.runtime.function.Function1;
@@ -16,14 +19,17 @@ import org.simantics.scl.runtime.function.Function1;
  */
 public class Nodes {
 
+    public static final Predicate<Node> DIAGRAM_RESOURCE_PREDICATE = n -> n.getDiagramResource() != null;
+    public static final Predicate<Node> DIAGRAM_RESOURCE_AND_RVI_PREDICATE = n -> n.getDiagramResource() != null && n.getRVI() != null;
+
     public static Collection<Node> breadthFirstFlatten(IFilter filter, Collection<Node> roots) {
-        Collection<Node> result = new ArrayList<Node>();
-        List<Node> sortedRoots = new ArrayList<Node>(roots);
+        Collection<Node> result = new ArrayList<>();
+        List<Node> sortedRoots = new ArrayList<>(roots);
         Collections.sort(sortedRoots);
-        Deque<Node> todo = new ArrayDeque<Node>(sortedRoots);
+        Deque<Node> todo = new ArrayDeque<>(sortedRoots);
         while (!todo.isEmpty()) {
             Node n = todo.removeFirst();
-            List<Node> sorted = new ArrayList<Node>(n.getChildren());
+            List<Node> sorted = new ArrayList<>(n.getChildren());
             Collections.sort(sorted);
             todo.addAll(sorted);
             if (filter == null || filter.select(n))
@@ -33,8 +39,8 @@ public class Nodes {
     }
 
     public static Collection<Node> depthFirstFlatten(IFilter filter, Collection<Node> roots, Comparator<? super Node> comparator) {
-        Collection<Node> result = new ArrayList<Node>();
-        List<Node> sortedRoots = new ArrayList<Node>(roots);
+        Collection<Node> result = new ArrayList<>();
+        List<Node> sortedRoots = new ArrayList<>(roots);
         Collections.sort(sortedRoots, comparator);
         for (Node n : sortedRoots) {
             depthFirstFlattenRec(filter, comparator, n, result);
@@ -50,7 +56,7 @@ public class Nodes {
         if (children.isEmpty())
             return result;
 
-        List<Node> sorted = new ArrayList<Node>(children);
+        List<Node> sorted = new ArrayList<>(children);
         Collections.sort(sorted, comparator);
         for (Node child : sorted)
             depthFirstFlattenRec(filter, comparator, child, result);
@@ -67,7 +73,7 @@ public class Nodes {
      *         if the walk was cancelled
      */
     public static boolean walkTree(Function1<Node, Boolean> filter, Collection<Node> roots) {
-        List<Node> sortedRoots = new ArrayList<Node>(roots);
+        List<Node> sortedRoots = new ArrayList<>(roots);
         Collections.sort(sortedRoots);
         for (Node n : sortedRoots)
             if (!walkTreeRec(filter, n))
@@ -81,7 +87,7 @@ public class Nodes {
 
         Collection<Node> children = n.getChildren();
         if (!children.isEmpty()) {
-            List<Node> sorted = new ArrayList<Node>(children);
+            List<Node> sorted = new ArrayList<>(children);
             Collections.sort(sorted);
             for (Node child : sorted)
                 if (!walkTreeRec(filter, child))
@@ -90,4 +96,41 @@ public class Nodes {
         return true;
     }
 
+    public static boolean parentIsInSet(Set<Node> set, Node node) {
+        for (Node n = node.getParent(); n != null; n = n.getParent())
+            if (set.contains(n))
+                return true;
+        return false;
+    }
+
+    public static Set<Node> depthFirstFilter(Predicate<Node> filter, Collection<Node> nodes) {
+        Set<Node> result = new TreeSet<>(Node.CASE_INSENSITIVE_COMPARATOR);
+        for (Node n : nodes) {
+            Node newNode = depthFirstFilterRec(filter, n, null);
+            if (newNode != null)
+                result.add(newNode);
+        }
+        return result;
+    }
+
+    public static Node depthFirstFilter(Predicate<Node> filter, Node n) {
+        return depthFirstFilterRec(filter, n, null);
+    }
+
+    private static Node depthFirstFilterRec(Predicate<Node> filter, Node n, Node newParent) {
+        Collection<Node> children = n.getChildren();
+        if (children.isEmpty())
+            return filter.test(n) ? n.cloneWithoutChildren(newParent) : null;
+
+        Node newNode = n.cloneWithoutChildren(newParent);
+        int childCount = 0;
+        for (Node child : children) {
+            Node newChild = depthFirstFilterRec(filter, child, newNode);
+            if (newChild != null)
+                ++childCount;
+        }
+
+        return childCount > 0 ? newNode : null;
+    }
+
 }