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 y.base.GraphEvent;
31  import y.base.GraphListener;
32  import y.base.Node;
33  import y.base.NodeCursor;
34  import y.base.NodeList;
35  import y.view.AbstractMouseInputEditor;
36  import y.view.Drawable;
37  import y.view.Graph2D;
38  import y.view.Graph2DView;
39  import y.view.HitInfo;
40  import y.view.Mouse2DEvent;
41  import y.view.MouseInputEditor;
42  import y.view.MouseInputEditorProvider;
43  import y.view.NodeRealizer;
44  
45  import java.awt.Color;
46  import java.awt.Graphics2D;
47  import java.awt.Rectangle;
48  import java.awt.geom.GeneralPath;
49  import java.util.WeakHashMap;
50  
51  /**
52   * Hides or unhides children of an item.
53   */
54  class CollapseButton extends AbstractMouseInputEditor implements Drawable, MouseInputEditorProvider, MouseInputEditor {
55    /**
56     * The target node for this collapse button's edit action.
57     */
58    private final Node node;
59    /**
60     * The graph data structure for this collapse button's associated node.
61     */
62    private final Graph2D graph;
63  
64    /**
65     * Initializes a new <code>CollapseButton</code> instance.
66     */
67    CollapseButton( final Node node, Graph2D graph ) {
68      this.graph = graph;
69      this.node = node;
70    }
71  
72    /**
73     * Returns the bounds of this collapse button in graph (world) coordinates.
74     * @return the bounds of this collapse button in graph (world) coordinates.
75     */
76    public Rectangle getBounds() {
77      Rectangle r = new Rectangle(-1,-1);
78      if (isVisible()) {
79        r = new Rectangle(16, 16);
80        final NodeRealizer realizer = graph.getRealizer(node);
81        //place the collapse button for the center item to the bottom center
82        if (ViewModel.instance.isRoot(node)) {
83          r.x = (int) realizer.getCenterX() - 8;
84          r.y = (int) (realizer.getY() + realizer.getHeight());
85          //place the collapse button depending on the side of an item
86        } else {
87          if (ViewModel.instance.isLeft(node)) {
88            r.x = (int) (realizer.getX() - 13);
89          } else {
90            r.x = (int) (realizer.getX() + realizer.getWidth());
91          }
92          r.y = (int) (realizer.getY() + realizer.getHeight()/2+3);
93        }
94      }
95      return r;
96    }
97  
98    /**
99     * Paints a white arrow in a light blue circle to represent this collapse
100    * button.
101    */
102   public void paint(Graphics2D g) {
103     final double x = this.getBounds().x;
104     final double y = this.getBounds().y;
105     if (isVisible()) {
106       //make sure to point the arrow to the right direction
107       g = (Graphics2D) g.create();
108       g.translate(x,y);
109       GeneralPath gp = new GeneralPath();
110       //Right arrow
111       if (!ViewModel.instance.isCollapsed(node) == ViewModel.instance.isLeft(node)) {
112         gp.moveTo(4, 6);
113         gp.lineTo(4, 10);
114         gp.lineTo(8, 10);
115         gp.lineTo(8, 12);
116         gp.lineTo(13, 8);
117         gp.lineTo(8, 4);
118         gp.lineTo(8, 6);
119       //Left arrow
120       } else {
121         gp.moveTo(12, 6);
122         gp.lineTo(12, 10);
123         gp.lineTo(8, 10);
124         gp.lineTo(8, 12);
125         gp.lineTo(3, 8);
126         gp.lineTo(8, 4);
127         gp.lineTo(8, 6);
128       }
129       final Color circleBlue = new Color(6, 164, 255);
130       g.setColor(circleBlue);
131       g.fillOval(0, 0, 16, 16);
132       g.setColor(Color.WHITE);
133       g.fill(gp);
134       g.dispose();
135     }
136   }
137 
138   /**
139    * Determines if this collapse button should be visible.
140    * @return <code>true</code> if this collapse button is visible;
141    * <code>false</code> otherwise.
142    */
143   private boolean isVisible() {
144     return graph.contains(node) && 
145            (ViewModel.instance.isCollapsed(node) ||
146             !MindMapUtil.outEdges(node).isEmpty());
147   }
148 
149   /**
150    * Collapses or expands the subtree rooted at this collapse button's
151    * associated node.
152    */
153   private void handleClick() {
154     if (isVisible()) {
155       MindMapUtil.toggleCollapseState(graph, node);
156       graph.updateViews();
157     }
158   }
159 
160   /**
161    * Determines if this control should be activated for the given event.
162    * @param event the event that happened
163    * @return <code>true</code> if the event position lies inside the bounds
164    * of this control and <code>false</code> otherwise.
165    */
166   public boolean startsEditing(final Mouse2DEvent event) {
167     return getBounds().contains(event.getX(), event.getY());
168   }
169 
170   /**
171    * Handles mouse events while this control is active.
172    * The default implementation calls {@link #handleClick()} for mouse clicks.
173    * @param event the event that happened
174    */
175   public void mouse2DEventHappened(final Mouse2DEvent event) {
176     if (getBounds().contains(event.getX(), event.getY())) {
177       if (event.getId() == Mouse2DEvent.MOUSE_CLICKED) {
178         handleClick();
179         stopEditing();
180       }
181     } else {
182       stopEditing();
183     }
184   }
185 
186   /**
187    * Returns this <code>CollapseButton</code> instance.
188    * @param view the view that will host the editor
189    * @param x the x-coordinate of the mouse event
190    * @param y the y-coordinate of the mouse event
191    * @param hitInfo the HitInfo that may be used to determine what instance to return or <code>null</code>
192    * @return this <code>CollapseButton</code> instance or <code>null</code> if
193    * the specified coordinates do not lie within this collapse button's bounds.
194    */
195   public MouseInputEditor findMouseInputEditor(Graph2DView view, double x, double y, HitInfo hitInfo) {
196     return getBounds().contains(x, y) ? this : null;
197   }
198 
199 
200   /**
201    * Adds and removes collapse buttons to the view whenever a node is
202    * created or deleted.
203    */
204   static class Handler implements GraphListener {
205     private final Graph2DView view;
206     private final WeakHashMap node2button;
207 
208     /**
209      * Initializes a new <code>Handler</code> instance for the given view.
210      * @param view the view displaying the mind map.
211      */
212     Handler( final Graph2DView view ) {
213       this.view = view;
214       this.node2button = new WeakHashMap();
215 
216       view.getGraph2D().addGraphListener(this);
217     }
218 
219     /**
220      * Adds collapse buttons on node created/reinserted events and
221      * removes collapse buttons on node removed events.
222      */
223     public void onGraphEvent( final GraphEvent e ) {
224       switch (e.getType()) {
225         case GraphEvent.NODE_CREATION:
226         case GraphEvent.NODE_REINSERTION:
227           onNodeCreated((Node) e.getData());
228           break;
229         case GraphEvent.PRE_NODE_REMOVAL:
230           onNodeDeleted((Node) e.getData());
231           break;
232         case GraphEvent.SUBGRAPH_INSERTION:
233           for (NodeCursor nc = ((NodeList) e.getData()).nodes(); nc.ok(); nc.next()) {
234             onNodeCreated(nc.node());
235           }
236           break;
237         case GraphEvent.SUBGRAPH_REMOVAL:
238           for (NodeCursor nc = ((NodeList) e.getData()).nodes(); nc.ok(); nc.next()) {
239             onNodeDeleted(nc.node());
240           }
241           break;
242       }
243     }
244 
245     /**
246      * Adds a collapse button for the specified node to this handler's
247      * associated view.
248      */
249     private void onNodeCreated( final Node node ) {
250       final CollapseButton button = new CollapseButton(node, view.getGraph2D());
251       node2button.put(node, button);
252       view.addDrawable(button);
253     }
254 
255     /**
256      * Removes the collapse button associated to the specified node from this
257      * handler's associated view.
258      */
259     private void onNodeDeleted( final Node node ) {
260       final Object button = node2button.get(node);
261       if (button instanceof CollapseButton) {
262         view.removeDrawable(((CollapseButton) button));
263       }
264     }
265   }
266 }
267