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.view.rendering;
15  
16  import demo.view.DemoBase;
17  import y.geom.YRectangle;
18  import y.view.BevelNodePainter;
19  import y.view.GenericNodeRealizer;
20  import y.view.LineType;
21  import y.view.NodeLabel;
22  import y.view.NodePort;
23  import y.view.NodeRealizer;
24  import y.view.PortConfigurationAdapter;
25  import y.view.SelectionPortPainter;
26  import y.view.ShadowNodePainter;
27  import y.view.ShapeNodeRealizer;
28  import y.view.ShapePortConfiguration;
29  import y.view.ShinyPlateNodePainter;
30  import y.view.YLabel;
31  import y.view.YRenderingHints;
32  import y.view.hierarchy.DefaultGenericAutoBoundsFeature;
33  import y.view.hierarchy.DefaultHierarchyGraphFactory;
34  import y.view.hierarchy.GenericGroupNodeRealizer;
35  import y.view.hierarchy.GroupNodePainter;
36  import y.view.hierarchy.HierarchyManager;
37  
38  import java.awt.BorderLayout;
39  import java.awt.Color;
40  import java.awt.Component;
41  import java.awt.EventQueue;
42  import java.awt.GradientPaint;
43  import java.awt.Graphics2D;
44  import java.awt.GridBagConstraints;
45  import java.awt.GridBagLayout;
46  import java.awt.GridLayout;
47  import java.awt.Insets;
48  import java.awt.Paint;
49  import java.awt.RenderingHints;
50  import java.awt.Shape;
51  import java.awt.Stroke;
52  import java.awt.event.ActionEvent;
53  import java.awt.geom.Rectangle2D;
54  import java.awt.geom.RoundRectangle2D;
55  import java.util.HashMap;
56  import java.util.Locale;
57  import java.util.Map;
58  import javax.swing.AbstractAction;
59  import javax.swing.AbstractButton;
60  import javax.swing.Action;
61  import javax.swing.BorderFactory;
62  import javax.swing.ButtonGroup;
63  import javax.swing.JCheckBox;
64  import javax.swing.JComponent;
65  import javax.swing.JPanel;
66  import javax.swing.JRadioButton;
67  import javax.swing.JSplitPane;
68  
69  /**
70   * Demonstrates how to control the level of detail for graph visualization
71   * using rendering hints.
72   * The following hints can be set (or unset):
73   * <ul>
74   * <li>{@link YRenderingHints#KEY_GROUP_STATE_PAINTING}</li>
75   * <li>{@link YRenderingHints#KEY_EDGE_LABEL_PAINTING}</li>
76   * <li>{@link YRenderingHints#KEY_NODE_LABEL_PAINTING}</li>
77   * <li>{@link YRenderingHints#KEY_NODE_PORT_PAINTING}</li>
78   * <li>{@link YRenderingHints#KEY_GRADIENT_PAINTING}</li>
79   * <li>{@link YRenderingHints#KEY_SELECTION_PAINTING}</li>
80   * <li>{@link YRenderingHints#KEY_SHADOW_PAINTING}</li>
81   * <li>{@link ShapeNodeRealizer#KEY_SLOPPY_RECT_PAINTING}</li>
82   * <li>{@link YRenderingHints#KEY_SLOPPY_POLYLINE_PAINTING}</li>
83   * </ul>
84   * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/mvc_view.html#rendering_hints">Section View Implementations</a> in the yFiles for Java Developer's Guide
85   * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/realizers.html#cls_ShapeNodeRealizer">Section Bringing Graph Elements to Life: The Realizer Concept</a> in the yFiles for Java Developer's Guide
86   */
87  public class LevelOfDetailDemo extends DemoBase {
88    /**
89     * Painter implementation that demonstrates how to query rendering hints.
90     * The implementation respects the following rendering hints:
91     * <ul>
92     * <li>{@link YRenderingHints#KEY_NODE_LABEL_PAINTING}</li>
93     * <li>{@link YRenderingHints#KEY_NODE_PORT_PAINTING}</li>
94     * <li>{@link YRenderingHints#KEY_GRADIENT_PAINTING}</li>
95     * <li>{@link YRenderingHints#KEY_SELECTION_PAINTING}</li>
96     * <li>{@link ShapeNodeRealizer#KEY_SLOPPY_RECT_PAINTING}</li>
97     * </ul>
98     */
99    static final class HeaderNodePainter implements GenericNodeRealizer.Painter {
100     static final Color BACKGROUND = new Color(153, 204, 255);
101 
102     /**
103      * Checks whether or not sloppy painting should use "nice" shapes.
104      * @param graphics the graphics context to check for rendering hints.
105      * @return <code>true</code> if "nice" shapes should be used;
106      * <code>false</code> otherwise.
107      */
108     static boolean isSloppyShapePaintingEnabled( final Graphics2D graphics ) {
109       return ShapeNodeRealizer.VALUE_SLOPPY_RECT_PAINTING_OFF.equals(
110               graphics.getRenderingHint(ShapeNodeRealizer.KEY_SLOPPY_RECT_PAINTING));
111     }
112 
113     /**
114      * Checks whether or not node ports should be painted.
115      * The default behavior is to paint node ports for detail rendering but not
116      * for sloppy rendering.
117      * @param graphics the graphics context to check for rendering hints.
118      * @param sloppy the rendering mode.
119      * @return <code>true</code> if node ports should be painted;
120      * <code>false</code> otherwise.
121      */
122     static boolean shouldPaintPorts( final Graphics2D graphics, final boolean sloppy ) {
123       if (sloppy) {
124         return YRenderingHints.VALUE_NODE_PORT_PAINTING_ON.equals(
125                 graphics.getRenderingHint(YRenderingHints.KEY_NODE_PORT_PAINTING));
126       } else {
127         return !YRenderingHints.VALUE_NODE_PORT_PAINTING_OFF.equals(
128                 graphics.getRenderingHint(YRenderingHints.KEY_NODE_PORT_PAINTING));
129       }
130     }
131 
132     /**
133      * Checks whether or not node labels should be painted.
134      * The default behavior is to paint node labels for detail rendering but not
135      * for sloppy rendering.
136      * @param graphics the graphics context to check for rendering hints.
137      * @param sloppy the rendering mode.
138      * @return <code>true</code> if labels should be painted;
139      * <code>false</code> otherwise.
140      */
141     static boolean shouldPaintLabels( final Graphics2D graphics, final boolean sloppy ) {
142       if (sloppy) {
143         return YRenderingHints.VALUE_NODE_LABEL_PAINTING_ON.equals(
144                 graphics.getRenderingHint(YRenderingHints.KEY_NODE_LABEL_PAINTING));
145       } else {
146         return !YRenderingHints.VALUE_NODE_LABEL_PAINTING_OFF.equals(
147                 graphics.getRenderingHint(YRenderingHints.KEY_NODE_LABEL_PAINTING));
148       }
149     }
150 
151 
152     public void paint( final NodeRealizer context, final Graphics2D graphics ) {
153       paint(context, graphics, false);
154     }
155 
156     public void paintSloppy( final NodeRealizer context, final Graphics2D graphics ) {
157       paint(context, graphics, true);
158     }
159 
160     /**
161      * Paints the node in detail and sloppy rendering mode.
162      * @param context the node context.
163      * @param graphics the graphics context to paint upon.
164      * @param sloppy the rendering mode.
165      */
166     void paint(
167             final NodeRealizer context,
168             final Graphics2D graphics,
169             final boolean sloppy
170     ) {
171       if (!context.isVisible()) {
172         return;
173       }
174 
175       if (!sloppy &&
176           context.isSelected() &&
177           // check whether or not selection state should be respected for
178           // rendering
179           YRenderingHints.isSelectionPaintingEnabled(graphics)) {
180         context.paintHotSpots(graphics);
181       }
182 
183       final Shape shape = createShape(context, graphics, sloppy);
184       paintFilledShape(context, graphics, shape, sloppy);
185       paintShapeBorder(context, graphics, shape);
186 
187       // check whether or not node ports should be painted
188       if (shouldPaintPorts(graphics, sloppy)) {
189         context.paintPorts(graphics);
190       }
191 
192       // check whether or not node labels should be painted
193       if (shouldPaintLabels(graphics, sloppy)) {
194         context.paintText(graphics);
195       }
196     }
197 
198     /**
199      * Creates the shape that represents the node.
200      * @param context the node context. Provides geometry for the node's shape.
201      * @param graphics the graphics context to check for rendering hints.
202      * @param sloppy the rendering mode.
203      * @return the shape that represents the node.
204      */
205     Shape createShape(
206             final NodeRealizer context,
207             final Graphics2D graphics,
208             final boolean sloppy
209     ) {
210       // check whether or not a "nice" shape should be used for sloppy rendering
211       if (sloppy && !isSloppyShapePaintingEnabled(graphics)) {
212         // use "simply" shape
213         return new Rectangle2D.Double(
214                 context.getX(), context.getY(),
215                 context.getWidth(), context.getHeight());
216       } else {
217         // use "nice" shape
218         return new RoundRectangle2D.Double(
219                 context.getX(), context.getY(),
220                 context.getWidth(), context.getHeight(),
221                 8, 8);
222       }
223     }
224 
225     /**
226      * Paints the node interior.
227      * @param context the node context. Provides color/paint for the node's
228      * interior.
229      * @param graphics the graphics context to paint upon.
230      * @param shape the node's shape.
231      * @param sloppy the rendering mode.
232      */
233     void paintFilledShape(
234             final NodeRealizer context,
235             final Graphics2D graphics,
236             final Shape shape,
237             final boolean sloppy
238     ) {
239       if (context.isTransparent()) {
240         return;
241       }
242 
243       // check whether or not gradient paints should be used
244       if (sloppy || !YRenderingHints.isGradientPaintingEnabled(graphics)) {
245         // use flat colors (no gradient paints)
246         final Color fc = context.getFillColor();
247         if (fc != null) {
248           final Color oldColor = graphics.getColor();
249 
250           graphics.setColor(fc);
251           graphics.fill(shape);
252 
253           graphics.setColor(oldColor);
254         }
255       } else {
256         // use gradient paints
257         Color fc1 = context.getFillColor();
258         Color fc2 = context.getFillColor2();
259         if (fc1 != null || fc2 != null) {
260           final Paint oldPaint = graphics.getPaint();
261 
262           if (fc1 == null) {
263             fc1 = new Color(fc2.getRed(), fc2.getGreen(), fc2.getBlue(), 0);
264           }
265           if (fc2 == null) {
266             fc2 = new Color(fc1.getRed(), fc1.getGreen(), fc1.getBlue(), 0);
267           }
268 
269           final float y = (float) context.getY();
270           final double x = context.getX();
271           final double w = context.getWidth();
272           graphics.setPaint(new GradientPaint(
273                   (float) (x + w * 0.33), y, fc1,
274                   (float) (x + w), y, fc2));
275           graphics.fill(shape);
276 
277           graphics.setPaint(oldPaint);
278         }
279       }
280 
281       // paint the header
282       if (context.labelCount() > 0) {
283         final Shape oldClip = graphics.getClip();
284         final Color oldColor = graphics.getColor();
285 
286         final YRectangle r = context.getLabel().getBox();
287         if (oldClip == null) {
288           final double x = context.getX();
289           final double w = context.getWidth();
290           final double minX = Math.min(r.x, x);
291           final double maxX = Math.max(r.x + r.width, x + w);
292           graphics.clip(new Rectangle2D.Double(minX, r.getY(), maxX - minX, r.getHeight()));
293         } else {
294           final Rectangle2D cb = oldClip.getBounds2D();
295           graphics.clip(new Rectangle2D.Double(cb.getX(), r.getY(), cb.getWidth(), r.getHeight()));
296         }
297         graphics.setColor(BACKGROUND);
298         graphics.fill(shape);
299 
300         graphics.setColor(oldColor);
301         graphics.setClip(oldClip);
302       }
303     }
304 
305     /**
306      * Paints the node's border.
307      * @param context the node context. Provides color and stroke for the node's
308      * border.
309      * @param graphics the graphics context to paint upon.
310      * @param shape the node's shape.
311      */
312     void paintShapeBorder(
313             final NodeRealizer context,
314             final Graphics2D graphics,
315             final Shape shape
316     ) {
317       final Color lc = context.getLineColor();
318       if (lc != null) {
319         final LineType lt = context.getLineType();
320         if (lt != null) {
321           final Color oldColor = graphics.getColor();
322           final Stroke oldStroke = graphics.getStroke();
323 
324           graphics.setColor(lc);
325           graphics.setStroke(lt);
326           graphics.draw(shape);
327 
328           graphics.setStroke(oldStroke);
329           graphics.setColor(oldColor);
330         }
331       }
332     }
333   }
334 
335 
336 
337   /**
338    * {@link GenericNodeRealizer} configuration name for group nodes.
339    */
340   private static final String CONFIGURATION_GROUP = "GroupingDemo_GROUP_NODE";
341   /**
342    * {@link GenericNodeRealizer} configuration name for nodes with a bevel
343    * border.
344    */
345   private static final String CONFIGURATION_BEVEL = "LevelOfDetailDemo#BevelNode";
346   /**
347    * {@link NodePort} configuration name.
348    */
349   private static final String CONFIGURATION_PORT = "LevelOfDetailDemo#Port";
350 
351   /**
352    * {@link y.view.Graph2DView#getPaintDetailThreshold() paintDetailThreshold}
353    * value used in the demo
354    */
355   private static final double PAINT_DETAIL_THRESHOLD = 0.5;
356 
357   /**
358    * Store this setting for demo use in the DemoBrowser to be able to reset it when
359    * stopping the demo or starting the next demo.
360    */
361   private boolean fractionMetricsForSizeCalculationEnabled;
362 
363   public LevelOfDetailDemo() {
364     this(null);
365   }
366 
367   public LevelOfDetailDemo( final String helpFilePath ) {
368     // support for group nodes
369     new HierarchyManager(view.getGraph2D());
370 
371     configureDefaultGroupNodeRealizers();
372     configureDefaultPorts();
373 
374 
375     view.setPaintDetailThreshold(PAINT_DETAIL_THRESHOLD);
376     view.getRenderingHints().put(
377             RenderingHints.KEY_FRACTIONALMETRICS,
378             RenderingHints.VALUE_FRACTIONALMETRICS_ON);
379     // Stores the value to be able to reset it when running the demo in the DemoBrowser,
380     // so this setting cannot effect other demos.
381     fractionMetricsForSizeCalculationEnabled = YLabel.isFractionMetricsForSizeCalculationEnabled();
382     YLabel.setFractionMetricsForSizeCalculationEnabled(true);
383 
384 
385     // create gui
386     final GridBagConstraints gbc = new GridBagConstraints();
387     final JPanel controls = new JPanel(new GridBagLayout());
388     gbc.anchor = GridBagConstraints.NORTHWEST;
389     gbc.fill = GridBagConstraints.NONE;
390     gbc.gridx = 0;
391     gbc.gridy = 0;
392     gbc.weightx = 0;
393     gbc.weighty = 0;
394     controls.add(createOptionPane(), gbc);
395     gbc.fill = GridBagConstraints.BOTH;
396     ++gbc.gridy;
397     gbc.weightx = 1;
398     gbc.weighty = 1;
399     controls.add(new JPanel(), gbc);
400 
401     contentPane.remove(view);
402     contentPane.add(new JSplitPane(
403             JSplitPane.HORIZONTAL_SPLIT,
404             controls,
405             view), BorderLayout.CENTER);
406 
407     addHelpPane(helpFilePath);
408 
409 
410     loadInitialGraph();
411   }
412 
413   /**
414    * Cleans up.
415    * This method is called by the demo browser when the demo is stopped or another demo starts.
416    */
417   public void dispose() {
418     YLabel.setFractionMetricsForSizeCalculationEnabled(fractionMetricsForSizeCalculationEnabled);
419   }
420 
421   /**
422    * Loads an initial sample graph.
423    */
424   protected void loadInitialGraph() {
425     loadGraph("resource/LevelOfDetailDemo.graphml");
426   }
427 
428   /**
429    * Creates a control to enable/disable various rendering hints.
430    * @return a control to enable/disable various rendering hints.
431    */
432   private Component createOptionPane() {
433     final GridBagConstraints gbc = new GridBagConstraints();
434     final JPanel optionPane = new JPanel(new GridBagLayout());
435     gbc.anchor = GridBagConstraints.CENTER;
436     gbc.fill = GridBagConstraints.HORIZONTAL;
437     gbc.gridx = 0;
438     gbc.gridy = 0;
439     gbc.insets = new Insets(0, 0, 10, 0);
440     gbc.weightx = 1;
441     gbc.weighty = 0;
442 
443     // control for coarse grained level of detail
444     final StateEditor se = new StateEditor();
445     se.controls[0].setText("Depends on zoom");
446     se.controls[1].setText("Always detailed");
447     se.controls[2].setText("Always sloppy");
448     se.setAction(new AbstractAction("Level of Detail") {
449       public void actionPerformed( final ActionEvent e ) {
450         if (Boolean.TRUE.equals(se.getState())) {
451           // always use realizer's paint method
452           view.setPaintDetailThreshold(0);
453         } else if (Boolean.FALSE.equals(se.getState())) {
454           // always use realizer's paintSloppy method
455           view.setPaintDetailThreshold(Double.MAX_VALUE);
456         } else {
457           // if zoom < PAINT_DETAIL_THRESHOLD use paintSloppy
458           // else use paint
459           view.setPaintDetailThreshold(PAINT_DETAIL_THRESHOLD);
460         }
461         view.updateView();
462       }
463     });
464     optionPane.add(se, gbc);
465 
466     gbc.insets = new Insets(0, 0, 0, 0);
467     ++gbc.gridy;
468     optionPane.add(createOnOffDefaultControl(
469             YRenderingHints.KEY_GROUP_STATE_PAINTING,
470             YRenderingHints.VALUE_GROUP_STATE_PAINTING_ON,
471             YRenderingHints.VALUE_GROUP_STATE_PAINTING_OFF
472     ), gbc);
473     ++gbc.gridy;
474     optionPane.add(createOnOffDefaultControl(
475             YRenderingHints.KEY_EDGE_LABEL_PAINTING,
476             YRenderingHints.VALUE_EDGE_LABEL_PAINTING_ON,
477             YRenderingHints.VALUE_EDGE_LABEL_PAINTING_OFF
478     ), gbc);
479     ++gbc.gridy;
480     optionPane.add(createOnOffDefaultControl(
481             YRenderingHints.KEY_NODE_LABEL_PAINTING,
482             YRenderingHints.VALUE_NODE_LABEL_PAINTING_ON,
483             YRenderingHints.VALUE_NODE_LABEL_PAINTING_OFF
484     ), gbc);
485     ++gbc.gridy;
486     optionPane.add(createOnOffDefaultControl(
487             YRenderingHints.KEY_NODE_PORT_PAINTING,
488             YRenderingHints.VALUE_NODE_PORT_PAINTING_ON,
489             YRenderingHints.VALUE_NODE_PORT_PAINTING_OFF
490     ), gbc);
491 
492     ++gbc.gridy;
493     optionPane.add(createOnOffControl(
494             YRenderingHints.KEY_GRADIENT_PAINTING,
495             YRenderingHints.VALUE_GRADIENT_PAINTING_OFF
496     ), gbc);
497     ++gbc.gridy;
498     optionPane.add(createOnOffControl(
499             YRenderingHints.KEY_SELECTION_PAINTING,
500             YRenderingHints.VALUE_SELECTION_PAINTING_OFF
501     ), gbc);
502     ++gbc.gridy;
503     optionPane.add(createOnOffControl(
504             YRenderingHints.KEY_SHADOW_PAINTING,
505             YRenderingHints.VALUE_SHADOW_PAINTING_OFF
506     ), gbc);
507     ++gbc.gridy;
508     optionPane.add(createOnOffControl(
509             ShapeNodeRealizer.KEY_SLOPPY_RECT_PAINTING,
510             ShapeNodeRealizer.VALUE_SLOPPY_RECT_PAINTING_OFF
511     ), gbc);
512     ++gbc.gridy;
513     optionPane.add(createOnOffControl(
514         YRenderingHints.KEY_SLOPPY_POLYLINE_PAINTING,
515         YRenderingHints.VALUE_SLOPPY_POLYLINE_PAINTING_OFF
516     ), gbc);
517 
518     return optionPane;
519   }
520 
521   /**
522    * Creates a control to set one of three states for a rendering hint,
523    * <code>on</code>, <code>off</code>, or not used.
524    * @param key the rendering hint key to set.
525    * @param valueOn the appropriate <code>on</code> value for the rendering
526    * hint.
527    * @param valueOff the appropriate <code>off</code> value for the rendering
528    * hint.
529    * @return a control to set one of three states for a rendering hint,
530    * <code>on</code>, <code>off</code>, or not used.
531    */
532   private Component createOnOffDefaultControl(
533           final RenderingHints.Key key,
534           final Object valueOn,
535           final Object valueOff
536   ) {
537     String name = key.toString();
538     if (name.endsWith(" key")) {
539       name = name.substring(0, name.length() - 4);
540     }
541     if (name.endsWith(" enable")) {
542       name = name.substring(0, name.length() - 7);
543     }
544 
545     final StateEditor se = new StateEditor();
546     se.setAction(new AbstractAction(name) {
547       public void actionPerformed( final ActionEvent e ) {
548         final RenderingHints hints = view.getRenderingHints();
549         if (Boolean.TRUE.equals(se.getState())) {
550           hints.put(key, valueOn);
551         } else if (Boolean.FALSE.equals(se.getState())) {
552           hints.put(key, valueOff);
553         } else {
554           hints.remove(key);
555         }
556         view.updateView();
557       }
558     });
559     return se;
560   }
561 
562   /**
563    * Creates a control to set or unset a rendering hint.
564    *
565    * @param key the rendering hint key to set.
566    * @param value the value to be set for the rendering hint.
567    * @return a control to set or unset a rendering hint.
568    */
569   private JComponent createOnOffControl(
570           final RenderingHints.Key key,
571           final Object value
572   ) {
573     String name = key.toString();
574     if (name.endsWith(" key")) {
575       name = name.substring(0, name.length() - 4);
576     }
577     if (name.endsWith(" enable")) {
578       name = name.substring(0, name.length() - 7);
579     }
580 
581     final JCheckBox jcb = new JCheckBox();
582     jcb.setSelected(true);
583     jcb.setAction(new AbstractAction(name) {
584       public void actionPerformed( final ActionEvent e ) {
585         final RenderingHints hints = view.getRenderingHints();
586         if (jcb.isSelected()) {
587           hints.remove(key);
588         } else {
589           hints.put(key, value);
590         }
591         view.updateView();
592       }
593     });
594 
595     return jcb;
596   }
597 
598   /**
599    * Overwritten to register a configuration for nodes with bevel borders.
600    * @see #CONFIGURATION_BEVEL
601    */
602   protected void configureDefaultRealizers() {
603     super.configureDefaultRealizers();
604 
605     final GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
606     final Map map = factory.createDefaultConfigurationMap();
607     final BevelNodePainter painter = new BevelNodePainter();
608     painter.setDrawShadow(true);
609     map.put(GenericNodeRealizer.Painter.class, painter);
610     factory.addConfiguration(CONFIGURATION_BEVEL, map);
611   }
612 
613   /**
614    * Configures the default group (and folder) nodes.
615    * @see #CONFIGURATION_GROUP
616    */
617   private void configureDefaultGroupNodeRealizers() {
618     //Create additional configuration for default group node realizers
619     final Map map = GenericGroupNodeRealizer.createDefaultConfigurationMap();
620 
621     final GroupNodePainter gnp = new GroupNodePainter(new HeaderNodePainter());
622     map.put(GenericNodeRealizer.Painter.class, new ShadowNodePainter(gnp));
623     map.put(GenericNodeRealizer.ContainsTest.class, gnp);
624     map.put(GenericNodeRealizer.GenericMouseInputEditorProvider.class, gnp);
625     map.put(GenericNodeRealizer.Initializer.class, gnp);
626 
627     final DefaultGenericAutoBoundsFeature abf = new DefaultGenericAutoBoundsFeature();
628     abf.setConsiderNodeLabelSize(true);
629     map.put(GenericGroupNodeRealizer.GenericAutoBoundsFeature.class, abf);
630     map.put(GenericNodeRealizer.GenericSizeConstraintProvider.class, abf);
631     map.put(GenericNodeRealizer.LabelBoundsChangedHandler.class, abf);
632 
633     final GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
634     factory.addConfiguration(CONFIGURATION_GROUP, map);
635 
636 
637     final GenericGroupNodeRealizer gnr = new GenericGroupNodeRealizer();
638 
639     //Register first, since this will also configure the node label
640     gnr.setConfiguration(CONFIGURATION_GROUP);
641 
642     //Nicer colors
643     gnr.setFillColor(new Color(228, 245, 255));
644     gnr.setFillColor2(Color.WHITE);
645     gnr.setLineColor(new Color(102, 102, 153));
646     final NodeLabel label = gnr.getLabel();
647     label.setBackgroundColor(null);
648     label.setTextColor(Color.BLACK);
649     label.setFontSize(15);
650 
651 
652     //Set default group and folder node realizers
653     final DefaultHierarchyGraphFactory hgf = (DefaultHierarchyGraphFactory)
654             view.getGraph2D().getHierarchyManager().getGraphFactory();
655 
656     hgf.setProxyNodeRealizerEnabled(true);
657 
658     hgf.setDefaultGroupNodeRealizer(gnr.createCopy());
659     hgf.setDefaultFolderNodeRealizer(gnr.createCopy());
660   }
661 
662   /**
663    * Registers a configuration for node ports that are represented by a
664    * small blue ball.
665    * @see #CONFIGURATION_PORT
666    */
667   private void configureDefaultPorts() {
668     // there is only one very simple dedicated port painter/port contains test
669     // implementation available (ShapePortConfiguration)
670     // luckily node painters/node contains tests can be reused
671 
672     // step 1: create a node configuration to be used as port configuration
673     final ShinyPlateNodePainter impl = new ShinyPlateNodePainter();
674     impl.setDrawShadow(false);
675     final HashMap nodeImpls = new HashMap();
676     nodeImpls.put(GenericNodeRealizer.ContainsTest.class, impl);
677     nodeImpls.put(GenericNodeRealizer.Painter.class, impl);
678 
679     // step 2: create an adapter for the node configuration
680     final PortConfigurationAdapter adapter =
681             new PortConfigurationAdapter(nodeImpls);
682     adapter.setFillColor(new Color(51, 102, 255));
683     adapter.setFillColor2(null);
684     adapter.setLineColor(new Color(0, 102, 255));
685 
686     // step 3: register the adapted node configuration as a port configuration
687     final ShapePortConfiguration boundsProvider = new ShapePortConfiguration();
688     boundsProvider.setSize(16, 16);
689 
690     final NodePort.Factory factory = NodePort.getFactory();
691     final Map portImpls = factory.createDefaultConfigurationMap();
692     portImpls.put(NodePort.ContainsTest.class, adapter);
693     final SelectionPortPainter painter = new SelectionPortPainter(adapter);
694     painter.setStyle(SelectionPortPainter.STYLE_SMOOTHED);
695     portImpls.put(NodePort.Painter.class, painter);
696     portImpls.put(NodePort.BoundsProvider.class, boundsProvider);
697     factory.addConfiguration(CONFIGURATION_PORT, portImpls);
698   }
699 
700   public static void main( String[] args ) {
701     EventQueue.invokeLater(new Runnable() {
702       public void run() {
703         Locale.setDefault(Locale.ENGLISH);
704         initLnF();
705         (new LevelOfDetailDemo("resource/levelofdetailhelp.html")).start();
706       }
707     });
708   }
709 
710 
711   /**
712    * An implementation of a tri-state component.
713    */
714   private static final class StateEditor extends JComponent {
715     private final AbstractButton[] controls;
716     private Action action;
717     private Boolean state;
718     private String name;
719 
720     StateEditor() {
721       setLayout(new GridLayout(3, 1));
722       name = "";
723       final ButtonGroup group = new ButtonGroup();
724       controls = new AbstractButton[3];
725       for (int i = 0; i < controls.length; ++i) {
726         controls[i] = new JRadioButton();
727         group.add(controls[i]);
728         add(controls[i]);
729       }
730       controls[0].setSelected(true);
731       controls[0].setAction(new AbstractAction("Default") {
732         public void actionPerformed( final ActionEvent e ) {
733           state = null;
734           fireActionPerformed();
735         }
736       });
737       controls[1].setAction(new AbstractAction("On") {
738         public void actionPerformed( final ActionEvent e ) {
739           state = Boolean.TRUE;
740           fireActionPerformed();
741         }
742       });
743       controls[2].setAction(new AbstractAction("Off") {
744         public void actionPerformed( final ActionEvent e ) {
745           state = Boolean.FALSE;
746           fireActionPerformed();
747         }
748       });
749       setBorder(BorderFactory.createTitledBorder(""));
750     }
751 
752     /**
753      * Returns the state of the component.
754      * @return either {@link Boolean#TRUE}, {@link Boolean#FALSE}, or
755      * <code>null</code>.
756      */
757     public Boolean getState() {
758       return state;
759     }
760 
761     /**
762      * Sets the state of the component.
763      * @param state either {@link Boolean#TRUE}, {@link Boolean#FALSE}, or
764      * <code>null</code>.
765      */
766     public void setState( final Boolean state ) {
767       if (state == null) {
768         if (this.state != state) {
769           controls[0].setSelected(true);
770         }
771       } else if (Boolean.TRUE == state) {
772         if (this.state != state) {
773           controls[1].setSelected(true);
774         }
775       } else if (Boolean.FALSE == state) {
776         if (this.state != state) {
777           controls[2].setSelected(true);
778         }
779       }
780     }
781 
782     /**
783      * Returns the currently set <code>Action</code> that is triggered upon
784      * state changes or <code>null</code> if no <code>Action</code> is set.
785      * @return the currently set <code>Action</code> that is triggered upon
786      * state changes or <code>null</code> if no <code>Action</code> is set.
787      */
788     public Action getAction() {
789       return action;
790     }
791 
792     /**
793      * Sets the <code>Action</code> that is triggered upon state changes.
794      * @param action the <code>Action</code> that is triggered upon state
795      * changes.
796      */
797     public void setAction( final Action action ) {
798       this.action = action;
799       String name = "";
800       if (action != null) {
801         final Object value = action.getValue(Action.NAME);
802         if (value instanceof String) {
803           name = (String) value;
804         }
805       }
806       setBorder(BorderFactory.createTitledBorder(name));
807       this.name = name;
808     }
809 
810     private void fireActionPerformed() {
811       final Action action = this.action;
812       if (action != null) {
813         action.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, name));
814       }
815     }
816   }
817 }
818