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.advanced;
29  
30  import demo.view.DemoBase;
31  
32  import java.awt.BorderLayout;
33  import java.awt.Color;
34  import java.awt.Graphics2D;
35  import java.awt.GridLayout;
36  import java.awt.EventQueue;
37  import java.awt.Font;
38  import java.awt.FontMetrics;
39  import java.awt.Rectangle;
40  import java.awt.event.ActionEvent;
41  import java.awt.event.ComponentAdapter;
42  import java.awt.event.ComponentEvent;
43  import java.awt.geom.Rectangle2D;
44  import java.net.URL;
45  import java.util.HashMap;
46  import java.util.HashSet;
47  import java.util.Iterator;
48  import java.util.Locale;
49  import java.util.Set;
50  import javax.swing.AbstractAction;
51  import javax.swing.Action;
52  import javax.swing.ActionMap;
53  import javax.swing.ImageIcon;
54  import javax.swing.InputMap;
55  import javax.swing.JComponent;
56  import javax.swing.JPanel;
57  import javax.swing.JRootPane;
58  import javax.swing.JToolBar;
59  
60  import y.base.Edge;
61  import y.base.EdgeCursor;
62  import y.base.Graph;
63  import y.base.GraphEvent;
64  import y.base.GraphListener;
65  import y.base.Node;
66  import y.base.NodeCursor;
67  import y.layout.ComponentLayouter;
68  import y.layout.Layouter;
69  import y.layout.hierarchic.IncrementalHierarchicLayouter;
70  import y.layout.orthogonal.OrthogonalLayouter;
71  import y.layout.router.polyline.EdgeRouter;
72  import y.layout.tree.BalloonLayouter;
73  import y.layout.tree.TreeReductionStage;
74  import y.view.DefaultBackgroundRenderer;
75  import y.view.EditMode;
76  import y.view.Graph2D;
77  import y.view.Graph2DCopyFactory;
78  import y.view.Graph2DEvent;
79  import y.view.Graph2DListener;
80  import y.view.Graph2DUndoManager;
81  import y.view.Graph2DView;
82  import y.view.Graph2DViewActions;
83  import y.view.Graph2DViewMouseWheelZoomListener;
84  import y.view.ModelViewManager;
85  import y.view.NodeLabel;
86  import y.view.NodeRealizer;
87  import y.view.ShapeNodeRealizer;
88  import y.view.Graph2DLayoutExecutor;
89  
90  /**
91   * Demonstrates automatic structural synchronization between several graphs using 
92   * {@link y.view.ModelViewManager}.
93   * <p>
94   * The demo shows four different Graph2DViews in a 2-by-2 matrix. The top-left one 
95   * presents the model graph, the latter three ones show derived views of this model 
96   * graph.
97   * <br/>
98   * Each of the derived views has special characteristics: for example, the bottom-left 
99   * one does not show any of the edges from the model graph, the bottom-right one 
100  * is empty at first and only shows nodes created interactively by a user. 
101  * Also, in some views the visual representation of the nodes differs from the model 
102  * graph.
103  * </p>
104  * <p>
105  * In all views there can be applied an automatic layout to the contained graph. 
106  * Additionally, the two views at the bottom, which prevent editing of their contained 
107  * graphs, provide a button to synchronize their contents back to the model graph's 
108  * view, which in turn updates the other derived views.
109  * </p>
110  *
111  * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/multiviews" target="_blank">Section Multiple Views on a Common Model Graph</a> in the yFiles for Java Developer's Guide
112  */
113 public class ModelViewManagerDemo extends DemoBase {
114   private final ModelViewManager manager;
115   private final Graph2DView[] subViews;
116   private HashMap view2undoManager = new HashMap(2);
117 
118   public ModelViewManagerDemo() {
119     subViews = new Graph2DView[3];
120 
121     initGraph(view.getGraph2D());
122     manager = ModelViewManager.getInstance(view.getGraph2D());
123 
124     contentPane.remove(view);
125     contentPane.add(createMultiView(), BorderLayout.CENTER);
126   }
127 
128   /**
129    * Creates a sample graph.
130    */
131   private void initGraph( final Graph2D graph ) {
132     graph.clear();
133     graph.getDefaultNodeRealizer().setFillColor(new Color(73, 147, 255));
134 
135     //create nodes
136     final Node[] nodes = new Node[10];
137     for (int i = 0; i < nodes.length; ++i) {
138       nodes[i] = graph.createNode();
139     }
140 
141     //create edges
142     graph.createEdge(nodes[1], nodes[8]);
143     graph.createEdge(nodes[1], nodes[2]);
144     graph.createEdge(nodes[1], nodes[6]);
145     graph.createEdge(nodes[1], nodes[0]);
146     graph.createEdge(nodes[2], nodes[0]);
147     graph.createEdge(nodes[3], nodes[5]);
148     graph.createEdge(nodes[3], nodes[6]);
149     graph.createEdge(nodes[3], nodes[1]);
150     graph.createEdge(nodes[4], nodes[2]);
151     graph.createEdge(nodes[4], nodes[7]);
152     graph.createEdge(nodes[5], nodes[0]);
153     graph.createEdge(nodes[6], nodes[5]);
154     graph.createEdge(nodes[6], nodes[0]);
155     graph.createEdge(nodes[7], nodes[2]);
156     graph.createEdge(nodes[7], nodes[8]);
157     graph.createEdge(nodes[8], nodes[4]);
158     graph.createEdge(nodes[9], nodes[8]);
159     graph.createEdge(nodes[9], nodes[7]);
160 
161     //node labels
162     for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
163       graph.getRealizer(nc.node()).setLabelText(Integer.toString(nc.node().index()));
164     }
165 
166     // calculate an initial hierarchical layout
167     final IncrementalHierarchicLayouter ihl = new IncrementalHierarchicLayouter();
168     new Graph2DLayoutExecutor().doLayout(graph, ihl);
169   }
170 
171   /**
172    * Creates and initializes the views managed by the demo's
173    * <code>ModelViewManager</code>.
174    */
175   private JComponent createMultiView() {
176     final JPanel pane = new JPanel(new GridLayout(2, 2, 1, 1));
177 
178     // shared listeners
179     final LabelChangeHandler labelChangeHandler = new LabelChangeHandler();
180     final Graph2DViewMouseWheelZoomListener mwzl =
181             new Graph2DViewMouseWheelZoomListener();
182 
183     // the model view
184     final JToolBar vtb = createToolBar(view);
185     if (vtb != null) {
186       final JPanel viewAndTools = new JPanel(new BorderLayout());
187       viewAndTools.add(view, BorderLayout.CENTER);
188       viewAndTools.add(vtb, BorderLayout.NORTH);
189       pane.add(viewAndTools);
190     } else {
191       pane.add(view);
192     }
193     view.fitContent();
194     view.getGraph2D().addGraph2DListener(labelChangeHandler);
195     MyBackgroundRenderer.newInstance(view).setText("Editable Model");
196     getUndoManager(view).resetQueue();
197 
198 
199 
200     // create Graph2DViews for the graphs handled as views for the model
201     // in the demo's ModelViewManager
202     for (int i = 0; i < subViews.length; ++i) {
203       subViews[i] = new Graph2DView();
204       subViews[i].setFitContentOnResize(true);
205       final JToolBar svitb = createToolBar(subViews[i]);
206       if (svitb != null) {
207         final JPanel viewAndTools = new JPanel(new BorderLayout());
208         viewAndTools.add(subViews[i], BorderLayout.CENTER);
209         viewAndTools.add(svitb, BorderLayout.NORTH);
210         pane.add(viewAndTools);
211       } else {
212         pane.add(subViews[i]);
213       }
214     }
215 
216 
217 
218     // set up an editable, auto synchronizing view
219     final Graph2D graph = subViews[0].getGraph2D();
220     graph.setGraphCopyFactory(new MyCopyFactory(createRedCircle()));
221 
222     // register the Graph2DView's graph as a graph view of the demo's
223     // ModelViewManager model
224     manager.addViewGraph(graph, null, true);
225     manager.synchronizeModelToViewGraph(graph);
226 
227     graph.setDefaultNodeRealizer(createRedCircle());
228     graph.addGraph2DListener(labelChangeHandler);
229 
230     // configure the Graph2DView for editing
231     final Graph2DViewActions actions = new Graph2DViewActions(subViews[0]);
232     final ActionMap amap = actions.createActionMap();
233     final InputMap imap = actions.createDefaultInputMap(amap);
234     subViews[0].getCanvasComponent().setActionMap(amap);
235     subViews[0].getCanvasComponent().setInputMap(JComponent.WHEN_FOCUSED, imap);
236     subViews[0].getCanvasComponent().addMouseWheelListener(mwzl);
237     subViews[0].addViewMode(new EditMode());
238     MyBackgroundRenderer.newInstance(subViews[0]).setText("Editable View");
239 
240     // calculate an initial layout that differs from the model's layout
241     subViews[0].applyLayout(createOrthogonalLayouter());
242     subViews[0].fitContent();
243     getUndoManager(subViews[0]).resetQueue();
244 
245 
246 
247     // set up non-editable view, that displays nodes only
248     subViews[1].setGraph2D((Graph2D) manager.createViewGraph(
249             new MyCopyFactory(createOrangeOctagon()), new NoEdgesFilter(), false));
250 
251     // configure the Graph2DView
252     subViews[1].fitContent();
253     subViews[1].getCanvasComponent().addMouseWheelListener(mwzl);
254     MyBackgroundRenderer.newInstance(subViews[1]).setText("Non-editable View");
255 
256 
257 
258     // set up non-editable view, that displays only user-created graph elements
259     subViews[2].setGraph2D((Graph2D) manager.createViewGraph(
260             null, new ExcludeFilter(view.getGraph2D()), false));
261 
262     // configure the Graph2DView
263     subViews[2].fitContent();
264     subViews[2].getCanvasComponent().addMouseWheelListener(mwzl);
265     MyBackgroundRenderer.newInstance(subViews[2]).setText("Non-editable View");
266 
267 
268 
269     // ensure that all Graph2DViews are properly refreshed on structural
270     // changes
271     manager.getModel().addGraphListener(new UpdateHandler());
272     for (Iterator it = manager.viewGraphs(); it.hasNext();) {
273       ((Graph) it.next()).addGraphListener(new UpdateHandler());
274     }
275 
276 
277     return pane;
278   }
279 
280   /**
281    * Overwritten to be able to trigger an initial <code>fitContent()</code>
282    * for all the <code>Graph2DView</code>s used in this demo.
283    */
284   public void addContentTo( final JRootPane rootPane ) {
285     super.addContentTo(rootPane);
286     final ComponentAdapter handler = new ComponentAdapter() {
287       private int callCount;
288 
289       public void componentResized( final ComponentEvent e ) {
290         if (callCount < 2) {
291           configureviews();
292         }
293         ++callCount;
294         if (callCount == 2) {
295           rootPane.removeComponentListener(this);
296         }
297       }
298 
299       private void configureviews() {
300         view.fitContent();
301         view.updateView();
302         for (int i = 0; i < subViews.length; ++i) {
303           subViews[i].fitContent();
304           subViews[i].updateView();
305         }
306       }
307     };
308     rootPane.addComponentListener(handler);
309   }
310 
311   /**
312    * Overwritten to prevent the standard toolbar from being created.
313    * Each <code>Graph2DView</code> used in this demo comes with its own
314    * custom toolbar.
315    * @return <code>null</code>.
316    */
317   protected JToolBar createToolBar() {
318     return null;
319   }
320 
321   /**
322    * Create a custom toolbar for the specified <code>Graph2DView</code>.
323    */
324   private JToolBar createToolBar( final Graph2DView view ) {
325     final JToolBar jtb = new JToolBar();
326     jtb.setFloatable(false);
327 
328     // add delete actions for the two editable views
329     if (view == this.view || view == this.subViews[0]) {
330       jtb.add(new DeleteSelection(view));
331     }
332 
333     // add a fit content action for all views
334     jtb.add(new FitContent(view));
335 
336     // add undo/redo for the two editable views
337     if (view == this.view || view == this.subViews[0]) {
338       jtb.addSeparator();
339       jtb.add(createUndoAction(view));
340       jtb.add(createRedoAction(view));
341     }
342 
343     jtb.addSeparator();
344     // add a layout action for each view
345     jtb.add(createActionControl(createLayoutAction(view)));
346 
347     // add a synchronize view contents to model for the two non-editable views
348     if (view == this.subViews[1] || view == this.subViews[2]) {
349       jtb.add(createActionControl(new SynchViewToModel(view)));
350     }
351 
352     return jtb;
353   }
354 
355   private Action createUndoAction(Graph2DView view) {
356     final Action action = getUndoManager(view).getUndoAction();
357     action.putValue(Action.SMALL_ICON, getIconResource("resource/undo.png"));
358     action.putValue(Action.SHORT_DESCRIPTION, "Undo");
359     return action;
360   }
361 
362   private Action createRedoAction(Graph2DView view) {
363     final Action action = getUndoManager(view).getRedoAction();
364     action.putValue(Action.SMALL_ICON, getIconResource("resource/redo.png"));
365     action.putValue(Action.SHORT_DESCRIPTION, "Redo");
366     return action;
367   }
368 
369   private Graph2DUndoManager getUndoManager(Graph2DView view) {
370     Graph2DUndoManager undoManager = (Graph2DUndoManager) view2undoManager.get(view);
371     if(undoManager == null) {
372       undoManager = new Graph2DUndoManager(view.getGraph2D());
373       undoManager.setViewContainer(view);
374       view2undoManager.put(view, undoManager);
375     }
376     return undoManager;
377   }
378 
379   /**
380    * Factory method for layout actions depending on the specified
381    * <code>Graph2DView</code>.
382    */
383   private Action createLayoutAction( final Graph2DView view ) {
384     final Layout layout;
385 
386     if (view == this.view) {
387       // create a hierarchical layout action for the model view
388       layout = new Layout(view, new IncrementalHierarchicLayouter());
389       layout.putValue(Action.SHORT_DESCRIPTION, "Layout Hierarchically");
390     } else if (view == subViews[0]) {
391       // create an orthogonal layout action for the editable non-model view
392       layout = new Layout(view, createOrthogonalLayouter());
393       layout.putValue(Action.SHORT_DESCRIPTION, "Layout Orthogonally");
394     } else if (view == subViews[1]) {
395       // create a grid layout action for the non-editable nodes-only view
396       layout = new Layout(view, new ComponentLayouter());
397       layout.putValue(Action.SHORT_DESCRIPTION, "Layout Component Grid");
398     } else if (view == subViews[2]) {
399       // create a balloon layout action for the non-editable diffs view
400       layout = new Layout(view, createBalloonLayouter());
401       layout.putValue(Action.SHORT_DESCRIPTION, "Layout Balloon-style Tree");
402     } else {
403       layout = new Layout(view, null);
404     }
405 
406     return layout;
407   }
408 
409 
410   public static void main( String[] args ) {
411     try {
412       EventQueue.invokeAndWait(new Runnable() {
413         public void run() {
414           Locale.setDefault(Locale.ENGLISH);
415           initLnF();
416           (new ModelViewManagerDemo()).start();
417         }
418       });
419     } catch (Throwable t) {
420       t.printStackTrace();
421     }
422   }
423 
424   /**
425    * Factory method for a template <code>NodeRealizer</code>.
426    */
427   private static NodeRealizer createRedCircle() {
428     final ShapeNodeRealizer snr = new ShapeNodeRealizer();
429     snr.setShapeType(ShapeNodeRealizer.ELLIPSE);
430     snr.setFillColor(new Color(196, 0, 64));
431     return snr;
432   }
433 
434   /**
435    * Factory method for a template <code>NodeRealizer</code>.
436    */
437   private static NodeRealizer createOrangeOctagon() {
438     final ShapeNodeRealizer snr = new ShapeNodeRealizer();
439     snr.setWidth(50);
440     snr.setShapeType(ShapeNodeRealizer.OCTAGON);
441     snr.setFillColor(new Color(223, 134, 17));
442     return snr;
443   }
444 
445   /**
446    * Factory method for a configured <code>Layouter</code>.
447    */
448   private static Layouter createBalloonLayouter() {
449     final EdgeRouter orthogonal = new EdgeRouter();
450     orthogonal.setReroutingEnabled(true);
451     orthogonal.setSphereOfAction(EdgeRouter.ROUTE_SELECTED_EDGES);
452 
453     final TreeReductionStage trs = new TreeReductionStage();
454     trs.setNonTreeEdgeSelectionKey(EdgeRouter.SELECTED_EDGES);
455     trs.setNonTreeEdgeRouter(orthogonal);
456 
457     final BalloonLayouter bl = new BalloonLayouter();
458     bl.setRootNodePolicy(BalloonLayouter.DIRECTED_ROOT);
459     bl.setPreferredChildWedge(300);
460     bl.setPreferredRootWedge(360);
461     bl.setMinimalEdgeLength(40);
462     bl.setCompactnessFactor(0.5);
463     bl.setAllowOverlaps(false);
464     bl.setFromSketchModeEnabled(false);
465     bl.appendStage(trs);
466     return bl;
467   }
468 
469   /**
470    * Factory method for a configured <code>Layouter</code>.
471    */
472   private static Layouter createOrthogonalLayouter() {
473     final OrthogonalLayouter ol = new OrthogonalLayouter();
474     ol.setLayoutStyle(OrthogonalLayouter.NORMAL_TREE_STYLE);
475     ol.setGrid(25);
476     ol.setUseLengthReduction(true);
477     ol.setUseCrossingPostprocessing(true);
478     ol.setPerceivedBendsOptimizationEnabled(true);
479     ol.setUseRandomization(false);
480     ol.setUseFaceMaximization(true);
481     ol.setUseSketchDrawing(false);
482     return ol;
483   }
484 
485 
486   /**
487    * Custom <code>ModelViewManager.Filter</code> filter implementation
488    * that rejects all edge representatives from being automatically created by a
489    * <code>ModelViewManager</code>.
490    */
491   private static final class NoEdgesFilter implements ModelViewManager.Filter {
492     public boolean acceptInsertion( final Node node ) {
493       return true;
494     }
495 
496     public boolean acceptInsertion( final Edge edge ) {
497       return false;
498     }
499 
500     public boolean acceptRemoval( final Node node ) {
501       return true;
502     }
503 
504     public boolean acceptRemoval( final Edge edge ) {
505       return true;
506     }
507 
508     public boolean acceptRetention( final Node node ) {
509       return true;
510     }
511 
512     public boolean acceptRetention( final Edge edge ) {
513       return true;
514     }
515   }
516 
517   /**
518    * Custom <code>ModelViewManager.Filter</code> filter implementation
519    * that rejects edge and node representatives from being automatically
520    * created by a <code>ModelViewManager</code>, if the corresponding
521    * model element is stored in one of this filter's exclusion sets.
522    */
523   private static final class ExcludeFilter implements ModelViewManager.Filter {
524     final Set excludedNodes;
525     final Set excludedEdges;
526 
527     ExcludeFilter( final Graph graph ) {
528       excludedNodes = new HashSet();
529       excludedEdges = new HashSet();
530 
531       for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
532         excludedNodes.add(nc.node());
533       }
534       for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
535         excludedEdges.add(ec.edge());
536       }
537     }
538 
539     public boolean acceptInsertion( final Node node ) {
540       return !excludedNodes.contains(node);
541     }
542 
543     public boolean acceptInsertion( final Edge edge ) {
544       return !excludedEdges.contains(edge);
545     }
546 
547     public boolean acceptRemoval( final Node node ) {
548       return true;
549     }
550 
551     public boolean acceptRemoval( final Edge edge ) {
552       return true;
553     }
554 
555     public boolean acceptRetention( final Node node ) {
556       return true;
557     }
558 
559     public boolean acceptRetention( final Edge edge ) {
560       return true;
561     }
562   }
563 
564   /**
565    * <code>Graph2DCopyFactory</code> that uses a template
566    * <code>NodeRealizer</code> when copying nodes instead of copying the
567    * original realizer.
568    */
569   private static final class MyCopyFactory extends Graph2DCopyFactory {
570     private NodeRealizer template;
571 
572     MyCopyFactory( final NodeRealizer template ) {
573       setTemplateImpl(template);
574     }
575 
576     protected NodeRealizer copyRealizer( final NodeRealizer nr ) {
577       if (template != null) {
578         // instead of copying the original realizer, create a copy of the
579         // template realizer at an appropriate position
580         final NodeRealizer _nr = template.createCopy();
581         _nr.setCenter(nr.getCenterX(), nr.getCenterY());
582 
583         // manually copy node labels
584         final int lc = nr.labelCount();
585         if (lc > 0) {
586           _nr.setLabel((NodeLabel) nr.getLabel().clone());
587           for (int i = 1; i < lc; ++i) {
588             _nr.addLabel((NodeLabel) nr.getLabel(i).clone());
589           }
590         }
591         return _nr;
592       } else {
593         return nr.createCopy();
594       }
595     }
596 
597     NodeRealizer getTemplate() {
598       return template;
599     }
600 
601     void setTemplate( final NodeRealizer template ) {
602       setTemplateImpl(template);
603     }
604 
605     private void setTemplateImpl( final NodeRealizer template ) {
606       this.template = template != null ? template.createCopy() : null;
607     }
608   }
609 
610   /**
611    * <code>BackgroundRenderer</code> that displays a short text message.
612    */
613   private static final class MyBackgroundRenderer
614           extends DefaultBackgroundRenderer {
615     private String text;
616     private Color textColor;
617     private final Rectangle r;
618 
619     MyBackgroundRenderer( final Graph2DView view ) {
620       super(view);
621       textColor = new Color(192, 192, 192);
622       r = new Rectangle(0, 0, -1, 1);
623     }
624 
625     String getText() {
626       return text;
627     }
628 
629     void setText( final String text ) {
630       this.text = text;
631     }
632 
633     Color getTextColor() {
634       return textColor;
635     }
636 
637     void setTextColor( final Color color ) {
638       this.textColor = color;
639     }
640 
641     public void paint(
642             final Graphics2D gfx,
643             final int x,
644             final int y,
645             final int w,
646             final int h ) {
647       super.paint(gfx, x, y, w, h);
648       paintText(gfx);
649     }
650 
651     private void paintText( final Graphics2D gfx ) {
652       if (text != null && textColor != null) {
653         final Color oldColor = gfx.getColor();
654         final Font oldFont = gfx.getFont();
655 
656         undoWorldTransform(gfx);
657 
658         gfx.setColor(textColor);
659         gfx.setFont(oldFont.deriveFont(30.0f));
660 
661         view.getBounds(r);
662         r.setLocation(0, 0);
663         final FontMetrics fm = gfx.getFontMetrics();
664         final Rectangle2D bnds = fm.getStringBounds(text, gfx);
665         final float textX = (float) (r.x + (r.width - bnds.getWidth()) * 0.5);
666         final float textY = (float) (r.y + (r.height - bnds.getHeight()) * 0.5 + fm.getMaxAscent());
667         gfx.drawString(text, textX, textY);
668 
669         redoWorldTransform(gfx);
670 
671         gfx.setFont(oldFont);
672         gfx.setColor(oldColor);
673       }
674     }
675 
676 
677     static MyBackgroundRenderer newInstance( final Graph2DView view ) {
678       final MyBackgroundRenderer mbr = new MyBackgroundRenderer(view);
679       view.setBackgroundRenderer(mbr);
680       return mbr;
681     }
682   }
683 
684   /**
685    * <code>GraphListener</code> that updates all {@link y.view.View}s
686    * associated to source of an structural change.
687    */
688   private static class UpdateHandler implements GraphListener {
689     private int block;
690 
691     public void onGraphEvent( final GraphEvent e ) {
692       if (e.getGraph() instanceof Graph2D) {
693         switch (e.getType()) {
694           case GraphEvent.PRE_EVENT:
695             ++block;
696             break;
697           case GraphEvent.POST_EVENT:
698             --block;
699             break;
700           default:
701             break;
702         }
703         if (block == 0) {
704           ((Graph2D) e.getGraph()).updateViews();
705         }
706       }
707     }
708   }
709 
710   /**
711    * <code>Graph2DListener</code> that propagates label text changes to the
712    * model and all views of the demo's <code>ModelViewManager</code>.
713    */
714   private class LabelChangeHandler implements Graph2DListener {
715     private boolean armed;
716 
717     LabelChangeHandler() {
718       armed = true;
719     }
720 
721     public void onGraph2DEvent( final Graph2DEvent e ) {
722       if (!armed) {
723         return;
724       }
725 
726       if ("text".equals(e.getPropertyName()) &&
727           e.getSubject() instanceof NodeLabel) {
728         final NodeLabel nl = (NodeLabel) e.getSubject();
729         setLabelText(nl.getNode(), nl.getText());
730       }
731     }
732 
733     private void setLabelText( final Node node, final String text ) {
734       armed = false;
735 
736       final Node mn;
737       final Graph2D model = view.getGraph2D();
738       if (node.getGraph() != model) {
739         // determine the model representative of node
740         mn = manager.getModelNode(node);
741         if (mn != null) {
742           // set the label text for the model representative
743           model.getRealizer(mn).setLabelText(text);
744           model.updateViews();
745         }
746       } else {
747         mn = node;
748       }
749 
750       if (mn != null) {
751         for (Iterator it = manager.viewGraphs(); it.hasNext();) {
752           final Graph2D graph = ((Graph2D) it.next());
753           // determine the view representative of node
754           final Node vn = manager.getViewNode(mn, graph);
755           if (vn != null && vn != node) {
756             // set the label text for the view representative
757             graph.getRealizer(vn).setLabelText(text);
758             graph.updateViews();
759           }
760         }
761       }
762 
763       armed = true;
764     }
765   }
766 
767   /**
768    * <code>Action</code> that synchronizes the contents of the graph of its
769    * associated view to the model of the demo's <code>ModelViewManager</code.
770    */
771   private final class SynchViewToModel extends AbstractAction {
772     private final Graph2DView view;
773 
774     SynchViewToModel( final Graph2DView view ) {
775       super("Synchronize");
776       this.view = view;
777       final URL imageURL = ClassLoader.getSystemResource(
778               "demo/view/advanced/resource/Export16.gif");
779       if (imageURL != null) {
780         this.putValue(Action.SMALL_ICON, new ImageIcon(imageURL));
781       }
782       this.putValue(Action.SHORT_DESCRIPTION, "Synchronize View to Model");
783     }
784 
785     public void actionPerformed( final ActionEvent e ) {
786       manager.synchronizeViewGraphToModel(view.getGraph2D());
787 
788       ModelViewManagerDemo.this.view.fitContent();
789       ModelViewManagerDemo.this.view.updateView();
790       for (int i = 0; i < subViews.length; ++i) {
791         subViews[i].fitContent();
792         subViews[i].updateView();
793       }
794     }
795   }
796 
797   /**
798    * <code>Action</code> that calculates a layout for the graph of its
799    * associated view using its associated layout algorithm.
800    */
801   private static final class Layout extends AbstractAction {
802     private final Graph2DView view;
803     private final Layouter layouter;
804 
805     Layout( final Graph2DView view, final Layouter layouter ) {
806       super("Layout");
807       this.view = view;
808       this.layouter = layouter;
809       this.putValue(Action.SMALL_ICON, getIconResource("resource/layout.png"));
810       this.putValue(Action.SHORT_DESCRIPTION, "Layout Graph");
811     }
812 
813     public void actionPerformed( final ActionEvent e ) {
814       view.applyLayout(layouter);
815 
816       view.fitContent();
817       view.updateView();
818     }
819   }
820 }
821