1
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
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 loadGraph("resource/SearchDemo.graphml");
87
88 view.getRenderingHints().put(
90 RenderingHints.KEY_FRACTIONALMETRICS,
91 RenderingHints.VALUE_FRACTIONALMETRICS_ON);
92
93 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 addHelpPane(helpFilePath);
102 }
103
104 protected Action createSaveAction() {
105 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
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
142 protected boolean isUndoRedoEnabled() {
143 return false;
144 }
145
146
149 protected boolean isClipboardEnabled() {
150 return false;
151 }
152
153
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
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 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
226 public Graph2DView getView() {
227 return view;
228 }
229
230
234 public SearchResult getSearchResult() {
235 return searchResult;
236 }
237
238
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
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 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 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 newCenterY = bnds.getCenterY() + (view.getHeight() - canvasHeight) * 0.5 / newZoom;
377 }
378
379 if (oldZoom != newZoom ||
380 oldCenter.getX() != newCenterX ||
381 oldCenter.getY() != newCenterY) {
382 view.focusView(newZoom, new Point2D.Double(newCenterX, newCenterY), true);
384 } else {
385 view.updateView();
386 }
387 }
388 }
389
390
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
414 public Action getClearAction() {
415 if (clear == null) {
416 clear = createClearAction();
417 }
418 return clear;
419 }
420
421
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
447 public Action getPreviousAction() {
448 if (previous == null) {
449 previous = createPreviousAction();
450 }
451 return previous;
452 }
453
454
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
478 public Action getNextAction() {
479 if (next == null) {
480 next = createNextAction();
481 }
482 return next;
483 }
484
485
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
509 public Action getSelectAllAction() {
510 if (selectAll == null) {
511 selectAll = createSelectAllAction();
512 }
513 return selectAll;
514 }
515
516
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 searchResult.resetEmphasis();
532 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 focusView(bnds);
544 } else {
545 view.updateView();
546 }
547 }
548 }
549 };
550 }
551
552
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
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
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
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
666 void add( final Node node ) {
667 nodes.add(node);
668 }
669
670
674 public NodeCursor nodes() {
675 return nodes.nodes();
676 }
677
678
684 public Node emphasizedNode() {
685 return current;
686 }
687
688
692 public void resetEmphasis() {
693 current = null;
694 cursor = null;
695 }
696
697
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
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
739 void sort( final Comparator c ) {
740 nodes.sort(c);
741 }
742
743
747 Collection asCollection() {
748 return nodes;
749 }
750 }
751
752
755 public static interface SearchCriterion {
756
766 public boolean accept( Graph2D graph, Node node );
767 }
768 }
769
770
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
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
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
846 static final class LabelText implements SearchCriterion {
847 private final String query;
848
849
855 LabelText( final String query ) {
856 this.query = query;
857 }
858
859
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