1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.layout.hierarchic;
15  
16  import demo.view.DemoDefaults;
17  
18  import y.base.DataMap;
19  import y.base.EdgeCursor;
20  import y.base.EdgeList;
21  import y.base.Node;
22  import y.base.NodeCursor;
23  import y.geom.YInsets;
24  import y.layout.grouping.Grouping;
25  import y.layout.hierarchic.IncrementalHierarchicLayouter;
26  import y.layout.hierarchic.incremental.IncrementalHintsFactory;
27  import y.util.Maps;
28  import y.view.EditMode;
29  import y.view.GenericNodeRealizer;
30  import y.view.GenericNodeRealizer.Factory;
31  import y.view.Graph2D;
32  import y.view.Graph2DCanvas;
33  import y.view.Graph2DLayoutExecutor;
34  import y.view.HitInfo;
35  import y.view.LineType;
36  import y.view.NodeLabel;
37  import y.view.NodeRealizer;
38  import y.view.ShinyPlateNodePainter;
39  import y.view.SmartNodeLabelModel;
40  import y.view.TooltipMode;
41  import y.view.ViewMode;
42  import y.view.hierarchy.DefaultHierarchyGraphFactory;
43  import y.view.hierarchy.GenericGroupNodeRealizer;
44  import y.view.hierarchy.GroupLayoutConfigurator;
45  import y.view.hierarchy.HierarchyManager;
46  import y.view.tabular.TableGroupNodeRealizer;
47  import y.view.tabular.TableGroupNodeRealizer.Column;
48  import y.view.tabular.TableGroupNodeRealizer.Table;
49  import y.view.tabular.TableNodePainter;
50  import y.view.tabular.TableStyle;
51  
52  import java.awt.Color;
53  import java.awt.EventQueue;
54  import java.awt.event.ActionEvent;
55  import java.awt.event.MouseEvent;
56  import java.util.Locale;
57  import java.util.Map;
58  
59  import javax.swing.AbstractAction;
60  import javax.swing.JToolBar;
61  
62  /**
63   * This demo shows the effect of combining
64   * <code>IncrementalHierarchicLayouter</code>'s support for grouping and
65   * swim lanes.
66   * <p>
67   * Things to try:
68   * </p>
69   * <ul>
70   *   <li>
71   *     Drag a node or set of nodes into another swim lane.
72   *     This will automatically trigger an incremental layout calculation.
73   *   </li>
74   *   <li>
75   *     Create a new node. It will be assigned to either a new swim lane if
76   *     created to the left or right of the existing lanes or to the lane in
77   *     which the node's center lies.
78   *     This will automatically trigger an incremental layout calculation.
79   *   </li>
80   *   <li>
81   *     Open/close folder/group nodes. Upon closing a group node, the resulting
82   *     folder node will be assigned to the minimum swim lane of the group's
83   *     child nodes.
84   *     This will automatically trigger an incremental layout calculation.
85   *   </li>
86   * </ul>
87   *
88   */
89  public class SwimlaneGroupDemo extends IncrementalHierarchicGroupDemo {
90    private static final Color NODE_COLOR = DemoDefaults.DEFAULT_NODE_COLOR;
91    private static final Color NODE_GRADIENT_COLOR = Color.WHITE;
92    private static final Color NODE_LINE_COLOR = DemoDefaults.DEFAULT_NODE_LINE_COLOR;
93    private static final Color GROUP_NODE_COLOR = new Color(255, 255, 255, 127);
94    private static final Color GROUP_NODE_LINE_COLOR = DemoDefaults.DEFAULT_NODE_COLOR;
95    private static final Color GROUP_NODE_LABEL_COLOR = DemoDefaults.DEFAULT_NODE_COLOR;
96    private static final Color ODD_LANE_COLOR = DemoDefaults.DEFAULT_CONTRAST_COLOR;
97    private static final Color EVEN_LANE_COLOR = new Color(237, 247, 247);
98  
99    private static final String NODE_CONFIGURATION = "NODE_CONFIGURATION";
100   private static final String SWIMLANE_CONFIGURATION = "SWIMLANE_CONFIGURATION";
101 
102   static {
103     initConfigurations();
104   }
105 
106 
107   public SwimlaneGroupDemo() {
108     view.addViewMode(new TriggerIncrementalLayout());
109     configureRealizers(view.getGraph2D());
110     loadInitialGraph();
111   }
112 
113   /**
114    * Creates a sample graph to display initially.
115    */
116   protected void loadInitialGraph() {
117     final Graph2D graph = view.getGraph2D();
118     graph.clear();
119    
120     HierarchyManager hierarchy = getHierarchyManager();
121     
122     if (layouter != null && hierarchy != null) {
123       // create a dummy node that visualizes swim lanes
124       final TableGroupNodeRealizer tgnr = new TableGroupNodeRealizer();
125       tgnr.setConfiguration(SWIMLANE_CONFIGURATION);
126 
127       tgnr.setLabelText("Swimlane Pool");
128       NodeLabel label = tgnr.getLabel();
129       SmartNodeLabelModel labelModel = new SmartNodeLabelModel();
130       label.setLabelModel(labelModel);
131       label.setModelParameter(labelModel.createDiscreteModelParameter(SmartNodeLabelModel.POSITION_TOP));
132       // "removes" the label from the graph view,
133       // but keeps it in the tree component
134       tgnr.getLabel().setVisible(false);
135 
136       // configure swim lane colors
137       final TableStyle.SimpleStyle oddLane =
138           new TableStyle.SimpleStyle(null, null, ODD_LANE_COLOR);
139       tgnr.setStyleProperty(TableNodePainter.COLUMN_STYLE_ID, oddLane);
140       tgnr.setStyleProperty(TableNodePainter.COLUMN_SELECTION_STYLE_ID, oddLane);
141 
142       final TableStyle.SimpleStyle evenLane =
143           new TableStyle.SimpleStyle(null, null, EVEN_LANE_COLOR);
144       tgnr.setStyleProperty(TableNodePainter.ALTERNATE_COLUMN_STYLE_ID, evenLane);
145       tgnr.setStyleProperty(TableNodePainter.ALTERNATE_COLUMN_SELECTION_STYLE_ID, evenLane);
146 
147       final TableStyle.SimpleStyle none = new TableStyle.SimpleStyle();
148       tgnr.setStyleProperty(TableNodePainter.ROW_STYLE_ID, none);
149       tgnr.setStyleProperty(TableNodePainter.ROW_SELECTION_STYLE_ID, none);
150       tgnr.setStyleProperty(TableNodePainter.TABLE_STYLE_ID, none);
151       tgnr.setStyleProperty(TableNodePainter.TABLE_SELECTION_STYLE_ID, none);
152 
153       // configure swim lane insets and minimum size
154       tgnr.setDefaultColumnInsets(new YInsets(25, 5, 0, 5));
155       tgnr.setDefaultMinimumColumnWidth(50);
156       tgnr.setDefaultRowInsets(new YInsets(15, 0, 15, 0));
157 
158       // label swim lanes
159       final Column[] columns = new Column[9];
160       final Table table = tgnr.getTable();
161       for (int i = 0; i < columns.length; ++i) {
162         columns[i] = i == 0 ? table.getColumn(0) : table.addColumn();
163 
164         final NodeLabel nl = tgnr.createNodeLabel();
165         nl.setText("Lane " + (i + 1));
166         tgnr.configureColumnLabel(nl, columns[i], true, 0);
167         tgnr.addLabel(nl);
168       }
169 
170       tgnr.updateTableBounds();
171 
172 
173       final Node pool = hierarchy.createGroupNode(graph);
174       graph.setRealizer(pool, tgnr);
175 
176       final Node n00 = graph.createNode();
177       final Node n01 = graph.createNode();
178       final Node g03 = hierarchy.createGroupNode(graph);
179       final Node g04 = hierarchy.createGroupNode(graph);
180       final Node n05 = graph.createNode();
181       final Node n06 = graph.createNode();
182       final Node n07 = graph.createNode();
183       final Node g08 = hierarchy.createGroupNode(graph);
184       final Node n09 = graph.createNode();
185       final Node n10 = graph.createNode();
186       final Node n11 = graph.createNode();
187       final Node g12 = hierarchy.createGroupNode(graph);
188       final Node n13 = graph.createNode();
189       final Node n14 = graph.createNode();
190       final Node n15 = graph.createNode();
191       final Node n16 = graph.createNode();
192       final Node n17 = graph.createNode();
193       final Node g18 = hierarchy.createGroupNode(graph);
194       final Node n19 = graph.createNode();
195       final Node n20 = graph.createNode();
196       final Node n21 = graph.createNode();
197       final Node n22 = graph.createNode();
198       final Node n23 = graph.createNode();
199 
200       // configure node nesting hierarchy
201       hierarchy.setParentNode(n00, pool);
202       hierarchy.setParentNode(n01, pool);
203 
204       hierarchy.setParentNode(g03, pool);
205       hierarchy.setParentNode(g04, pool);
206       hierarchy.setParentNode(n05, pool);
207       hierarchy.setParentNode(n06, pool);
208       hierarchy.setParentNode(n07, pool);
209 
210       hierarchy.setParentNode(g08, g03);
211       hierarchy.setParentNode(n09, g03);
212       hierarchy.setParentNode(n10, g03);
213       hierarchy.setParentNode(n11, g03);
214 
215       hierarchy.setParentNode(g12, g08);
216       hierarchy.setParentNode(n13, g08);
217       hierarchy.setParentNode(n14, g08);
218 
219       hierarchy.setParentNode(n15, g12);
220       hierarchy.setParentNode(n16, g12);
221       hierarchy.setParentNode(n17, g12);
222 
223       hierarchy.setParentNode(g18, g04);
224       hierarchy.setParentNode(n19, g04);
225       hierarchy.setParentNode(n20, g04);
226 
227       hierarchy.setParentNode(n21, g18);
228       hierarchy.setParentNode(n22, g18);
229       hierarchy.setParentNode(n23, g18);
230 
231 
232       hierarchy.createEdge(n00, n01);
233       hierarchy.createEdge(n01, n06);
234       hierarchy.createEdge(n06, n07);
235       hierarchy.createEdge(n06, n05);
236       hierarchy.createEdge(n06, n20);
237       hierarchy.createEdge(n07, n11);
238       hierarchy.createEdge(n09, n05);
239       hierarchy.createEdge(n10, n05);
240       hierarchy.createEdge(n11, n09);
241       hierarchy.createEdge(n11, n14);
242       hierarchy.createEdge(n13, n09);
243       hierarchy.createEdge(n14, n13);
244       hierarchy.createEdge(n14, n15);
245       hierarchy.createEdge(n15, n13);
246       hierarchy.createEdge(n15, n17);
247       hierarchy.createEdge(n16, n13);
248       hierarchy.createEdge(n17, n16);
249       hierarchy.createEdge(n19, n05);
250       hierarchy.createEdge(n20, n19);
251       hierarchy.createEdge(n20, n21);
252       hierarchy.createEdge(n21, n22);
253       hierarchy.createEdge(n21, n23);
254       hierarchy.createEdge(n21, n05);
255       hierarchy.createEdge(n22, n05);
256       hierarchy.createEdge(n23, n05);
257 
258       // create initial swim lane affiliations for nodes
259       table.moveToColumn(n00, columns[8]);
260       table.moveToColumn(n01, columns[5]);
261       // g02
262       // g03
263       // g04
264       table.moveToColumn(n05, columns[5]);
265       table.moveToColumn(n06, columns[5]);
266       table.moveToColumn(n07, columns[1]);
267       // g08
268       table.moveToColumn(n09, columns[1]);
269       table.moveToColumn(n10, columns[0]);
270       table.moveToColumn(n11, columns[1]);
271       // g12
272       table.moveToColumn(n13, columns[2]);
273       table.moveToColumn(n14, columns[2]);
274       table.moveToColumn(n15, columns[3]);
275       table.moveToColumn(n16, columns[2]);
276       table.moveToColumn(n17, columns[4]);
277       // g18
278       table.moveToColumn(n19, columns[7]);
279       table.moveToColumn(n20, columns[6]);
280       table.moveToColumn(n21, columns[6]);
281       table.moveToColumn(n22, columns[6]);
282       table.moveToColumn(n23, columns[6]);
283 
284       // update node labels to display swim lane affiliation
285       initLabels(graph);
286 
287       layout();
288     }
289 
290     view.fitContent();
291     view.getGraph2D().updateViews();
292   }
293 
294   /**
295    * Updates node labels to display either group or folder state or
296    * for normal nodes the associated swim lane.
297    */
298   private void initLabels(final Graph2D graph) {
299     Node tableNode = null;
300     Table table = null;
301     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
302       final NodeRealizer nr = graph.getRealizer(nc.node());
303       if (nr instanceof TableGroupNodeRealizer) {
304         tableNode = nc.node();
305         table = ((TableGroupNodeRealizer) nr).getTable();
306         break;
307       }
308     }
309 
310     HierarchyManager hierarchy = getHierarchyManager();
311     
312     int grp = 0;
313     int fldr = 0;
314     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
315       if (nc.node() == tableNode) {
316         continue;
317       }
318             
319       if (hierarchy.isNormalNode(nc.node())) {
320         final Column column = table == null ? null : table.getColumn(nc.node());
321         if (column != null) {
322           graph.getRealizer(nc.node()).setLabelText(
323               Integer.toString(column.getIndex() + 1));
324         } else {
325           graph.getRealizer(nc.node()).setLabelText("");
326         }
327         NodeLabel label = graph.getRealizer(nc.node()).getLabel();
328         SmartNodeLabelModel model = new SmartNodeLabelModel();
329         label.setLabelModel(model);
330         label.setModelParameter(model.getDefaultParameter());
331       } else if (hierarchy.isGroupNode(nc.node())) {
332         graph.getRealizer(nc.node()).setLabelText(
333             "Group " + (++grp));
334       } else if (hierarchy.isFolderNode(nc.node())) {
335         graph.getRealizer(nc.node()).setLabelText(
336             "Folder " + (++fldr));
337       }
338     }
339   }
340 
341   /*
342   * #####################################################################
343   * overriden methods
344   * #####################################################################
345   */
346 
347   /**
348    * Overwritten to configure incremental layout to take swim lane pool nodes
349    * into account.
350    */
351   void layoutIncrementally() {
352     Graph2D graph = view.getGraph2D();
353 
354     layouter.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_INCREMENTAL);
355 
356     // create storage for both nodes and edges
357     DataMap incrementalElements = Maps.createHashedDataMap();
358     // configure the mode
359     final IncrementalHintsFactory ihf = layouter.createIncrementalHintsFactory();
360 
361     //prepare grouping information
362     final GroupLayoutConfigurator glc = new GroupLayoutConfigurator(graph);
363     glc.prepareAll();
364     final Grouping grouping = new Grouping(graph);
365 
366     //mark incremental elements
367     for (NodeCursor nc = graph.selectedNodes(); nc.ok(); nc.next()) {
368       Node n = nc.node();
369       incrementalElements.set(n, ihf.createLayerIncrementallyHint(nc.node()));
370       if (grouping.isGroupNode(n)) {
371         //also mark the group node's incoming/outgoing edges
372         EdgeList markedEdges = grouping.getEdgesGoingIn(n);
373         markedEdges.addAll(grouping.getEdgesGoingOut(n));
374         for (EdgeCursor ec = markedEdges.edges(); ec.ok(); ec.next()) {
375           incrementalElements.set(ec.edge(), ihf.createSequenceIncrementallyHint(ec.edge()));
376         }
377       }
378     }
379     grouping.dispose();
380     glc.restoreAll();
381 
382     for (EdgeCursor ec = graph.selectedEdges(); ec.ok(); ec.next()) {
383       incrementalElements.set(ec.edge(), ihf.createSequenceIncrementallyHint(ec.edge()));
384     }
385     graph.addDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY, incrementalElements);
386 
387     try {
388       final Graph2DLayoutExecutor layoutExecutor =
389               new Graph2DLayoutExecutor(Graph2DLayoutExecutor.ANIMATED);
390       layoutExecutor.setConfiguringTableNodeRealizers(true);
391       layoutExecutor.doLayout(view, layouter);
392     } finally {
393       graph.removeDataProvider(IncrementalHierarchicLayouter.INCREMENTAL_HINTS_DPKEY);
394     }
395 
396     // update node labels to display swim lane affiliation
397     initLabels(graph);
398     graph.updateViews();
399   }
400 
401   /**
402    * Overwritten to configure layout to take swim lane pool nodes into account.
403    */
404   void layout() {
405     final Graph2D graph = view.getGraph2D();
406     layouter.setLayoutMode(IncrementalHierarchicLayouter.LAYOUT_MODE_FROM_SCRATCH);
407 
408     final Graph2DLayoutExecutor layoutExecutor = new Graph2DLayoutExecutor();
409     layoutExecutor.setConfiguringTableNodeRealizers(true);
410     layoutExecutor.doLayout(view, layouter);
411 
412     // update node labels to display swim lane affiliation
413     initLabels(graph);
414     graph.updateViews();
415   }
416 
417   /*
418   * #####################################################################
419   * GUI
420   * #####################################################################
421   */
422 
423   protected void addLayoutActions(JToolBar toolBar) {
424     toolBar.addSeparator();
425     toolBar.add(createActionControl(new AbstractAction(
426             "Layout", SHARED_LAYOUT_ICON) {
427       public void actionPerformed(ActionEvent e) {
428         layout();
429       }
430     }));
431   }
432 
433   protected EditMode createEditMode() {
434     final EditMode editMode = new EditMode() {
435       protected void nodeCreated(final Node v) {
436         layoutIncrementally();
437       }      
438     };
439     // listen for clicks on state icon +/- of group and folder nodes.  
440     editMode.getMouseInputMode().setNodeSearchingEnabled(true);
441 
442     // do not automatically create node labels as these are used to display
443     // swim lane affiliation of nodes
444     editMode.assignNodeLabel(false);
445 
446     // activate child node creation when clicking into group nodes
447     editMode.setChildNodeCreationEnabled(true);
448     
449     return editMode;
450   }
451 
452   /**
453    * Creates a custom {@link TooltipMode} which shows tooltips for nodes, edges and "lanes".
454    */
455   protected TooltipMode createTooltipMode() {
456     return new TooltipMode() {
457       protected String getNodeTip(Node node) {
458         NodeRealizer nodeRealizer = view.getGraph2D().getRealizer(node);
459         if (nodeRealizer instanceof TableGroupNodeRealizer) {
460           // get a tooltip text for the column where the mouse is located
461           MouseEvent event = getLastMoveEvent();
462           Graph2DCanvas canvas = (Graph2DCanvas) view.getCanvasComponent();
463           double x = canvas.translateX(event.getX());
464           double y = canvas.translateY(event.getY());
465           // the first label is the table title in this case, so start with the second one
466           int index = ((TableGroupNodeRealizer) nodeRealizer).getTable().columnAt(x,y).getIndex() + 1;
467           return nodeRealizer.getLabel(index).getText();
468         } else {
469           // use the default tooltip text for all other node types
470           return super.getNodeTip(node);
471         }
472       }
473     };
474   }
475 
476   void configureRealizers(final Graph2D graph) {
477     graph.setDefaultNodeRealizer(createDefaultNodeRealizer());
478     final DefaultHierarchyGraphFactory hgf =
479         (DefaultHierarchyGraphFactory) graph.getHierarchyManager().getGraphFactory();
480     hgf.setDefaultGroupNodeRealizer(createDefaultGroupNodeRealizer());
481     hgf.setDefaultFolderNodeRealizer(createDefaultFolderNodeRealizer());
482   }
483 
484   private NodeRealizer createDefaultNodeRealizer() {
485     Factory factory = GenericNodeRealizer.getFactory();
486     Map map = factory.createDefaultConfigurationMap();
487     ShinyPlateNodePainter painter = new ShinyPlateNodePainter();
488     map.put(GenericNodeRealizer.Painter.class, painter);
489     map.put(GenericNodeRealizer.ContainsTest.class, painter);
490     factory.addConfiguration(NODE_CONFIGURATION, map);
491     GenericNodeRealizer gnr = new GenericNodeRealizer(NODE_CONFIGURATION);
492     gnr.setFillColor(NODE_COLOR);
493     gnr.setLineColor(NODE_LINE_COLOR);
494     gnr.setFillColor2(NODE_GRADIENT_COLOR);
495     gnr.setLineType(LineType.LINE_1);
496     return gnr;
497   }
498 
499   private NodeRealizer createDefaultGroupNodeRealizer() {
500     GenericGroupNodeRealizer defaultGroup = new GenericGroupNodeRealizer();
501     defaultGroup.setConfiguration(CONFIGURATION_GROUP);
502     defaultGroup.setSize(100, 60);
503     defaultGroup.setFillColor(GROUP_NODE_COLOR);
504     defaultGroup.setGroupClosed(false);
505     defaultGroup.setLineType(LineType.LINE_2);
506     defaultGroup.setLineColor(GROUP_NODE_LINE_COLOR);
507     defaultGroup.getLabel().setBackgroundColor(GROUP_NODE_LABEL_COLOR);
508     defaultGroup.getLabel().setTextColor(getBlackOrWhite(GROUP_NODE_LABEL_COLOR));   
509     return defaultGroup;
510   }
511 
512   private NodeRealizer createDefaultFolderNodeRealizer() {
513     GenericGroupNodeRealizer defaultFolder = new GenericGroupNodeRealizer();
514     defaultFolder.setConfiguration(CONFIGURATION_GROUP);    
515     defaultFolder.setSize(100, 60);
516     defaultFolder.setFillColor(GROUP_NODE_COLOR);
517     defaultFolder.setGroupClosed(true);
518     defaultFolder.setLineType(LineType.LINE_2);
519     defaultFolder.setLineColor(GROUP_NODE_LINE_COLOR);
520     defaultFolder.getLabel().setBackgroundColor(GROUP_NODE_LABEL_COLOR);
521     defaultFolder.getLabel().setTextColor(getBlackOrWhite(GROUP_NODE_LABEL_COLOR));
522     return defaultFolder;
523   }
524 
525   private Color getBlackOrWhite(Color c) {
526     if (c.getRed() + c.getGreen() + c.getBlue() > 3 * 127) {
527       return Color.BLACK;
528     } else {
529       return Color.WHITE;
530     }
531   }
532 
533 
534   public static void main(String[] args) {
535     EventQueue.invokeLater(new Runnable() {
536       public void run() {
537         Locale.setDefault(Locale.ENGLISH);
538         initLnF();
539         (new SwimlaneGroupDemo()).start();
540       }
541     });
542   }
543 
544 
545   /**
546    * Registers the configuration for the <code>TableGroupNodeRealizer</code>
547    * that is used to display swim lanes.
548    */
549   private static void initConfigurations() {
550     // create a configuration that uses alternating colors for swim lanes
551     final Map map = TableGroupNodeRealizer.createDefaultConfigurationMap();
552     map.put(TableGroupNodeRealizer.Painter.class,
553         TableNodePainter.newAlternatingColumnsInstance());
554     map.put(TableGroupNodeRealizer.GenericMouseInputEditorProvider.class, null);
555 
556     // register the configuration
557     TableGroupNodeRealizer.getFactory()
558         .addConfiguration(SWIMLANE_CONFIGURATION, map);
559   }
560 
561 
562   /**
563    * <code>ViewMode</code> that triggers an incremental layout calculation
564    * after node drag operations.
565    */
566   class TriggerIncrementalLayout extends ViewMode {
567     private boolean dragging;
568     private boolean hasHitNodes;
569 
570     TriggerIncrementalLayout() {
571       this.dragging = false;
572       this.hasHitNodes = false;
573     }
574 
575     public void mouseDraggedLeft(final double x, final double y) {
576       dragging = true;
577     }
578 
579     public void mousePressedLeft(final double x, final double y) {
580       final HitInfo info = new HitInfo(view, x, y, true, HitInfo.NODE);
581       hasHitNodes = info.hasHitNodes();
582     }
583 
584     public void mouseReleasedLeft(final double x, final double y) {
585       if (dragging && hasHitNodes) {
586         layoutIncrementally();
587       }
588       hasHitNodes = false;
589       dragging = false;
590     }
591   }
592 }
593