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