1   /****************************************************************************
2    * This demo file is part of yFiles for Java 2.14.
3    * Copyright (c) 2000-2017 by yWorks GmbH, Vor dem Kreuzberg 28,
4    * 72070 Tuebingen, Germany. All rights reserved.
5    * 
6    * yFiles demo files exhibit yFiles for Java functionalities. Any redistribution
7    * of demo files in source code or binary form, with or without
8    * modification, is not permitted.
9    * 
10   * Owners of a valid software license for a yFiles for Java version that this
11   * demo is shipped with are allowed to use the demo source code as basis
12   * for their own yFiles for Java powered applications. Use of such programs is
13   * governed by the rights and conditions as set out in the yFiles for Java
14   * license agreement.
15   * 
16   * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
17   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18   * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19   * NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21   * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   *
27   ***************************************************************************/
28  package demo.view.mindmap;
29  
30  import demo.view.DemoBase;
31  import y.base.Command;
32  import y.base.Edge;
33  import y.base.EdgeCursor;
34  import y.base.EdgeList;
35  import y.base.Node;
36  import y.layout.FreeNodeLabelModel;
37  import y.util.Cursors;
38  import y.view.GenericEdgeRealizer;
39  import y.view.GenericNodeRealizer;
40  import y.view.Graph2D;
41  import y.view.Graph2DUndoManager;
42  import y.view.Graph2DView;
43  import y.view.LineType;
44  import y.view.NodeLabel;
45  import y.view.NodeRealizer;
46  import y.view.ShapeNodeRealizer;
47  
48  import javax.swing.Icon;
49  import java.awt.Color;
50  import java.awt.Font;
51  import java.util.Collection;
52  import java.util.Iterator;
53  import java.util.LinkedHashSet;
54  
55  /**
56   * This class provides static methods to manipulate the mind map
57   */
58  class MindMapUtil {
59    static final Color CROSS_EDGE_COLOR = new Color(126, 192, 200);
60    static final Color BLACK = new Color(50, 50, 50);
61    static final Color RED = new Color(216, 38, 34);
62    static final Color GREEN = new Color(128, 255, 128);
63    static final Color DARK_GREEN = new Color(87, 173, 87);
64    static final Color BLUE = new Color(80, 80, 255);
65    static final Color LIGHT_BLUE = new Color(44, 174, 212);
66    static final Color MAGENTA = new Color(255, 145, 255);
67    static final Color ORANGE = new Color(255, 101, 2);
68    static final Color BROWN = new Color(139,69,19);
69  
70  
71    private static final int MINIMUM_NODE_WIDTH = 20;
72  
73  
74    /**
75     * Prevent instantiation of utility class.
76     */
77    private MindMapUtil() {
78    }
79  
80  
81    /**
82     * Creates a new and configured item and automatically starts the inline
83     * text editor for the new item's label.
84     * @param parent the parent of the new item
85     */
86    static void addNode( final Graph2DView view, final Node parent ) {
87      addNodeImpl(view, parent, ViewModel.instance.isLeft(parent));
88    }
89  
90    /**
91     * Creates a new and configured item and automatically starts the inline
92     * text editor for the new item's label.
93     * @param parent the parent of the new item
94     * @param placeLeft if <code>true</code>, the new item will be placed to the
95     * left of the root item; otherwise it will be placed to the right of the
96     * root item. This parameter is ignored if the specified parent is the root
97     * item.
98     */
99    static void addNode( final Graph2DView view, final Node parent, final boolean placeLeft ) {
100     addNodeImpl(view, parent, placeLeft);
101   }
102 
103   private static void addNodeImpl(
104           final Graph2DView view,
105           final Node parent,
106           final boolean placeLeft
107   ) {
108     final Graph2D graph = view.getGraph2D();
109     final Node child = addNodeImpl(graph, parent, "", placeLeft, false);
110     KeyboardHandling.editLabel(view, graph.getRealizer(child).getLabel());
111   }
112 
113   /**
114    * Creates a new and configured item.
115    * @param graph the current <code>Graph2D</code>
116    * @param parent the parent of the new item
117    * @param name the text of the new item
118    * @param placeLeft if <code>true</code>, the new item will be placed to the
119    * left of the root item; otherwise it will be placed to the right of the
120    * root item. This parameter is ignored if the specified parent is the root
121    * item.
122    * @return the new item
123    */
124   static Node addNode(
125           final Graph2D graph,
126           final Node parent,
127           final String name,
128           final boolean placeLeft
129   ) {
130     return addNodeImpl(graph, parent, name, placeLeft, true);
131   }
132 
133   private static Node addNodeImpl(
134           final Graph2D graph,
135           final Node parent,
136           final String name,
137           final boolean placeLeft,
138           final boolean loadFromFile
139   ) {
140     final ViewModel model = ViewModel.instance;
141 
142     graph.firePreEvent(parent);
143     final Node node = graph.createNode(0, 0, name);
144     final Edge edge = graph.createEdge(parent, node);
145     graph.setRealizer(edge, new GenericEdgeRealizer("BezierGradientEdge"));
146 
147     //make siblings visible
148     if (!loadFromFile && model.isCollapsed(parent)) {
149       expandNode(graph, parent);
150     }
151 
152     final NodeRealizer realizer = graph.getRealizer(node);
153     realizer.setFillColor(
154             ViewModel.instance.isRoot(parent)
155             ? MindMapUtil.BLUE
156             : graph.getRealizer(parent).getFillColor());
157     final NodeLabel label = realizer.getLabel();
158     label.setFontSize(16);
159     realizer.setWidth(Math.max(20, label.getWidth()));
160     
161     boolean isLeftSide = placeLeft;
162     if (model.isRoot(parent)) {
163       int left = 0;
164       int right = 0;
165       for (EdgeCursor ec = parent.outEdges(); ec.ok(); ec.next()) {
166         if (model.isLeft(ec.edge().target())) {
167           ++left;
168         } else {
169           ++right;
170         }
171       }
172       isLeftSide = left < right;
173     }
174 
175     updateVisuals(graph, node, isLeftSide);
176     //to put the new item at the end of the outEdges, move it to the bottom
177     final EdgeList edgeList = outEdges(parent);
178     if (edgeList.size() > 1 && !loadFromFile) {
179       //depending on the side, the old bottom item is the first or the last
180       final NodeRealizer firstRealizer = graph.getRealizer(edgeList.firstEdge().target());
181       //the last is the new item, so take the second last
182       final NodeRealizer lastRealizer = graph.getRealizer(((Edge)edgeList.get(edgeList.size()-2)).target());
183       final double max = Math.max(firstRealizer.getY(), lastRealizer.getY());
184       graph.getRealizer(node).setY(max+1);
185     }
186     if (!loadFromFile) {
187       LayoutUtil.layout(graph);
188     }
189     graph.firePostEvent();
190     return node;
191   }
192 
193   /**
194    * Removes the specified item and all of its descendants.
195    * @param graph the mind map that contains the items to be removed.
196    * @param node the root item to be removed.
197    */
198   static void removeSubtree( final Graph2D graph, final Node node ) {
199     final ViewModel model = ViewModel.instance;
200     for (EdgeCursor ec = node.outEdges(); ec.ok(); ec.next()) {
201       final Edge edge = ec.edge();
202       //only "real" children should be deleted, but not cross-referenced items
203       if (!model.isCrossReference(edge)) {
204         removeSubtree(graph, edge.target());
205       //if the cross reference was connected to one of the item's descendants,
206       //it may already be deleted at this point
207       } else if (graph.contains(edge)) {
208         graph.removeEdge(edge);
209       }
210     }
211     graph.removeNode(node);
212   }
213 
214   /**
215    * Sets the default root item visualization for the specified node.
216    * @param graph the mind map.
217    * @param node the mind map's root item.
218    * @param nodeText the label text for the root item.
219    */
220   static void setRootRealizer(
221           final Graph2D graph, final Node node, final String nodeText
222   ) {
223     final ShapeNodeRealizer nr = new ShapeNodeRealizer(ShapeNodeRealizer.ELLIPSE);
224     nr.setLocation(0, 0);
225     nr.setLineType(LineType.LINE_4);
226     nr.setFillColor(Color.WHITE);
227     nr.setLineColor(MindMapUtil.BLACK);
228     final NodeLabel nl = nr.getLabel();
229     nl.setFontSize(30);
230     nl.setText(nodeText);
231     nr.setWidth(Math.max(20, nl.getWidth() * 1.3));
232     nr.setHeight(nl.getHeight() * 2.5);
233 
234     graph.setRealizer(node, nr);
235   }
236 
237   /**
238    * Switches the specified item from collapsed to expanded state or vice versa. 
239    * @param graph the mind map.
240    * @param node the item whose descendant have to be collapsed or expanded.
241    */
242   static void toggleCollapseState( final Graph2D graph, final Node node ) {
243     graph.firePreEvent();
244 
245     if (ViewModel.instance.isCollapsed(node)) {
246       MindMapUtil.expandNode(graph, node);
247     } else {
248       MindMapUtil.collapseNode(graph, node);
249     }
250     LayoutUtil.layout(graph);
251 
252     graph.firePostEvent();
253   }
254 
255   /**
256    * Collapses the specified item, that means the item's descendants are
257    * temporarily removed from the mind map.
258    * @param graph the mind map.
259    * @param root the item whose descendants are temporarily removed.
260    * @see #expandNode(y.view.Graph2D, y.base.Node)
261    */
262   static void collapseNode( final Graph2D graph, final Node root ) {
263     final ViewModel model = ViewModel.instance;
264 
265     // determine the nodes and edges which have to be removed from the graph
266     final LinkedHashSet nodesToHide = new LinkedHashSet();
267     final EdgeList edgesToHide = new EdgeList();
268     final LinkedHashSet crossReferences = new LinkedHashSet();
269     for (EdgeCursor ec = root.outEdges(); ec.ok(); ec.next()) {
270       final Edge edge = ec.edge();
271       if (!model.isCrossReference(edge)) {
272         final Node node = edge.target();
273         edgesToHide.add(edge);
274         nodesToHide.add(node);
275         collectSubgraph(node, nodesToHide, edgesToHide, crossReferences);
276       }
277     }
278 
279     // remove the previously collected nodes and edges
280     graph.firePreEvent();
281     graph.backupRealizers(Cursors.createEdgeCursor(crossReferences));
282     graph.backupRealizers(edgesToHide.edges());
283     graph.backupRealizers(Cursors.createNodeCursor(nodesToHide));
284 
285     for (Iterator it = crossReferences.iterator(); it.hasNext(); ) {
286       graph.removeEdge((Edge) it.next());
287     }
288     for (EdgeCursor ec = edgesToHide.edges(); ec.ok(); ec.next()) {
289       graph.removeEdge(ec.edge());
290     }
291 
292     // cache the removed edges to be able to reinsert them on a later expand
293     model.addHiddenCrossReferences(crossReferences);
294     model.setHiddenEdges(root, edgesToHide);
295 
296     // make sure undo/redo will properly update ViewModel's caches for
297     // temporarily removed edges
298     getUndoManager(graph).push(new Collapse(root, edgesToHide, crossReferences));
299 
300     for (Iterator it = nodesToHide.iterator(); it.hasNext(); ) {
301       final Node node = (Node) it.next();
302 
303       // store relative location to root
304       final double dx = graph.getCenterX(node) - graph.getCenterX(root);
305       final double dy = graph.getCenterY(node) - graph.getCenterY(root);
306       graph.getRealizer(node).setLocation(dx, dy);
307       
308       graph.removeNode(node);
309     }
310     graph.firePostEvent();
311   }
312 
313   /**
314    * Collects all nodes and edges in the subtree rooted at the specified node.
315    * Additionally, all cross-references connected to nodes in the subtree are
316    * collected as well.
317    * @param root the root of the subtree to traverse.
318    * @param nodesToHide output parameter to store the subtree nodes.
319    * @param edgesToHide output parameter to store the (non-cross-reference)
320    * subtree edges.
321    * @param cfsToHide output parameter to store all cross-references connected
322    * to nodes in the subtree.
323    */
324   private static void collectSubgraph(
325           final Node root,
326           final Collection nodesToHide,
327           final Collection edgesToHide,
328           final Collection cfsToHide
329   ) {
330     final ViewModel model = ViewModel.instance;
331     for (EdgeCursor ec = root.inEdges(); ec.ok(); ec.next()) {
332       final Edge edge = ec.edge();
333       if (model.isCrossReference(edge)) {
334         // assumes a collection that discards duplicates (i.e. a set)
335         cfsToHide.add(edge);
336       }
337     }
338     for (EdgeCursor ec = root.outEdges(); ec.ok(); ec.next()) {
339       final Edge edge = ec.edge();
340       if (model.isCrossReference(edge)) {
341         // assumes a collection that discards duplicates (i.e. a set)
342         cfsToHide.add(edge);
343       } else {
344         final Node node = edge.target();
345         nodesToHide.add(node);
346         edgesToHide.add(edge);
347         collectSubgraph(node, nodesToHide, edgesToHide, cfsToHide);
348       }
349     }
350   }
351 
352   /**
353    * Expands the specified item, that means the item's descendants that were
354    * previously removed by {@link #collapseNode(y.view.Graph2D, y.base.Node)}
355    * are inserted into the mind map again.
356    * @param graph the mind map.
357    * @param root the item whose descendants are added again.
358    * @see #collapseNode(y.view.Graph2D, y.base.Node)
359    */
360   static void expandNode( final Graph2D graph, final Node root ) {
361     final ViewModel model = ViewModel.instance;
362     final EdgeList hiddenEdges = model.popHiddenEdges(root);
363     if (hiddenEdges != null) {
364       graph.firePreEvent();
365 
366       for (EdgeCursor ec = hiddenEdges.edges(); ec.ok(); ec.next()) {
367         final Edge edge = ec.edge();
368 
369         if (!graph.contains(edge.source())) {
370           graph.reInsertNode(edge.source());
371           graph.setLocation(edge.source(),
372                             graph.getX(root) + graph.getX(edge.source()),
373                             graph.getY(root) + graph.getY(edge.source()));
374         }
375 
376         if (!graph.contains(edge.target())) {
377           graph.reInsertNode(edge.target());
378           graph.setLocation(edge.target(),
379                             graph.getX(root) + graph.getX(edge.target()),
380                             graph.getY(root) + graph.getY(edge.target()));
381         }
382 
383         graph.reInsertEdge(edge);
384 
385         //cosmetics
386         graph.getRealizer(edge).clearBends();
387       }
388 
389       // handle cross-references
390       final LinkedHashSet crossReferences = new LinkedHashSet();
391       for (Iterator it = model.hiddenCrossReferences(); it.hasNext();) {
392         final Edge edge = (Edge) it.next();
393         if (graph.contains(edge)) {
394           it.remove();
395           continue;
396         }
397 
398         if (graph.contains(edge.source()) && graph.contains(edge.target())) {
399           it.remove();
400           crossReferences.add(edge);
401           graph.reInsertEdge(edge);
402           //cosmetics
403           graph.getRealizer(edge).clearBends();
404         }
405       }
406 
407       // make sure undo/redo will properly update ViewModel's caches for
408       // temporarily removed edges
409       getUndoManager(graph).push(new Expand(root, hiddenEdges, crossReferences));
410 
411       //maybe the item was moved in collapsed state, preventing the children to get updated properly
412       if (!model.isRoot(root)) {
413         updateVisualsRecursive(graph, root, model.isLeft(root));
414       }
415 
416       graph.firePostEvent();
417     }
418   }
419 
420   /**
421    * Retrieves the icon resource identified by the given filename.
422    * @param iconName the filename of the icon resource.
423    */
424   static Icon getIcon( final String iconName ) {
425     return DemoBase.getIconResource("resource/" + iconName);
426   }
427 
428   /**
429    * Calls {@link #updateVisuals(y.view.Graph2D, y.base.Node, boolean)}
430    * for the specified item and all of its descendants.
431    * @param graph the mind map.
432    * @param node the item to be updated.
433    * @param left if <code>true</code>, the item relative location is to the left
434    * otherwise to the right of the root item.
435    */
436   static void updateVisualsRecursive(
437           final Graph2D graph, final Node node, final boolean left
438   ) {
439     updateVisuals(graph, node, left);
440     for (EdgeCursor ec = outEdges(node).edges(); ec.ok(); ec.next()) {
441       //only follow non cross edges
442       updateVisualsRecursive(graph, ec.edge().target(), left);
443     }
444   }
445 
446   /**
447    * Updates the specified node's visualization according to its current level
448    * in the mind map and sets its location relative to the root item.
449    * @param graph the mind map.
450    * @param node the item to be updated.
451    * @param left if <code>true</code>, the item relative location is to the left
452    * otherwise to the right of the root item.
453    */
454   static void updateVisuals(
455           final Graph2D graph, final Node node, final boolean left
456   ) {
457     final NodeRealizer nr = graph.getRealizer(node);
458 
459     final ViewModel model = ViewModel.instance;
460     final boolean notRoot = !model.isRoot(node);
461     final boolean updateVisuals = notRoot && isMindMapRealizer(nr);
462 
463     //starting at the center item, the lines should get thinner
464     final Edge inEdge = inEdge(node);
465     if (inEdge != null && updateVisuals) {
466       if (model.isRoot(inEdge.source())) {
467         nr.setLineType(LineType.LINE_6);
468       } else {
469         nr.setLineType(LineType.LINE_3);
470       }
471 
472       //Adjust Font
473       final NodeLabel label = nr.getLabel();
474       label.setFontStyle(Font.BOLD);
475       if (model.isRoot(inEdge.source())) {
476         label.setFontSize(16);
477       } else  {
478         label.setFontSize(14);
479       }
480     }
481 
482     //Specify appropriate placement
483     if (notRoot) {
484       model.setLeft(node, left);
485     }
486 
487     //Set width depending on icon
488     if (updateVisuals) {
489       updateWidth(graph, node);
490     }
491   }
492 
493   /**
494    * Determines whether or not the specified realizer is an instance of the
495    * default realizer type used for mind maps.
496    * @param nr the node realizer instance to check.
497    * @return <code>true</code> if the specified realizer is an instance of the
498    * default realizer type used for mind maps; <code>false</code> otherwise.
499    */
500   private static boolean isMindMapRealizer( final NodeRealizer nr ) {
501     return nr instanceof GenericNodeRealizer &&
502            "MindMapUnderline".equals(((GenericNodeRealizer) nr).getConfiguration());
503   }
504 
505   /**
506    * Changes the specified node's width and label position depending on the
507    * node's state icon.
508    * @param graph the mind map.
509    * @param node the item to change.
510    */
511   static void updateWidth( final Graph2D graph, final Node node ) {
512     if (!ViewModel.instance.isRoot(node)) {
513       NodeRealizer nr = graph.getRealizer(node);
514       NodeLabel nl = nr.getLabel();
515 
516       int xoffset = 0;
517       final Icon icon = MindMapNodePainter.getStateIcon(nr);
518       if (icon == null) {
519         nr.setWidth(Math.max(MINIMUM_NODE_WIDTH, nl.getWidth()));
520       } else {
521         final int reserved = icon.getIconWidth() + 4;
522         if (!ViewModel.instance.isLeft(node)) {
523           xoffset = reserved;
524         }
525         nr.setWidth(Math.max(MINIMUM_NODE_WIDTH, nl.getWidth() + reserved));
526       }
527 
528       final FreeNodeLabelModel m = new FreeNodeLabelModel();
529       nl.setLabelModel(m, new FreeNodeLabelModel.ModelParameter(xoffset, 0));
530     } else {
531       final NodeRealizer realizer = graph.getRealizer(node);
532       realizer.setWidth(Math.max(MINIMUM_NODE_WIDTH, realizer.getLabel().getWidth() * 1.3));
533     }
534   }
535 
536   /**
537    * Returns all non-cross-reference outgoing edges.
538    * @param node the source node of the out edges
539    * @return all non-cross-reference outgoing edges.
540    */
541   static EdgeList outEdges( final Node node ) {
542     final ViewModel model = ViewModel.instance;
543     final EdgeList edges = new EdgeList();
544     for (EdgeCursor ec = node.outEdges();ec.ok();ec.next()) {
545       if (!model.isCrossReference(ec.edge())) {
546         edges.add(ec.edge());
547       }
548     }
549     return edges;
550   }
551 
552   /**
553    * Returns the one non-cross-reference incoming edge.
554    * @param node the target node
555    * @return the one non-cross-reference incoming edge or <code>null</code>
556    * if there is no such edge.
557    */
558   static Edge inEdge( final Node node ) {
559     final ViewModel model = ViewModel.instance;
560     for (EdgeCursor ec = node.inEdges(); ec.ok(); ec.next()) {
561       if (!model.isCrossReference(ec.edge())) {
562         return ec.edge();
563       }
564     }
565     return null;
566   }
567 
568   /**
569    * Returns the undo queue for the specified graph.
570    */
571   private static Graph2DUndoManager getUndoManager( final Graph2D graph ) {
572     return (Graph2DUndoManager) graph.getBackupRealizersHandler();
573   }
574 
575   /**
576    * Updates {@link ViewModel}'s caches for temporarily removed edges
577    * after a collapse or expand operation.
578    */
579   private static class ChangeState {
580     private final Node root;
581     private final EdgeList edges;
582     private final Collection refs;
583 
584     /**
585      * Initializes a new <code>ChangeState</code> instance for the
586      * given subtree root and the given edges.
587      * @param root the node that is either collapsed or expanded.
588      * @param edges the non-cross-reference edges that are temporarily removed.
589      * @param refs the cross-reference edges that are temporarily removed.
590      */
591     ChangeState( final Node root, final EdgeList edges, final Collection refs ) {
592       this.root = root;
593       this.edges = edges;
594       this.refs = refs;
595     }
596 
597     public void execute() {
598     }
599 
600     /**
601      * Updates {@link ViewModel}'s caches for temporarily removed edges
602      * after a collapse operation.
603      */
604     void collapse() {
605       final ViewModel model = ViewModel.instance;
606       model.addHiddenCrossReferences(refs);
607       model.setHiddenEdges(root, edges);
608     }
609 
610     /**
611      * Updates {@link ViewModel}'s caches for temporarily removed edges
612      * after an expand operation.
613      */
614     void expand() {
615       final ViewModel model = ViewModel.instance;
616       model.popHiddenEdges(root);
617       for (Iterator it = model.hiddenCrossReferences(); it.hasNext();) {
618         if (refs.contains(it.next())) {
619           it.remove();
620         }
621       }
622     }
623   }
624 
625   /**
626    * Updates {@link ViewModel}'s caches for temporarily removed edges
627    * when undoing or redoing a collapse operation.
628    */
629   private static class Collapse extends ChangeState implements Command {
630     Collapse( final Node root, final EdgeList edges, final Collection refs ) {
631       super(root, edges, refs);
632     }
633 
634     public void undo() {
635       expand();
636     }
637 
638     public void redo() {
639       collapse();
640     }
641   }
642 
643   /**
644    * Updates {@link ViewModel}'s caches for temporarily removed edges
645    * when undoing or redoing an expand operation.
646    */
647   private static class Expand extends ChangeState implements Command {
648     Expand( final Node root, final EdgeList edges, final Collection refs ) {
649       super(root, edges, refs);
650     }
651 
652     public void undo() {
653       collapse();
654     }
655 
656     public void redo() {
657       expand();
658     }
659   }
660 }
661