1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.graphexplorer;
15  
16  import y.view.Graph2D;
17  import y.view.hierarchy.DefaultNodeChangePropagator;
18  import y.view.hierarchy.HierarchyJTree;
19  import y.view.hierarchy.HierarchyManager;
20  import y.view.hierarchy.HierarchyTreeModel;
21  import y.view.hierarchy.HierarchyTreeTransferHandler;
22  
23  import java.awt.BorderLayout;
24  import java.awt.Color;
25  import java.awt.Dimension;
26  import java.awt.event.KeyAdapter;
27  import java.awt.event.KeyEvent;
28  import javax.swing.JLabel;
29  import javax.swing.JPanel;
30  import javax.swing.JScrollPane;
31  import javax.swing.JTextField;
32  import javax.swing.JTree;
33  import javax.swing.border.EmptyBorder;
34  import javax.swing.event.DocumentEvent;
35  import javax.swing.event.DocumentListener;
36  import javax.swing.tree.TreeModel;
37  import javax.swing.tree.TreePath;
38  
39  /**
40   * A panel that contains a tree view for the given graph model as well as a search field to search for specific nodes.
41   */
42  class SearchableTreeViewPanel extends JPanel {
43    private JTree jTree;
44    private TreePath lastMatch;
45  
46    SearchableTreeViewPanel( final Graph2D model ) {
47      setLayout(new BorderLayout());
48      jTree = configureHierarchyJTree(model);
49      jTree.setExpandsSelectedPaths(true);
50      jTree.setDragEnabled(false);
51  
52      final JScrollPane scrollPane = new JScrollPane(jTree);
53      scrollPane.setPreferredSize(new Dimension(200, 0));
54      scrollPane.setMinimumSize(new Dimension(200, 0));
55      add(addSearchPanel(jTree), BorderLayout.NORTH);
56      add(scrollPane, BorderLayout.CENTER);
57    }
58  
59    JTree getTree() {
60      return jTree;
61    }
62  
63    private JPanel addSearchPanel(final JTree currentTree) {
64      final JPanel searchPanel = new JPanel(new BorderLayout(5, 5));
65      searchPanel.setBorder(new EmptyBorder(2, 5, 2, 2));
66      searchPanel.add(new JLabel("Search"), BorderLayout.WEST);
67      final JTextField searchField = new JTextField();
68  
69      // A document listener handles changes of the search string.
70      searchField.getDocument().addDocumentListener(new DocumentListener() {
71        public void insertUpdate(DocumentEvent e) {
72          updateSelection(searchField, currentTree);
73        }
74  
75        public void removeUpdate(DocumentEvent e) {
76          updateSelection(searchField, currentTree);
77        }
78  
79        public void changedUpdate(DocumentEvent e) {
80          updateSelection(searchField, currentTree);
81        }
82      });
83  
84      // A key listener handles the special keys ENTER and ESCAPE.
85      // They do not change the document in any way.
86      searchField.addKeyListener(new KeyAdapter() {
87        public void keyPressed(KeyEvent e) {
88          int keyCode = e.getKeyCode();
89          if (keyCode == KeyEvent.VK_ESCAPE) {
90            // Clear search field, if ESCAPE is pressed.
91            clearSearchField(searchField);
92  
93            // Stop further event handling.
94            // -> Do not remove the focus.
95            e.consume();
96          } else if (keyCode == KeyEvent.VK_ENTER) {
97            // Scroll next match into view, if ENTER is pressed.
98            String searchString = searchField.getText();
99            if (currentTree != null && currentTree.getModel() != null && searchString != null && searchString.length() > 0) {
100             TreePath match = findNextMatch(searchString, new TreePath(currentTree.getModel().getRoot()), currentTree);
101             if (match != null) {
102               selectMatch(match, currentTree);
103             } else {
104               lastMatch = null;
105               TreePath wrappedMatch = findNextMatch(searchString, new TreePath(currentTree.getModel().getRoot()),
106                   currentTree);
107               if (wrappedMatch != null) {
108                 selectMatch(wrappedMatch, currentTree);
109               } else {
110                 noMatchExists(searchField);
111               }
112             }
113           }
114         }
115       }
116     });
117     searchPanel.add(searchField, BorderLayout.CENTER);
118     return searchPanel;
119   }
120 
121   private void selectMatch(final TreePath match, final JTree actualTree) {
122     actualTree.setSelectionPath(match);
123     actualTree.scrollPathToVisible(match);
124     lastMatch = match;
125   }
126 
127   private void clearSearchField(final JTextField searchField) {
128     searchField.setText("");
129     searchField.setBackground(Color.WHITE);
130     searchField.setForeground(Color.BLACK);
131     lastMatch = null;
132   }
133 
134   private void noMatchExists(final JTextField searchField) {
135     searchField.setBackground(Color.RED);
136     searchField.setForeground(Color.WHITE);
137     lastMatch = null;
138   }
139 
140   private void updateSelection(final JTextField searchField, final JTree actualTree) {
141     searchField.setBackground(Color.WHITE);
142     searchField.setForeground(Color.BLACK);
143     lastMatch = null;
144     if (actualTree != null && actualTree.getModel() != null) {
145       actualTree.clearSelection();
146       String searchString = searchField.getText();
147       if (searchString != null && searchString.length() > 0) {
148         TreePath match = findNextMatch(searchString, new TreePath(actualTree.getModel().getRoot()), actualTree);
149         if (match != null) {
150           selectMatch(match, actualTree);
151         } else {
152           noMatchExists(searchField);
153         }
154       }
155     }
156   }
157 
158   //find the next matching element in the tree that matches with the searchString
159   private TreePath findNextMatch(String searchString, TreePath parent, final JTree actualTree) {
160     TreeModel model = actualTree.getModel();
161     Object treeRoot = parent.getLastPathComponent();
162     for (int i = 0; i < model.getChildCount(treeRoot); i++) {
163       Object treeNode = model.getChild(treeRoot, i);
164       TreePath path = parent.pathByAddingChild(treeNode);
165       if (isMatch(treeNode, searchString)) {
166         if (lastMatch != null) {
167           if (path.equals(lastMatch)) {
168             lastMatch = null;
169           }
170         } else {
171           return path;
172         }
173       }
174       TreePath match = findNextMatch(searchString, path, actualTree);
175       if (match != null) {
176         return match;
177       }
178     }
179     return null;
180   }
181 
182   private boolean isMatch(Object treeNode, String searchString) {
183     // currently case insensitive prefix match
184     // This can be customized/made configurable at a later stage.
185     String text = treeNode.toString().toUpperCase();
186     return text.startsWith(searchString.toUpperCase());
187   }
188 
189   private JTree configureHierarchyJTree(Graph2D model) {
190     //propagates text label changes on nodes as change events on the hierarchy.
191     model.addGraph2DListener(new DefaultNodeChangePropagator());
192 
193     //create a TreeModel, that represents the hierarchy of the nodes.
194     HierarchyManager hierarchy = model.getHierarchyManager();
195     HierarchyTreeModel treeModel = new HierarchyTreeModel(hierarchy);
196 
197     //use a convenience comparator that sorts the elements in the tree model
198     //folder/group nodes will come before normal nodes
199     treeModel.setChildComparator(HierarchyTreeModel.createNodeStateComparator(hierarchy));
200 
201     //display the graph hierarchy in a special JTree using the given TreeModel
202     final JTree tree = new HierarchyJTree(hierarchy, treeModel);
203     tree.setEditable(false);
204 
205     //add drag and drop functionality to HierarchyJTree. The drag and drop gesture
206     //will allow to reorganize the group structure using HierarchyJTree.
207     tree.setDragEnabled(true);
208     tree.setTransferHandler(new HierarchyTreeTransferHandler(hierarchy));
209     return tree;
210   }
211 }
212