1
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
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 loadGraph("resource/SearchDemo.graphml");
73
74 view.getRenderingHints().put(
76 RenderingHints.KEY_FRACTIONALMETRICS,
77 RenderingHints.VALUE_FRACTIONALMETRICS_ON);
78
79 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 addHelpPane(helpFilePath);
88 }
89
90 protected Action createSaveAction() {
91 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
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
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
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 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
198 public Graph2DView getView() {
199 return view;
200 }
201
202
206 public SearchResult getSearchResult() {
207 return searchResult;
208 }
209
210
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
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 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 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 newCenterY = bnds.getCenterY() + (view.getHeight() - canvasHeight) * 0.5 / newZoom;
349 }
350
351 if (oldZoom != newZoom ||
352 oldCenter.getX() != newCenterX ||
353 oldCenter.getY() != newCenterY) {
354 view.focusView(newZoom, new Point2D.Double(newCenterX, newCenterY), true);
356 } else {
357 view.updateView();
358 }
359 }
360 }
361
362
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
386 public Action getClearAction() {
387 if (clear == null) {
388 clear = createClearAction();
389 }
390 return clear;
391 }
392
393
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
419 public Action getPreviousAction() {
420 if (previous == null) {
421 previous = createPreviousAction();
422 }
423 return previous;
424 }
425
426
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
450 public Action getNextAction() {
451 if (next == null) {
452 next = createNextAction();
453 }
454 return next;
455 }
456
457
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
481 public Action getSelectAllAction() {
482 if (selectAll == null) {
483 selectAll = createSelectAllAction();
484 }
485 return selectAll;
486 }
487
488
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 searchResult.resetEmphasis();
504 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 focusView(bnds);
516 } else {
517 view.updateView();
518 }
519 }
520 }
521 };
522 }
523
524
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
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
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
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
638 void add( final Node node ) {
639 nodes.add(node);
640 }
641
642
646 public NodeCursor nodes() {
647 return nodes.nodes();
648 }
649
650
656 public Node emphasizedNode() {
657 return current;
658 }
659
660
664 public void resetEmphasis() {
665 current = null;
666 cursor = null;
667 }
668
669
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
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
711 void sort( final Comparator c ) {
712 nodes.sort(c);
713 }
714
715
719 Collection asCollection() {
720 return nodes;
721 }
722 }
723
724
727 public static interface SearchCriterion {
728
738 public boolean accept( Graph2D graph, Node node );
739 }
740 }
741
742
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
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
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
818 static final class LabelText implements SearchCriterion {
819 private final String query;
820
821
827 LabelText( final String query ) {
828 this.query = query;
829 }
830
831
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