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