1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.7. 
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-2009 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.application;
15  
16  import demo.view.DemoBase;
17  import demo.view.DemoDefaults;
18  import y.base.Node;
19  import y.geom.YInsets;
20  import y.layout.LayoutOrientation;
21  import y.layout.NodeLabelModel;
22  import y.layout.hierarchic.IncrementalHierarchicLayouter;
23  import y.layout.hierarchic.incremental.SimplexNodePlacer;
24  import y.option.RealizerCellRenderer;
25  import y.util.DataProviders;
26  import y.view.CreateEdgeMode;
27  import y.view.Drawable;
28  import y.view.DropSupport;
29  import y.view.EditMode;
30  import y.view.GenericNodeRealizer;
31  import y.view.Graph2D;
32  import y.view.Graph2DLayoutExecutor;
33  import y.view.Graph2DUndoManager;
34  import y.view.Graph2DView;
35  import y.view.Graph2DViewActions;
36  import y.view.HitInfo;
37  import y.view.LineType;
38  import y.view.MultiplexingNodeEditor;
39  import y.view.NodeRealizer;
40  import y.view.ShapeNodePainter;
41  import y.view.Graph2DListener;
42  import y.view.Graph2DEvent;
43  import y.view.NodeLabel;
44  import y.view.hierarchy.GenericGroupNodeRealizer;
45  import y.view.hierarchy.HierarchyManager;
46  import y.view.tabular.ColumnDropTargetListener;
47  import y.view.tabular.RowDropTargetListener;
48  import y.view.tabular.TableGroupNodeRealizer;
49  import y.view.tabular.TableGroupNodeRealizer.ColumnNodeLabelModel;
50  import y.view.tabular.TableGroupNodeRealizer.Column;
51  import y.view.tabular.TableGroupNodeRealizer.Row;
52  import y.view.tabular.TableGroupNodeRealizer.RowNodeLabelModel;
53  import y.view.tabular.TableLabelEditor;
54  import y.view.tabular.TableNodePainter;
55  import y.view.tabular.TableOrderEditor;
56  import y.view.tabular.TableSelectionEditor;
57  import y.view.tabular.TableSizeEditor;
58  import y.view.tabular.TableStyle;
59  import y.view.tabular.TableSupport;
60  
61  import java.awt.BorderLayout;
62  import java.awt.Color;
63  import java.awt.Component;
64  import java.awt.Dimension;
65  import java.awt.EventQueue;
66  import java.awt.Graphics;
67  import java.awt.Graphics2D;
68  import java.awt.Paint;
69  import java.awt.Rectangle;
70  import java.awt.Stroke;
71  import java.awt.dnd.DnDConstants;
72  import java.awt.dnd.DragGestureEvent;
73  import java.awt.dnd.DragGestureListener;
74  import java.awt.dnd.DragSource;
75  import java.awt.event.ActionEvent;
76  import java.awt.geom.Rectangle2D;
77  import java.io.IOException;
78  import java.net.URL;
79  import java.util.Collection;
80  import java.util.Iterator;
81  import java.util.List;
82  import java.util.Map;
83  import javax.swing.AbstractAction;
84  import javax.swing.Action;
85  import javax.swing.DefaultListCellRenderer;
86  import javax.swing.Icon;
87  import javax.swing.ImageIcon;
88  import javax.swing.JButton;
89  import javax.swing.JComponent;
90  import javax.swing.JEditorPane;
91  import javax.swing.JList;
92  import javax.swing.JScrollPane;
93  import javax.swing.JToolBar;
94  import javax.swing.ListSelectionModel;
95  import javax.swing.ListCellRenderer;
96  import javax.swing.border.LineBorder;
97  
98  /**
99   * <p>Demonstrates how to use and customize {@link y.view.tabular.TableGroupNodeRealizer} to work as a pool
100  * having several swim lanes and milestones.</p>
101  * <p>A list using {@link y.view.tabular.RowDropTargetListener} and
102  * {@link y.view.tabular.ColumnDropTargetListener} is added to showcase how additional rows and
103  * columns can be added via drag'n'drop.</p>
104  * <p>Two different ways to customize the rendering of rows and columns are used:</p>
105  * <ul>
106  * <li>For columns, customized {@link y.view.tabular.TableStyle.SimpleStyle SimpleStyles} are registered as style properties
107  * of the <code>TableGroupNodeRealizer</code> which are used by the default column sub painter.</li>
108  * <li>For rows, a custom row sub painter is used that alternates the fill color of childless rows while rendering rows
109  * with children in a third color.</li>
110  * </ul>
111  */
112 public class SwimlaneDemo extends DemoBase {
113   static final String CONFIGURATION_GROUP_NODE = "CONFIGURATION_GROUP_NODE";
114   static final String CONFIGURATION_TABLE_NODE = "CONFIGURATION_TABLE_NODE";
115 
116   static {
117     initConfigurations();
118   }
119 
120 
121   private YInsets rowInsets;
122   private YInsets columnInsets;
123   private Graph2DUndoManager undoManager;
124   protected String helpFile;
125   private MinimumSizeManager minimumSizeManager;
126 
127 
128   public static void main( String[] args ) {
129     EventQueue.invokeLater(new Runnable() {
130       public void run() {
131         initLnF();
132         final SwimlaneDemo demo = new SwimlaneDemo();
133         demo.setHelpFile("resource/swimlanehelp.html");
134         demo.start();
135       }
136     });
137   }
138 
139   private NodeRealizer createConfiguredNormalNodeRealizer() {
140     final NodeRealizer normalNode = view.getGraph2D().getDefaultNodeRealizer().createCopy();
141     normalNode.setSize(80, 50);
142     return normalNode;
143   }
144 
145   private NodeRealizer createConfiguredGroupNodeRealizer() {
146     final GenericGroupNodeRealizer ggnr = new GenericGroupNodeRealizer();
147     ggnr.setConfiguration(CONFIGURATION_GROUP_NODE);
148     ggnr.setFillColor(null);
149     ggnr.setLineType(LineType.DASHED_DOTTED_1);
150     ggnr.removeLabel(ggnr.getLabel());
151     ggnr.setGroupClosed(false);
152     ggnr.setSize(80, 50);
153     return ggnr;
154   }
155 
156   private NodeRealizer createConfiguredTableNodeRealizer() {
157     final TableGroupNodeRealizer tgnr = new TableGroupNodeRealizer();
158     tgnr.setConfiguration(CONFIGURATION_TABLE_NODE);
159 
160     // background color used for the TableGroupNodeRealizer and therefore per default for the table.
161     tgnr.setFillColor(new Color(236, 245, 255));
162 
163     // use custom styles for selected and unselected columns
164     final Color columnFillColor = new Color(113, 146, 178);
165     tgnr.setStyleProperty(
166             TableNodePainter.COLUMN_STYLE_ID,
167             new TableStyle.SimpleStyle(
168                     tgnr.getLineType(),
169                     tgnr.getLineColor(),
170                     columnFillColor,
171                     null,
172                     null,
173                     columnFillColor
174             )
175     );
176 
177     final LineType lt = tgnr.getLineType();
178     final Color columnSelectedFillColor = new Color(55, 93, 129);
179     tgnr.setStyleProperty(
180             TableNodePainter.COLUMN_SELECTION_STYLE_ID,
181             new TableStyle.SimpleStyle(
182                     LineType.getLineType((int) Math.ceil(lt.getLineWidth()) + 2, lt.getLineStyle()),
183                     tgnr.getLineColor(),
184                     columnSelectedFillColor,
185                     null,
186                     null,
187                     columnSelectedFillColor
188             )
189     );
190 
191     // Defaults for columns and rows should be set before those of the table.
192     // This way the defaults are also applied to the first row and column which
193     // are automatically added to the table on it's first access.
194     tgnr.setDefaultColumnWidth(600);
195     tgnr.setDefaultMinimumColumnWidth(200);
196     tgnr.setDefaultColumnInsets(columnInsets);
197     tgnr.setDefaultRowHeight(150);
198     tgnr.setDefaultMinimumRowHeight(50);
199     tgnr.setDefaultRowInsets(rowInsets);
200     tgnr.setAutoResize(true);
201 
202     final TableGroupNodeRealizer.Table table = tgnr.getTable();
203     table.setInsets(new YInsets(30, 0, 0, 0));
204 
205     tgnr.setSize(250, 200);
206     return tgnr;
207   }
208 
209   /**
210    * Adds configurations for nodes with a bevel node style and those using a
211    * {@link y.view.tabular.TableGroupNodeRealizer} to the factory.
212    */
213   private static void initConfigurations() {
214     final GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
215 
216     final Map groupMap = createGroupNodeConfiguration();
217     factory.addConfiguration(CONFIGURATION_GROUP_NODE, groupMap);
218 
219     final Map tableMap = createTableNodeConfiguration();
220     factory.addConfiguration(CONFIGURATION_TABLE_NODE, tableMap);
221   }
222 
223   protected void initialize() {
224     // mark all edges for orthogonal routing during interactive editing
225     view.getGraph2D().addDataProvider(
226             EditMode.ORTHOGONAL_ROUTING_DPKEY,
227             DataProviders.createConstantDataProvider(Boolean.TRUE));
228 
229     // a hierarchy manager has to be used for table group nodes to work.
230     new HierarchyManager(view.getGraph2D());
231 
232     minimumSizeManager = new MinimumSizeManager(view.getGraph2D());
233 
234     rowInsets = new YInsets(0, 30, 0, 0);
235     columnInsets = new YInsets(30, 5, 0, 5);
236     final DropSupport dropSupport = createDropSupport(view);
237 
238     contentPane.add(createDragNDropList(dropSupport), BorderLayout.WEST);
239 
240     loadGraph( "resource/SwimlaneDemo.graphml" );
241 
242     undoManager = new Graph2DUndoManager(view.getGraph2D());
243     undoManager.setViewContainer(view);
244 
245     view.setPreferredSize(new Dimension(950, 550));
246     view.fitContent();
247     view.updateView();
248   }
249 
250   protected void loadGraph( final URL resource ) {
251     // disable the size manager because loading a graph results in lots of
252     // label text property changes
253     minimumSizeManager.setEnabled(false);
254     try {
255       super.loadGraph(resource);
256     } finally {
257       minimumSizeManager.setEnabled(true);
258     }
259   }
260 
261   /**
262    * Sets the path to the help file used to describe this demo and adds a component including this help file to the
263    * content pane.
264    * As this setter isn't called if the demo is viewed in the DemoBrowser, the help component is only displayed when using
265    * the main method of this demo.
266    *
267    * @param helpFile The path to the help file.
268    */
269   public void setHelpFile(String helpFile) {
270     if (this.helpFile == null) {
271       this.helpFile = helpFile;
272       contentPane.add(createHelpPane(getClass().getResource(helpFile)), BorderLayout.EAST);
273     }
274   }
275 
276   private static DropSupport createDropSupport(Graph2DView view) {
277     // a customized DropSupport is used which only created new nodes if they are dropped onto a group node
278     DropSupport dropSupport = new DropSupport(view) {
279 
280       protected boolean dropNodeRealizer(Graph2DView view, NodeRealizer r, double worldCoordX, double worldCoordY) {
281         final HierarchyManager hm = HierarchyManager.getInstance(view.getGraph2D());
282         final HitInfo hitInfo = new HitInfo(view, worldCoordX, worldCoordY, true, HitInfo.NODE);
283         if (hm != null &&
284             hitInfo.hasHitNodes()) {
285           final Node node = (Node) hitInfo.hitNodes().current();
286           if (hm.isGroupNode(node) &&
287               ! (r instanceof TableGroupNodeRealizer)) {
288             // there is a group node at the drop location which will become the parent of the new node
289             return super.dropNodeRealizer(view, r, worldCoordX, worldCoordY);
290           }
291         } else if (r instanceof TableGroupNodeRealizer) {
292           return super.dropNodeRealizer(view, r, worldCoordX, worldCoordY);
293         }
294         return false;
295       }
296     };
297     dropSupport.setSnappingEnabled(true);
298     dropSupport.getSnapContext().setNodeToNodeDistance(30);
299     dropSupport.getSnapContext().setNodeToEdgeDistance(20);
300     dropSupport.getSnapContext().setUsingSegmentSnapLines(true);
301     dropSupport.setPreviewEnabled(true);
302     return dropSupport;
303   }
304 
305   private JList createDragNDropList(final DropSupport support) {
306     final Object[] listContent = new Object[] {
307             createConfiguredTableNodeRealizer(),
308             DropItemListCellRenderer.DROP_TYPE_ROW,
309             DropItemListCellRenderer.DROP_TYPE_COLUMN,
310             createConfiguredNormalNodeRealizer(),
311             createConfiguredGroupNodeRealizer()
312     };
313 
314     final Color lightBlueFillColor = new Color(126, 179, 240, 128);
315     final Color unselectedBorderColor = new Color(58, 82, 109);
316     final Stroke borderStroke = LineType.LINE_1;
317 
318     // configure how the icons for rows and column drag'n'dropable shall look like
319     final DropDrawable rowIcon = new DropDrawable(unselectedBorderColor, lightBlueFillColor, borderStroke);
320     rowIcon.insets = new YInsets(0, 15, 0, 0);
321     rowIcon.setBounds(0, 0, 80, 50);
322 
323     final DropDrawable columnIcon = new DropDrawable(unselectedBorderColor, lightBlueFillColor, borderStroke);
324     columnIcon.insets = new YInsets(15, 0, 0, 0);
325     columnIcon.setBounds(0, 0, 80, 50);
326 
327     final DropItemListCellRenderer cellRenderer =
328             new DropItemListCellRenderer(columnIcon, rowIcon);
329 
330     // configure the list itself
331     final JList dropItemList = new JList(listContent);
332     dropItemList.setCellRenderer(cellRenderer);
333     dropItemList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
334     dropItemList.setSelectedIndex(0);
335     dropItemList.setFixedCellHeight(100);
336     dropItemList.setFixedCellWidth(100);
337     dropItemList.setBorder(LineBorder.createBlackLineBorder());
338 
339     final DragSource dragSource = new DragSource();
340 
341     // configure the drop target listener used for rows and columns
342     final RowDropTargetListener rowListener =
343             new RowDropTargetListener(view) {
344               DropDrawable drawable = new DropDrawable(unselectedBorderColor, lightBlueFillColor, borderStroke);
345 
346               protected Drawable createDrawable(Rectangle2D bounds, YInsets insets) {
347                 drawable.insets = insets;
348                 drawable.setBounds(bounds);
349                 return drawable;
350               }
351             };
352     rowListener.setDefaultHeight(50);
353     rowListener.setDefaultMinimumHeight(30);
354     rowListener.setDrawableWidth(200);
355     rowListener.setDefaultInsets(rowInsets);
356     rowListener.setMaxLevel(2);
357 
358 
359     final ColumnDropTargetListener columnListener =
360             new ColumnDropTargetListener(view) {
361               DropDrawable drawable = new DropDrawable(unselectedBorderColor, lightBlueFillColor, borderStroke);
362 
363               protected Drawable createDrawable(Rectangle2D bounds, YInsets insets) {
364                 drawable.insets = insets;
365                 drawable.setBounds(bounds);
366                 return drawable;
367               }
368             };
369     columnListener.setDefaultWidth(100);
370     columnListener.setDefaultMinimumWidth(200);
371     columnListener.setDrawableHeight(180);
372     columnListener.setDefaultInsets(columnInsets);
373     columnListener.setMaxLevel(2);
374 
375 
376     // use the drop support class to initialize the drag and drop operation.
377     dragSource.createDefaultDragGestureRecognizer(dropItemList, DnDConstants.ACTION_MOVE,
378         new DragGestureListener() {
379           public void dragGestureRecognized(DragGestureEvent event) {
380             final Object value = dropItemList.getSelectedValue();
381             if (value.equals(DropItemListCellRenderer.DROP_TYPE_ROW)) {
382               support.startDrag(dragSource,
383                       rowListener,
384                       event,
385                       DragSource.DefaultMoveDrop);
386             } else if (value.equals(DropItemListCellRenderer.DROP_TYPE_COLUMN)) {
387               support.startDrag(dragSource,
388                       columnListener,
389                       event,
390                       DragSource.DefaultMoveDrop);
391             } else if (value instanceof NodeRealizer) {
392               NodeRealizer nr = (NodeRealizer) value;
393               support.startDrag(dragSource, nr, event, DragSource.DefaultMoveDrop);
394             }
395           }
396         });
397     return dropItemList;
398   }
399 
400   /**
401    * Creates the application help pane.
402    */
403   JComponent createHelpPane(URL helpURL) {
404     try {
405       JEditorPane editorPane = new JEditorPane(helpURL);
406       editorPane.setEditable(false);
407       editorPane.setPreferredSize(new Dimension(250, 250));
408       return new JScrollPane(editorPane);
409     } catch (IOException e) {
410       e.printStackTrace();
411     }
412     return null;
413   }
414 
415 
416   /**
417    * Creates a new edit mode that is configured to support user interaction with the {@link y.view.tabular.TableGroupNodeRealizer}.
418    *
419    * @return An edit mode for the user interaction in this demo.
420    */
421   protected EditMode createEditMode() {
422     final EditMode editMode = super.createEditMode();
423 
424     // the property setNodeSearchingEnabled has to be set to 'true' to allow custom MouseInputEditorProviders to be used
425     editMode.getMouseInputMode().setNodeSearchingEnabled(true);
426 
427     // nodes may only be created via drag'n'drop
428     editMode.allowNodeCreation(false);
429 
430     // activate snap lines
431     editMode.setSnappingEnabled(true);
432 
433     // ensure orthogonal edges during interactive edits
434     editMode.setOrthogonalEdgeRouting(true);
435 
436     // activate snapping and ensure orthogonal edges during edge creation
437     final CreateEdgeMode cem = new CreateEdgeMode();
438     cem.setFuzzyTargetPortDetermination(true);
439     cem.setSnapToOrthogonalSegmentsDistance(5);
440     cem.setUsingNodeCenterSnapping(true);
441     cem.setSnappingOrthogonalSegments(true);
442     cem.setIndicatingTargetNode(true);
443     cem.setRemovingInnerBends(true);
444     cem.setOrthogonalEdgeCreation(true);
445     editMode.setCreateEdgeMode(cem);
446 
447     return editMode;
448   }
449 
450   /**
451    * Overwritten to add undo/redo and  a layout action to the demo's
452    * tool bar.
453    */
454   protected JToolBar createToolBar() {
455     final JToolBar toolBar = super.createToolBar();
456     toolBar.addSeparator();
457     final Action undoAction = undoManager.getUndoAction();
458     final URL undoIconUrl = DemoBase.class.getResource("resource/undo.png");
459     if (undoIconUrl != null) {
460       undoAction.putValue(Action.SMALL_ICON, new ImageIcon(undoIconUrl));
461       undoAction.putValue(Action.SHORT_DESCRIPTION, "Undo");
462     }
463     toolBar.add(createActionControl(undoAction, false));
464     final Action redoAction = undoManager.getRedoAction();
465     final URL redoIconUrl = DemoBase.class.getResource("resource/redo.png");
466     if (redoIconUrl != null) {
467       redoAction.putValue(Action.SMALL_ICON, new ImageIcon(redoIconUrl));
468       redoAction.putValue(Action.SHORT_DESCRIPTION, "Redo");
469     }
470     toolBar.add(createActionControl(redoAction, false));
471 
472     toolBar.addSeparator();
473 
474     final AbstractAction layoutAction = new AbstractAction("Layout") {
475       public void actionPerformed( ActionEvent e ) {
476         layout(view.getGraph2D());
477         view.updateView();
478       }
479     };
480     final URL resource = DemoBase.class.getResource("resource/layout.png");
481     if (resource != null) {
482       layoutAction.putValue(Action.SMALL_ICON, new ImageIcon(resource));
483       layoutAction.putValue(Action.SHORT_DESCRIPTION, "Layout");
484     }
485 
486     toolBar.add(createActionControl(layoutAction, true));
487 
488     return toolBar;
489   }
490 
491   /**
492    * Creates a control for triggering the specified action from the
493    * demo toolbars.
494    * @param action   the <code>Action</code> that is triggered by the
495    * created control.
496    * @param showActionText   <code>true</code> if the control should display
497    * the action name; <code>false</code> otherwise.
498    * @return a control for triggering the specified action from the
499    * demo toolbars.
500    */
501   private JComponent createActionControl(
502           final Action action,
503           final boolean showActionText
504   ) {
505     final JButton jb = new JButton();
506     if (action.getValue(Action.SMALL_ICON) != null) {
507       jb.putClientProperty("hideActionText", showActionText ? Boolean.FALSE : Boolean.TRUE);
508     }
509     jb.setAction(action);
510     return jb;
511   }
512 
513   /**
514    * Overwritten to create an action that loads/opens a graph and clears
515    * the undo queue right afterwards.
516    * @return an action that loads/opens a graph and clears
517    * the undo queue right afterwards.
518    */
519   protected Action createLoadAction() {
520     final Action action = super.createLoadAction();
521     return new AbstractAction((String) action.getValue(Action.NAME)) {
522       public void actionPerformed( final ActionEvent e ) {
523         action.actionPerformed(e);
524         undoManager.resetQueue();
525       }
526     };
527   }
528 
529   /**
530    * Runs an incremental hierachic layout that respects the assignments of nodes to swimlanes and milestones.
531    */
532   private void layout( final Graph2D graph ) {
533     graph.firePreEvent();
534     try {
535       // undoability
536       graph.backupRealizers();
537 
538       final IncrementalHierarchicLayouter ihl = new IncrementalHierarchicLayouter();
539       ihl.setLayoutOrientation(LayoutOrientation.LEFT_TO_RIGHT);
540       ihl.setOrthogonallyRouted(true);
541       ihl.setRecursiveGroupLayeringEnabled(false);
542       ((SimplexNodePlacer) ihl.getNodePlacer()).setBaryCenterModeEnabled(true);
543 
544       final Graph2DLayoutExecutor layoutExecutor = new Graph2DLayoutExecutor(Graph2DLayoutExecutor.BUFFERED);
545       layoutExecutor.setConfiguringTableNodeRealizers(true);
546       layoutExecutor.getTableLayoutConfigurator().setCompactionEnabled(false);
547       layoutExecutor.getTableLayoutConfigurator().setHorizontalLayoutConfiguration(true);
548       layoutExecutor.doLayout(graph, ihl);
549       view.fitContent();
550       view.updateView();
551     } finally {
552       graph.firePostEvent();
553     }
554   }
555 
556   protected Action createDeleteSelectionAction() {
557     final Graph2DViewActions.DeleteSelectionAction action =
558             new Graph2DViewActions.DeleteSelectionAction(view);
559     action.setDeletionMask(Graph2DViewActions.DeleteSelectionAction.ALL_TYPES_MASK);
560     action.setKeepingTableNodesOnTableContentDeletion(true);
561     final URL deleteIconUrl = DemoBase.class.getResource("resource/delete.png");
562     if (deleteIconUrl != null) {
563       action.putValue(Action.SMALL_ICON, new ImageIcon(deleteIconUrl));
564     }
565     action.putValue(Action.SHORT_DESCRIPTION, "Delete Selection");
566     return action;
567   }
568 
569   private static Map createGroupNodeConfiguration() {
570     final Map map = GenericGroupNodeRealizer.createDefaultConfigurationMap();
571     final ShapeNodePainter painter = new ShapeNodePainter(ShapeNodePainter.ROUND_RECT);
572     map.put(GenericNodeRealizer.ContainsTest.class, painter);
573     map.put(GenericNodeRealizer.Painter.class, painter);
574     map.put(GenericNodeRealizer.GenericMouseInputEditorProvider.class, null);
575     return map;
576   }
577 
578   private static Map createTableNodeConfiguration() {
579     final Map map = TableGroupNodeRealizer.createDefaultConfigurationMap();
580 
581     // configure the painter used for the swim lanes
582     final AlternatingPainter rowPainter = new AlternatingPainter();
583     final TableNodePainter tableNodePainter = TableNodePainter.newDefaultInstance();
584     tableNodePainter.setSubPainter(TableNodePainter.PAINTER_ROW_BACKGROUND, rowPainter);
585     map.put(GenericNodeRealizer.Painter.class, tableNodePainter);
586 
587     // configure MouseInputEditor for the TableGroupNodeRealizer
588     final MultiplexingNodeEditor editor = new MultiplexingNodeEditor();
589     final TableLabelEditor editLabelEditor = new TableLabelEditor();
590     editor.addNodeEditor(editLabelEditor);
591     final TableSelectionEditor tableSelectionEditor = new TableSelectionEditor();
592     tableSelectionEditor.setSelectionPolicy(TableSelectionEditor.RELATE_TO_NODE_SELECTION);
593     editor.addNodeEditor(tableSelectionEditor);
594     final TableSizeEditor resizeEditor = new TableSizeEditor();
595     editor.addNodeEditor(resizeEditor);
596     final TableOrderEditor tableOrderEditor = new TableOrderEditor();
597     tableOrderEditor.setMaxColumnLevel(2);
598     tableOrderEditor.setMaxRowLevel(2);
599     editor.addNodeEditor(tableOrderEditor);
600     map.put(GenericNodeRealizer.GenericMouseInputEditorProvider.class, editor);
601 
602     return map;
603   }
604 
605   /**
606    * Ensures that the minimum width of columns and the minimum height of rows
607    * is never smaller than the width or height of their associated labels.
608    * <p>
609    * The implementation relies on the fact that there is at most one label
610    * associated to any column or row.
611    * </p>
612    */
613   private static final class MinimumSizeManager implements Graph2DListener {
614     private boolean enabled;
615 
616     MinimumSizeManager( final Graph2D graph ) {
617       graph.addGraph2DListener(this);
618       enabled = true;
619     }
620 
621     public boolean isEnabled() {
622       return enabled;
623     }
624 
625     public void setEnabled( final boolean enabled ) {
626       this.enabled = enabled;
627     }
628 
629     public void onGraph2DEvent( final Graph2DEvent e ) {
630       if (isEnabled()) {
631         if ("text".equals(e.getPropertyName())) {
632           final Object subject = e.getSubject();
633           if (subject instanceof NodeLabel) {
634             final NodeLabel label = (NodeLabel) subject;
635             final NodeLabelModel model = label.getLabelModel();
636             if (model instanceof ColumnNodeLabelModel) {
637               handleColumnLabelEvent(label);
638             } else if (model instanceof RowNodeLabelModel) {
639               handleRowLabelEvent(label);
640             }
641           }
642         }
643       }
644     }
645 
646     private void handleRowLabelEvent( final NodeLabel label ) {
647       final Row row = RowNodeLabelModel.getRow(label);
648       if (row != null) {
649         final double h = label.getHeight() + 8;
650         if (h > row.getHeight()) {
651           (new TableSupport()).setHeight(row, h, false);
652         }
653         row.setMinimumHeight(Math.max(
654                 h, ((TableGroupNodeRealizer) label.getRealizer())
655                         .getDefaultMinimumRowHeight()));
656       }
657     }
658 
659     private void handleColumnLabelEvent( final NodeLabel label ) {
660       final Column column = ColumnNodeLabelModel.getColumn(label);
661       if (column != null) {
662         final double w = label.getWidth() + 8;
663         if (w > column.getWidth()) {
664           (new TableSupport()).setWidth(column, w, false);
665         }
666         column.setMinimumWidth(Math.max(
667                 w, ((TableGroupNodeRealizer) label.getRealizer())
668                         .getDefaultMinimumColumnWidth()));
669       }
670     }
671   }
672 }
673 
674 //////////////////////////////////////////////////////////////////////////////////////////////////////
675 /////////////////////////////   Class AlternatingPainter    //////////////////////////////////////////
676 //////////////////////////////////////////////////////////////////////////////////////////////////////
677 
678 /**
679  * A background {@link y.view.GenericNodeRealizer.Painter Painter} for rows in
680  * a table that paints all inner or parent rows (i.e. rows for which
681  * <code>getRows()</code> returns a non-empty list) in a given color and
682  * alternates between two different colors for leaf rows (i.e. rows for which
683  * <code>getRows()</code> returns an empty list).
684  */
685 final class AlternatingPainter extends ShapeNodePainter {
686   private static final Color PARENT_ROW_COLOR = new Color(113, 146, 178);
687   private static final Color EVEN_ROW_COLOR = new Color(196, 215, 237);
688   private static final Color ODD_ROW_COLOR = new Color(171, 200, 226);
689   private static final Color SELECTED_ROW_COLOR = new Color(55, 93, 129);
690 
691   AlternatingPainter() {
692     super(ShapeNodePainter.RECT);
693   }
694 
695   protected Paint getFillPaint( final NodeRealizer context, final boolean selected ) {
696     return getFillColor(context, selected);
697   }
698 
699   /**
700    * Determines the fill color for the row corresponding to the specified
701    * realizer.
702    * @param context a dummy realizer representing a row in a table.
703    * @param selected ignored.
704    * @return the fill color for the row corresponding to the specified
705    * realizer.
706    */
707   protected Color getFillColor( final NodeRealizer context, final boolean selected ) {
708     final Row row = TableNodePainter.getRow(context);
709     if (row.isSelected()) {
710       return SELECTED_ROW_COLOR;
711     }
712 
713     if (row.getRows().isEmpty()) {
714       if (indexOf(row, TableNodePainter.getTable(context).getRows(), new int[]{-1}) % 2 == 0) {
715         return EVEN_ROW_COLOR;
716       } else {
717         return ODD_ROW_COLOR;
718       }
719     } else {
720       return PARENT_ROW_COLOR;
721     }
722   }
723 
724   /**
725    * Calls the various utility method and callbacks in this class.
726    */
727   public void paint(NodeRealizer context, Graphics2D graphics) {
728     if (!context.isVisible()){
729       return;
730     }
731     backupGraphics(graphics);
732     try {
733       paintNode(context, graphics, false);
734     } finally {
735       restoreGraphics(graphics);
736     }
737   }
738 
739   /**
740    * Determines the leaf index of the specified row.
741    * @param row    the <code>Row</code> to search for.
742    * @param i      used to track the number of previously visited leaf rows.
743    *  (<code>int[]</code> is used as poor man's mutable <code>Integer</code>.)
744    * @return the leaf index of the specified row.
745    */
746   private int indexOf( final Row row, final Collection rows, final int[] i ) {
747     for (Iterator it = rows.iterator(); it.hasNext();) {
748       final Row r = (Row) it.next();
749       final List children = r.getRows();
750       if (children.isEmpty()) {
751         ++i[0];
752         if (r.equals(row)) {
753           return i[0];
754         }
755       } else {
756         final int idx = indexOf(row, children, i);
757         if (idx > -1) {
758           return idx;
759         }
760       }
761     }
762 
763     return -1;
764   }
765 }
766 
767 //////////////////////////////////////////////////////////////////////////////////////////////////////
768 /////////////////////////////   Class DropDrawable    ////////////////////////////////////////////////
769 //////////////////////////////////////////////////////////////////////////////////////////////////////
770 
771 /**
772  * This class is used to render a representive of a row or a column either as {@link y.view.Drawable} during drag'n'drop
773  * gestures or as {@link javax.swing.Icon} in the drag'n'drop list.
774  */
775 class DropDrawable implements Drawable, Icon {
776   Rectangle bounds;
777   YInsets insets;
778 
779   Color borderColor;
780   Color fillColor;
781   Stroke stroke;
782 
783   /**
784    * Creates a new instance using the specified colors and stroke.
785    *
786    * @param borderColor The color used for the stripe border.
787    * @param fillColor The fill color of the stripe.
788    * @param stroke The stroke used for the border.
789    */
790   DropDrawable(Color borderColor, Color fillColor, Stroke stroke) {
791     this.borderColor = borderColor;
792     this.fillColor = fillColor;
793     this.stroke = stroke;
794   }
795 
796   /**
797    * Called from classes using the {@link y.view.Drawable} interface.
798    * It delegates to {@link #paintIcon(java.awt.Component, java.awt.Graphics, int, int)}.
799    *
800    * @param g The graphics object to render on.
801    */
802   public void paint(Graphics2D g) {
803     g.setStroke(stroke);
804     paintIcon(null, g, bounds.x, bounds.y);
805   }
806 
807   /**
808    * Called from classes using the {@link javax.swing.Icon} interface and from {@link #paint(java.awt.Graphics2D)}.
809    *
810    * @param c The component the icon shall be rendered in.
811    * @param g The graphics object to render on.
812    * @param x The horizontal coordinate of the icon.
813    * @param y The vertical coordinate of the icon.
814    */
815   public void paintIcon(Component c, Graphics g, int x, int y) {
816     // update the bounds if necessary
817     if (bounds == null ||
818         bounds.getX() != x ||
819         bounds.getY() != y) {
820       int newWidth = (bounds == null) ? 0 : bounds.width;
821       int newHeight = (bounds == null) ? 0 : bounds.height;
822       bounds = new Rectangle(x, y, newWidth, newHeight);
823     }
824 
825     // if the stripe shall be painted as an icon, it shall be horizontally centered in it's containing component.
826     int cWidth = 0;
827     if (c != null) {
828       cWidth = c.getWidth() - 2;
829     }
830     int offX = (cWidth > getIconWidth()) ? (cWidth - getIconWidth())/2 : 0;
831 
832     g.setColor(fillColor);
833     g.fillRect(bounds.x + offX, bounds.y, bounds.width, bounds.height);
834 
835     g.setColor(borderColor);
836     g.drawRect(bounds.x + offX, bounds.y, bounds.width, bounds.height);
837 
838     if (insets != null &&
839         (insets.top + insets.bottom < bounds.height &&
840          insets.left + insets.right < bounds.width)) {
841       g.setColor(fillColor);
842       g.fillRect((int) (bounds.x + offX + insets.left),
843               (int) (bounds.y + insets.top),
844               (int) (bounds.width - insets.left - insets.right),
845               (int) (bounds.height - insets.top - insets.bottom));
846       g.setColor(borderColor);
847       g.drawRect((int) (bounds.x + offX + insets.left),
848               (int) (bounds.y + insets.top),
849               (int) (bounds.width - insets.left - insets.right),
850               (int) (bounds.height - insets.top - insets.bottom));
851     }
852   }
853 
854   public Rectangle getBounds() {
855     return bounds;
856   }
857 
858   /**
859    * Sets the specified <code>bounds</code>.
860    * @param bounds The new bounds of the drawable.
861    */
862   public void setBounds(Rectangle2D bounds) {
863     this.bounds = new Rectangle((int) bounds.getX(), (int) bounds.getY(),
864                         (int) Math.ceil(bounds.getWidth()), (int) Math.ceil(bounds.getHeight()));
865   }
866 
867   /**
868    * Sets the bounds to the specified values.
869    * @param x The horizontal coordinate.
870    * @param y The vertical coordinate.
871    * @param width The width of the drawable.
872    * @param height The height of the drawable.
873    */
874   public void setBounds(int x, int y, int width, int height) {
875     this.bounds = new Rectangle(x, y, width, height);
876   }
877 
878   public int getIconWidth() {
879     return bounds != null ? bounds.width : 0;
880   }
881 
882   public int getIconHeight() {
883     return bounds != null ? bounds.height : 0;
884   }
885 }
886 
887 /**
888  * Cell renderer for the drop item list that is used as DnD source to create
889  * new nodes, columns, and rows.
890  */
891 class DropItemListCellRenderer implements ListCellRenderer {
892   /**
893    * Value type constant representing a {@link y.view.tabular.TableGroupNodeRealizer.Row}.
894    */
895   static final Object DROP_TYPE_ROW = "DROP_TYPE_ROW";
896   /**
897    * Value type constant representing a {@link y.view.tabular.TableGroupNodeRealizer.Column}.
898    */
899   static final Object DROP_TYPE_COLUMN = "DROP_TYPE_COLUMN";
900 
901 
902   private static final Dimension PREFERRED_SIZE = new Dimension(100, 100);
903 
904   private final DefaultListCellRenderer dlcr;
905   private final RealizerCellRenderer realizerRenderer;
906 
907   private final Icon rowIcon;
908   private final Icon columnIcon;
909 
910   /**
911    * Creates a new <code>DropItemListCellRenderer</code>.
912    *
913    * @param columnIcon   the icon to display {@link #DROP_TYPE_COLUMN} values.
914    * @param rowIcon      the icon to display {@link #DROP_TYPE_COLUMN} values.
915    */
916   DropItemListCellRenderer(
917           final Icon columnIcon,
918           final Icon rowIcon
919   ) {
920     this.columnIcon = columnIcon;
921     this.rowIcon = rowIcon;
922 
923     realizerRenderer = new RealizerCellRenderer(
924             PREFERRED_SIZE.width, PREFERRED_SIZE.height);
925     dlcr = new DefaultListCellRenderer();
926   }
927 
928   public Component getListCellRendererComponent(
929           JList list,
930           Object value,
931           int index,
932           boolean isSelected,
933           boolean cellHasFocus
934   ) {
935     if (value instanceof NodeRealizer) {
936       final Component c = realizerRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
937       if (c instanceof JComponent) {
938         if (value instanceof GenericNodeRealizer) {
939           final String configuration = ((GenericNodeRealizer) value).getConfiguration();
940           if (SwimlaneDemo.CONFIGURATION_GROUP_NODE.equals(configuration)) {
941             ((JComponent) c).setToolTipText("Create new group node");
942           }
943           if (SwimlaneDemo.CONFIGURATION_TABLE_NODE.equals(configuration)) {
944             ((JComponent) c).setToolTipText("Create new table node");
945           }
946           if (DemoDefaults.NODE_CONFIGURATION.equals(configuration)) {
947             ((JComponent) c).setToolTipText("Create new child node");
948           }
949         } else {
950           ((JComponent) c).setToolTipText("Create new node");
951         }
952       }
953       return c;
954     } else {
955       dlcr.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
956       dlcr.setText("");
957       dlcr.setPreferredSize(PREFERRED_SIZE);
958       if (DROP_TYPE_COLUMN.equals(value)) {
959         dlcr.setIcon(columnIcon);
960         dlcr.setToolTipText("Create new column");
961       } else if (DROP_TYPE_ROW.equals(value)) {
962         dlcr.setIcon(rowIcon);
963         dlcr.setToolTipText("Create new row");
964       } else {
965         dlcr.setIcon(null);
966         dlcr.setToolTipText(null);
967       }
968       return dlcr;
969     }
970   }
971 }
972