| SearchDemo.java |
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