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.application;
15  
16  import demo.view.DemoBase;
17  import demo.view.DemoDefaults;
18  
19  import y.base.Node;
20  import y.base.NodeCursor;
21  import y.base.NodeList;
22  import y.base.GraphListener;
23  import y.base.GraphEvent;
24  import y.view.Drawable;
25  import y.view.Graph2D;
26  import y.view.Graph2DView;
27  import y.view.NavigationMode;
28  import y.view.NodeRealizer;
29  import java.awt.Color;
30  import java.awt.EventQueue;
31  import java.awt.Graphics2D;
32  import java.awt.Rectangle;
33  import java.awt.RenderingHints;
34  import java.awt.event.ActionEvent;
35  import java.awt.event.KeyEvent;
36  import java.awt.geom.Point2D;
37  import java.awt.geom.RoundRectangle2D;
38  import java.awt.geom.Rectangle2D;
39  import java.util.Collection;
40  import java.util.Comparator;
41  import java.util.HashMap;
42  import java.util.HashSet;
43  import java.util.Locale;
44  import javax.swing.AbstractAction;
45  import javax.swing.Action;
46  import javax.swing.ActionMap;
47  import javax.swing.InputMap;
48  import javax.swing.JComponent;
49  import javax.swing.JLabel;
50  import javax.swing.JRootPane;
51  import javax.swing.JTextField;
52  import javax.swing.JToolBar;
53  import javax.swing.KeyStroke;
54  import javax.swing.event.DocumentEvent;
55  import javax.swing.event.DocumentListener;
56  
57  /**
58   * Demonstrates how to find nodes in a graph that match a specific criterion
59   * and how to visually present all matching nodes in simple way.
60   *
61   */
62  public class SearchDemo extends DemoBase {
63  
64    private LabelTextSearchSupport support;
65  
66    public SearchDemo() {
67      this(null);
68    }
69  
70    public SearchDemo( final String helpFilePath ) {
71      // load a sample graph
72      loadGraph("resource/SearchDemo.graphml");
73  
74      // add rendering hint to enforce proportional text scaling
75      view.getRenderingHints().put(
76              RenderingHints.KEY_FRACTIONALMETRICS,
77              RenderingHints.VALUE_FRACTIONALMETRICS_ON);
78  
79      // register keyboard action for "select next match" and "clear search"
80      final LabelTextSearchSupport support = getSearchSupport();
81      final ActionMap amap = support.createActionMap();
82      final InputMap imap = support.createDefaultInputMap();
83      contentPane.setActionMap(amap);
84      contentPane.setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, imap);
85  
86      // display the demo help if possible
87      addHelpPane(helpFilePath);
88    }
89  
90    protected Action createSaveAction() {
91      //Overridden method to disable the Save menu in the demo, because it is not an editable demo
92      return null;
93    }
94  
95    protected void registerViewModes() {
96      view.addViewMode(new NavigationMode());
97    }
98  
99    private LabelTextSearchSupport getSearchSupport() {
100     if (support == null) {
101       support = new LabelTextSearchSupport(view);
102     }
103     return support;
104   }
105 
106   /**
107    * Overwritten to display a search text field as well as controls for
108    * "select next match", "select previous match", and "select all matches".
109    */
110   protected JToolBar createToolBar() {
111     final LabelTextSearchSupport support = getSearchSupport();
112 
113     final JToolBar bar = super.createToolBar();
114     bar.addSeparator();
115     bar.add(new JLabel("Find:"));
116     bar.addSeparator(TOOLBAR_SMALL_SEPARATOR);
117     bar.add(support.getSearchField());
118     bar.addSeparator(TOOLBAR_SMALL_SEPARATOR);
119     bar.add(createActionControl(support.getPreviousAction()));
120     bar.add(createActionControl(support.getNextAction()));
121     bar.add(createActionControl(support.getSelectAllAction()));
122     return bar;
123   }
124 
125   /**
126    * Overwritten to request focus for the search text field initially.
127    */
128   public void addContentTo( final JRootPane rootPane ) {
129     super.addContentTo(rootPane);
130     EventQueue.invokeLater(new Runnable() {
131       public void run() {
132         getSearchSupport().getSearchField().requestFocus();
133       }
134     });
135   }
136 
137   public static void main( String[] args ) {
138     EventQueue.invokeLater(new Runnable() {
139       public void run() {
140         Locale.setDefault(Locale.ENGLISH);
141         initLnF();
142         (new SearchDemo("resource/searchhelp.html")).start();
143       }
144     });
145   }
146 
147 
148   /**
149    * Utility class that provides methods for searching for nodes that match
150    * a given search criterion and for displaying search results.
151    */
152   public static class SearchSupport {
153     private static final Object NEXT_ACTION_ID = "SearchSupport.Next";
154     private static final Object CLEAR_ACTION_ID = "SearchSupport.Clear";
155 
156 
157     private Action previous;
158     private Action next;
159     private Action selectAll;
160     private Action clear;
161 
162     private SearchResult searchResult;
163 
164     private final Graph2DView view;
165 
166     public SearchSupport( final Graph2DView view ) {
167       this.view = view;
168       this.view.addBackgroundDrawable(new Marker());
169       final Graph2D graph = this.view.getGraph2D();
170 
171       // register a listener that updates search results whenever a node
172       // is deleted to prevent stale data in the results
173       graph.addGraphListener(new GraphListener() {
174         public void onGraphEvent( final GraphEvent e ) {
175           if (searchResult != null) {
176             if (GraphEvent.POST_NODE_REMOVAL == e.getType() ||
177                 GraphEvent.SUBGRAPH_REMOVAL == e.getType()) {
178               final SearchResult oldResult = searchResult;
179               searchResult = new SearchResult();
180               for (NodeCursor nc = oldResult.nodes(); nc.ok(); nc.next()) {
181                 final Node node = nc.node();
182                 if (node.getGraph() == graph) {
183                   searchResult.add(node);
184                 }
185               }
186             }
187           }
188         }
189       });
190     }
191 
192     /**
193      * Returns the <code>Graph2DView</code> that is associated to the search
194      * support.
195      * @return the <code>Graph2DView</code> that is associated to the search
196      * support.
197      */
198     public Graph2DView getView() {
199       return view;
200     }
201 
202     /**
203      * Returns the current search result or <code>null</code> if there is none.
204      * @return the current search result or <code>null</code> if there is none.
205      */
206     public SearchResult getSearchResult() {
207       return searchResult;
208     }
209 
210     /**
211      * Updates the current search result and the enabled states of the support's
212      * clear, next, previous, and select all actions.
213      * @param query   specifies which nodes to include in the search result.
214      * If the specified query is <code>null</code> the current search result
215      * is reset to <code>null</code>, too.
216      * @param incremental   <code>true</code> if the current search result
217      * should be refined using the specified criterion; <code>false</code>
218      * if all nodes of the support's associated graph view's graph should be
219      * checked.
220      * @see #getClearAction()
221      * @see #getNextAction()
222      * @see #getPreviousAction()
223      * @see #getSelectAllAction()
224      */
225     public void search( final SearchCriterion query, final boolean incremental ) {
226       boolean resultChanged = false;
227       if (query != null) {
228         final Graph2D graph = view.getGraph2D();
229         final NodeCursor nc =
230                 searchResult != null && incremental
231                 ? searchResult.nodes()
232                 : graph.nodes();
233         final HashSet oldResult =
234                 searchResult == null
235                 ? new HashSet()
236                 : new HashSet(searchResult.asCollection());
237         final HashMap node2location = new HashMap();
238         searchResult = new SearchResult();
239         for (; nc.ok(); nc.next()) {
240           final Node node = nc.node();
241           if (query.accept(graph, node)) {
242             searchResult.add(node);
243             final NodeRealizer nr = graph.getRealizer(node);
244             node2location.put(node, new Point2D.Double(nr.getX(), nr.getY()));
245             if (!oldResult.contains(node)) {
246               resultChanged = true;
247             }
248           }
249         }
250         searchResult.sort(new Comparator() {
251           public int compare( final Object o1, final Object o2 ) {
252             final Point2D p1 = (Point2D) node2location.get(o1);
253             final Point2D p2 = (Point2D) node2location.get(o2);
254             if (p1.getY() < p2.getY()) {
255               return -1;
256             } else if (p1.getY() > p2.getY()) {
257               return 1;
258             } else {
259               if (p1.getX() < p2.getX()) {
260                 return -1;
261               } else if (p1.getX() > p2.getX()) {
262                 return 1;
263               } else {
264                 return 0;
265               }
266             }
267           }
268         });
269         resultChanged |= oldResult.size() != searchResult.asCollection().size();
270       } else if (searchResult != null) {
271         searchResult = null;
272         resultChanged = true;
273       }
274 
275       if (resultChanged) {
276         final boolean state =
277                 searchResult != null &&
278                 !searchResult.asCollection().isEmpty();
279         if (clear != null) {
280           clear.setEnabled(state);
281         }
282         if (previous != null) {
283           previous.setEnabled(state);
284         }
285         if (next != null) {
286           next.setEnabled(state);
287         }
288         if (selectAll != null) {
289           selectAll.setEnabled(state);
290         }
291       }
292     }
293 
294     /**
295      * Ensures that the specified rectangle is visible in the support's
296      * associated graph view.
297      * @param bnds   a rectangle in world (i.e. graph) coordinates.
298      */
299     private void focusView( final Rectangle2D bnds ) {
300       if (bnds.getWidth() > 0 && bnds.getHeight() > 0) {
301         final double minX = bnds.getX() - MARKER_MARGIN;
302         final double w = bnds.getWidth() + 2*MARKER_MARGIN;
303         final double maxX = minX + w;
304         final double minY = bnds.getY() - MARKER_MARGIN;
305         final double h = bnds.getHeight() + 2*MARKER_MARGIN;
306         final double maxY = minY + h;
307 
308         final int canvasWidth = view.getCanvasComponent().getWidth();
309         final int canvasHeight = view.getCanvasComponent().getHeight();
310         final Point2D oldCenter = view.getCenter();
311         final double oldZoom = view.getZoom();
312         double newZoom = oldZoom;
313         double newCenterX = oldCenter.getX();
314         double newCenterY = oldCenter.getY();
315         final Rectangle vr = view.getVisibleRect();
316 
317         // determine whether the specified rectangle (plus the marker margin)
318         // lies in the currently visible region
319         // if not, adjust zoom factor and view port accordingly
320         boolean widthFits = true;
321         boolean heightFits = true;
322         if (vr.getWidth() < w) {
323           newZoom = Math.min(newZoom, canvasWidth / w);
324           widthFits = false;
325         }
326         if (vr.getHeight() < h) {
327           newZoom = Math.min(newZoom, canvasHeight / h);
328           heightFits = false;
329         }
330         if (widthFits) {
331           if (vr.getX() > minX) {
332             newCenterX -= vr.getX() - minX;
333           } else if (vr.getMaxX() < maxX) {
334             newCenterX += maxX - vr.getMaxX();
335           }
336         } else {
337                                            // take scroll bars into account
338           newCenterX = bnds.getCenterX() + (view.getWidth() - canvasWidth) * 0.5 / newZoom;
339         }
340         if (heightFits) {
341           if (vr.getY() > minY) {
342             newCenterY -= vr.getY() - minY;
343           } else if (vr.getMaxY() < maxY) {
344             newCenterY += maxY - vr.getMaxY();
345           }
346         } else {
347                                            // take scroll bars into account
348           newCenterY = bnds.getCenterY() + (view.getHeight() - canvasHeight) * 0.5 / newZoom;
349         }
350 
351         if (oldZoom != newZoom ||
352             oldCenter.getX() != newCenterX ||
353             oldCenter.getY() != newCenterY) {
354           // animate the view port change
355           view.focusView(newZoom, new Point2D.Double(newCenterX, newCenterY), true);
356         } else {
357           view.updateView();
358         }
359       }
360     }
361 
362     /**
363      * Ensures that only the specified node is selected and that the specified
364      * node is visible in the support's associated graph view.
365      * @param node   the node to select and display.
366      */
367     private void emphasizeNode( final Node node ) {
368       final Graph2D graph = view.getGraph2D();
369       graph.unselectAll();
370       if (node != null) {
371         final NodeRealizer nr = graph.getRealizer(node);
372         nr.setSelected(true);
373         final Rectangle2D.Double bnds = new Rectangle2D.Double(0, 0, -1, -1);
374         nr.calcUnionRect(bnds);
375         focusView(bnds);
376       } else {
377         view.updateView();
378       }
379     }
380 
381     /**
382      * Returns the support's associated <em>clear search result</em> action.
383      * @return the support's associated <em>clear search result</em> action.
384      * @see #createClearAction()
385      */
386     public Action getClearAction() {
387       if (clear == null) {
388         clear = createClearAction();
389       }
390       return clear;
391     }
392 
393     /**
394      * Creates the support's associated <em>clear search result</em> action.
395      * The default implementation resets the support's search result to
396      * <code>null</code>.
397      * @return the support's associated <em>clear search result</em> action.
398      */
399     protected Action createClearAction() {
400       return new AbstractAction("Clear") {
401         {
402           setEnabled(searchResult != null);
403         }
404 
405         public void actionPerformed( final ActionEvent e ) {
406           if (searchResult != null) {
407             search(null, false);
408             view.updateView();
409           }
410         }
411       };
412     }
413 
414     /**
415      * Returns the support's associated <em>find previous match</em> action.
416      * @return the support's associated <em>find previous match</em> action.
417      * @see #createPreviousAction()
418      */
419     public Action getPreviousAction() {
420       if (previous == null) {
421         previous = createPreviousAction();
422       }
423       return previous;
424     }
425 
426     /**
427      * Creates the support's associated <em>find previous match</em> action.
428      * @return the support's associated <em>find previous match</em> action.
429      */
430     protected Action createPreviousAction() {
431       return new AbstractAction("Previous", getIconResource("resource/search_previous.png")) {
432           {
433             setEnabled(searchResult != null);
434           }
435 
436           public void actionPerformed( final ActionEvent e ) {
437             if (searchResult != null) {
438               searchResult.emphasizePrevious();
439               emphasizeNode(searchResult.emphasizedNode());
440             }
441           }
442         };
443     }
444 
445     /**
446      * Returns the support's associated <em>find next match</em> action.
447      * @return the support's associated <em>find next match</em> action.
448      * @see #createNextAction()
449      */
450     public Action getNextAction() {
451       if (next == null) {
452         next = createNextAction();
453       }
454       return next;
455     }
456 
457     /**
458      * Creates the support's associated <em>find next match</em> action.
459      * @return the support's associated <em>find next match</em> action.
460      */
461     protected Action createNextAction() {
462       return new AbstractAction("Next", getIconResource("resource/search_next.png")) {
463           {
464             setEnabled(searchResult != null);
465           }
466 
467           public void actionPerformed( final ActionEvent e ) {
468             if (searchResult != null) {
469               searchResult.emphasizeNext();
470               emphasizeNode(searchResult.emphasizedNode());
471             }
472           }
473         };
474     }
475 
476     /**
477      * Returns the support's associated <em>select all matches</em> action.
478      * @return the support's associated <em>select all matches</em> action.
479      * @see #createSelectAllAction()
480      */
481     public Action getSelectAllAction() {
482       if (selectAll == null) {
483         selectAll = createSelectAllAction();
484       }
485       return selectAll;
486     }
487 
488     /**
489      * Creates the support's associated <em>select all matches</em> action.
490      * @return the support's associated <em>select all matches</em> action.
491      */
492     protected Action createSelectAllAction() {
493       return new AbstractAction("Select All", getIconResource("resource/search_select_all.png")) {
494           {
495             setEnabled(searchResult != null);
496           }
497 
498           public void actionPerformed( final ActionEvent e ) {
499             if (searchResult != null) {
500               final Graph2D graph = view.getGraph2D();
501               graph.unselectAll();
502               // clear the result set's emphasis pointer
503               searchResult.resetEmphasis();
504               // select all matching nodes and en passent calculate the result
505               // set's bounding box
506               final Rectangle2D.Double bnds = new Rectangle2D.Double(0, 0, -1, -1);
507               for (NodeCursor nc = searchResult.nodes(); nc.ok(); nc.next()) {
508                 final NodeRealizer nr = graph.getRealizer(nc.node());
509                 nr.setSelected(true);
510                 nr.calcUnionRect(bnds);
511               }
512 
513               if (bnds.getWidth() > 0 && bnds.getHeight() > 0) {
514                 // ensure that all selected nodes are visible
515                 focusView(bnds);
516               } else {
517                 view.updateView();
518               }
519             }
520           }
521         };
522     }
523 
524     /**
525      * Creates a preconfigured action map for the support's
526      * <em>find next match</em> and <em>clear result</code> actions.
527      * @return a preconfigured action map for the support's
528      * <em>find next match</em> and <em>clear result</code> actions.
529      * @see #getClearAction()
530      * @see #getNextAction()
531      */
532     public ActionMap createActionMap() {
533       final ActionMap amap = new ActionMap();
534       amap.put(NEXT_ACTION_ID, getNextAction());
535       amap.put(CLEAR_ACTION_ID, getClearAction());
536       return amap;
537     }
538 
539     /**
540      * Creates a preconfigured input map for the support's
541      * <em>find next match</em> and <em>clear result</code> actions.
542      * The default implementation maps the <em>find next match</em> action
543      * to the <code>F3</code> function key and the <em>clear search result</em>
544      * action to the <code>ESCAPE</code> key.
545      * @return a preconfigured input map for the support's
546      * <em>find next match</em> and <em>clear result</code> actions.
547      * @see #getClearAction()
548      * @see #getNextAction()
549      */
550     public InputMap createDefaultInputMap() {
551       final InputMap imap = new InputMap();
552       imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), NEXT_ACTION_ID);
553       imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), CLEAR_ACTION_ID);
554       return imap;
555     }
556 
557     private static final int MARKER_MARGIN = 10;
558     private static final Color EMPHASIZE_COLOR = new Color(153,204,0);
559     private static final Color HIGHLIGHT_COLOR = DemoDefaults.DEFAULT_CONTRAST_COLOR;
560 
561     /**
562      * <code>Drawable</code> that highlights search results by drawing a thick,
563      * colored border around search result nodes.
564      */
565     private final class Marker implements Drawable {
566       private final RoundRectangle2D.Double marker;
567 
568       Marker() {
569         marker = new RoundRectangle2D.Double();
570       }
571 
572       public void paint( final Graphics2D g ) {
573         if (searchResult != null && !searchResult.asCollection().isEmpty()) {
574           final Color oldColor = g.getColor();
575 
576           final Graph2D graph = view.getGraph2D();
577           for (NodeCursor nc = searchResult.nodes(); nc.ok(); nc.next()) {
578             final Node node = nc.node();
579             if (graph.isSelected(node)) {
580               g.setColor(EMPHASIZE_COLOR);
581             } else {
582               g.setColor(HIGHLIGHT_COLOR);
583             }
584 
585             final NodeRealizer nr = graph.getRealizer(node);
586             marker.setRoundRect(
587                     nr.getX() - MARKER_MARGIN,
588                     nr.getY() - MARKER_MARGIN,
589                     nr.getWidth() + 2* MARKER_MARGIN,
590                     nr.getHeight() + 2* MARKER_MARGIN,
591                     MARKER_MARGIN,
592                     MARKER_MARGIN);
593             g.fill(marker);
594           }
595 
596           g.setColor(oldColor);
597         }
598       }
599 
600       public Rectangle getBounds() {
601         if (searchResult == null || searchResult.asCollection().isEmpty()) {
602           final Point2D center = view.getCenter();
603           return new Rectangle(
604                   (int) Math.rint(center.getX()),
605                   (int) Math.rint(center.getY()),
606                   -1,
607                   -1);
608         } else {
609           final Rectangle bnds = new Rectangle(0, 0, -1, -1);
610           final Graph2D graph = view.getGraph2D();
611           for (NodeCursor nc = searchResult.nodes(); nc.ok(); nc.next()) {
612             graph.getRealizer(nc.node()).calcUnionRect(bnds);
613           }
614           bnds.grow(MARKER_MARGIN, MARKER_MARGIN);
615           return bnds;
616         }
617       }
618     }
619 
620     /**
621      * Stores nodes that make up a <em>search result</em> and manages an
622      * emphasis pointer to allow for <em>find next</em> and
623      * <em>find previous</em> actions.
624      */
625     public static final class SearchResult {
626       private final NodeList nodes;
627       private NodeCursor cursor;
628       private Node current;
629 
630       SearchResult() {
631         nodes = new NodeList();
632       }
633 
634       /**
635        * Add the specified node to the search result set.
636        * @param node   the <code>Node</code> to add.
637        */
638       void add( final Node node ) {
639         nodes.add(node);
640       }
641 
642       /**
643        * Returns a cursor over all nodes in the search result set.
644        * @return a cursor over all nodes in the search result set.
645        */
646       public NodeCursor nodes() {
647         return nodes.nodes();
648       }
649 
650       /**
651        * Returns the currently emphasized node or <code>null</code> if there is
652        * none.
653        * @return the currently emphasized node or <code>null</code> if there is
654        * none.
655        */
656       public Node emphasizedNode() {
657         return current;
658       }
659 
660       /**
661        * Resets the emphasis cursor, that is calling {@link #emphasizedNode()}
662        * afterwards will return <code>null</code>.
663        */
664       public void resetEmphasis() {
665         current = null;
666         cursor = null;
667       }
668 
669       /**
670        * Sets the emphasis pointer to the next node in the search result set.
671        * If the emphasized node is the last node in the set, this method will
672        * set the pointer to the first node in the set.
673        */
674       public void emphasizeNext() {
675         if (cursor == null) {
676           if (nodes.isEmpty()) {
677             return;
678           } else {
679             cursor = nodes.nodes();
680             cursor.toLast();
681           }
682         }
683         cursor.cyclicNext();
684         current = cursor.node();
685       }
686 
687       /**
688        * Sets the emphasis pointer to the previous node in the search result set.
689        * If the emphasized node is the first node in the set, this method will
690        * set the pointer to the last node in the set.
691        */
692       public void emphasizePrevious() {
693         if (cursor == null) {
694           if (nodes.isEmpty()) {
695             return;
696           } else {
697             cursor = nodes.nodes();
698             cursor.toFirst();
699           }
700         }
701         cursor.cyclicPrev();
702         current = cursor.node();
703       }
704 
705       /**
706        * Sorts the nodes in the search result set according to the order
707        * induced by the specified comparator.
708        * @param c   the <code>Comparator</code> to sort the nodes in the search
709        * result set.
710        */
711       void sort( final Comparator c ) {
712         nodes.sort(c);
713       }
714 
715       /**
716        * Returns a <code>Collection</code> handle for the search result.
717        * @return a <code>Collection</code> handle for the search result.
718        */
719       Collection asCollection() {
720         return nodes;
721       }
722     }
723 
724     /**
725      * Specifies the contract of search criteria for node searches.
726      */
727     public static interface SearchCriterion {
728       /**
729        * Returns <code>true</code> if the specified node should be included
730        * in the search result and <code>false</code> otherwise.
731        * @param graph   the <code>Graph2D</code> to which the specified node
732        * belongs.
733        * @param node   the <code>Node</code> to test for inclusion in the
734        * search result.
735        * @return <code>true</code> if the specified node should be included
736        * in the search result and <code>false</code> otherwise.
737        */
738       public boolean accept( Graph2D graph, Node node );
739     }
740   }
741 
742   /**
743    * Search support for finding nodes whose label text contains a specific
744    * text string.
745    */
746   public static final class LabelTextSearchSupport extends SearchSupport {
747     private JTextField searchField;
748 
749     public LabelTextSearchSupport( final Graph2DView view ) {
750       super(view);
751     }
752 
753     /**
754      * Creates a <em>clear search result</em> action that clears the
755      * support's associated search text field as well as the search result.
756      * @return a <em>clear search result</em> action that clears the
757      * support's associated search text field as well as the search result.
758      * @see #getSearchField()
759      */
760     protected Action createClearAction() {
761       return new AbstractAction("Clear") {
762         {
763           setEnabled(getSearchResult() != null);
764         }
765 
766         public void actionPerformed( final ActionEvent e ) {
767           final SearchResult searchResult = getSearchResult();
768           if (searchResult != null || searchField != null) {
769             if (searchField != null) {
770               searchField.setText("");
771             } else {
772               search(null, false);
773             }
774             getView().getGraph2D().unselectAll();
775             getView().updateView();
776           }
777         }
778       };
779     }
780 
781     /**
782      * Returns a search text field that allows for convenient input of search
783      * queries. The search field is configured to automatically update the
784      * support's search result whenever its text content changes.
785      * @return a search text field that allows for convenient input of search
786      * queries.
787      */
788     public JComponent getSearchField() {
789       if (searchField == null) {
790         searchField = new JTextField(25);
791         searchField.setMaximumSize(searchField.getPreferredSize());
792         searchField.getDocument().addDocumentListener(new DocumentListener() {
793           public void changedUpdate( final DocumentEvent e ) {
794           }
795 
796           public void insertUpdate( final DocumentEvent e ) {
797             final String text = searchField.getText();
798             search(text.length() == 0 ? null : new LabelTextSearchSupport.LabelText(text), true);
799             getView().updateView();
800           }
801 
802           public void removeUpdate( final DocumentEvent e ) {
803             final String text = searchField.getText();
804             search(text.length() == 0 ? null : new LabelTextSearchSupport.LabelText(text), false);
805             getView().updateView();
806           }
807         });
808         searchField.addActionListener(getNextAction());
809       }
810       return searchField;
811     }
812 
813 
814     /**
815      * <code>SearchCriterion</code> that matches nodes whose default label
816      * contains a specific text.
817      */
818     static final class LabelText implements SearchCriterion {
819       private final String query;
820 
821       /**
822        * Initializes a new <code>LabelText</code> search criterion for the
823        * specified query text.
824        * @param query   the text that has to be contained in the default labels
825        * of nodes which are accepted by the criterion.
826        */
827       LabelText( final String query ) {
828         this.query = query;
829       }
830 
831       /**
832        * Returns <code>true</code> if the specified node's default label
833        * contains the criterion's associated query string and <code>false</code>
834        * otherwise.
835        * @param graph   the <code>Graph2D</code> to which the specified node
836        * belongs.
837        * @param node   the <code>Node</code> to test for inclusion in the
838        * search result.
839        * @return <code>true</code> if the specified node's default label
840        * contains the criterion's associated query string and <code>false</code>
841        * otherwise.
842        */
843       public boolean accept( final Graph2D graph, final Node node ) {
844         final NodeRealizer nr = graph.getRealizer(node);
845         if (nr.labelCount() > 0) {
846           if (nr.getLabel().getText().indexOf(query) > -1) {
847             return true;
848           }
849         }
850         return false;
851       }
852     }
853   }
854 }
855