/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* SearchableTree.java
* Created: Sept 15, 2004
* By: Richard Webster
*/
package org.openquark.util.ui;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.openquark.util.Messages;
/**
* A tree which can show search results.
* @author Richard Webster
*/
public class SearchableTree implements SearchableComponent, TreeModelListener {
/** Use this message bundle to get string resources. */
private static Messages messages = UIMessages.instance;
/** The tree to make searchable. */
private final UtilTree tree;
/** The parent node for all search results. */
private DefaultMutableTreeNode searchNode;
/** Keep track of the last search performed. */
private String lastSearchText;
/** Keep track of whether a search is in progress. */
private boolean searching = false;
// TODO: rerunning the search each time a change is made to the tree could be problematic if many changes are done at once...
// If this turns out to be a problem, then it might be better to use a timer to rerun the search after a small delay.
// /** The timer used for updating the search results after changes are made to the tree. */
// private Timer searchUpdateTimer;
/**
* SearchableTree constructor.
* @param tree the tree to make searchable
*/
public SearchableTree(final UtilTree tree) {
super();
this.tree = tree;
tree.getModel().addTreeModelListener(SearchableTree.this);
tree.addPropertyChangeListener(JTree.TREE_MODEL_PROPERTY, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
rerunLastSearch();
tree.getModel().addTreeModelListener(SearchableTree.this);
}
});
installRenderer(tree);
// // A timer that runs once changes are made to the tree.
// searchUpdateTimer = new Timer(250, new ActionListener() {
// public void actionPerformed(ActionEvent e) {
// if (e.getSource() == searchUpdateTimer) {
// rerunLastSearch();
// }
// }
// });
// searchUpdateTimer.setRepeats(false);
}
/**
* Reruns the last search on the tree.
* This can be called after updating the tree to apply the search criteria to the new tree contents.
*/
public void rerunLastSearch() {
if (lastSearchText != null && lastSearchText.length() > 0) {
showSearchResults(lastSearchText, false);
}
}
/**
* Returns the tree model.
* @return the tree model
*/
private DefaultTreeModel getTreeModel() {
return (DefaultTreeModel) tree.getModel();
}
/**
* Returns the root node of the tree.
* @return the root node of the tree
*/
private DefaultMutableTreeNode getRootNode() {
return (DefaultMutableTreeNode) getTreeModel().getRoot();
}
/**
* Installs appropriate renderer to the tree to highlight search results.
* @param tree
*/
private void installRenderer(final UtilTree tree) {
// Wrap the existing cell renderer with a search tree cell renderer
TreeCellRenderer renderer = tree.getCellRenderer();
if (!(renderer instanceof SearchableTreeCellRenderer)) {
tree.setCellRenderer(SearchableTreeCellRenderer.create(renderer));
}
// Install a listener that listens for cell renderer changes
tree.addPropertyChangeListener(
JTree.CELL_RENDERER_PROPERTY,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
TreeCellRenderer renderer = (TreeCellRenderer) evt.getNewValue();
tree.setCellRenderer(SearchableTreeCellRenderer.create(renderer));
}
}
);
}
/**
* Sets up the tree for showing search results.
*/
private void initSearch() {
final DefaultMutableTreeNode rootNode = getRootNode();
// If the search node already exists, then make sure it is in the correct spot in the tree.
if (searchNode != null) {
int searchIndex = rootNode.getIndex(searchNode);
// If the search node is already the last child of the root, then do nothing.
if (rootNode.getChildCount() != 0 && searchIndex == rootNode.getChildCount() - 1) {
return;
}
// If the search node is in the wrong spot, then remove it.
if (searchIndex > 0) {
rootNode.remove(searchNode);
}
}
// Create the search node, if necessary.
if (searchNode == null) {
searchNode = new DefaultMutableTreeNode(new DisplayOnlyTreeItem(messages.getString("Search_Results_TreeNode_Caption"), UIUtilities.LoadImageIcon("/Resources/search_results.gif"))); //$NON-NLS-1$//$NON-NLS-2$
}
// Add the search node as the last child of the root.
rootNode.add(searchNode);
getTreeModel().nodeStructureChanged(rootNode);
}
/**
* Updates the tree to show search results for any items matching the specified substring.
* If an empty string is provided, then the search results will be cleared.
* @param searchSubstring the substring to be searched for, or an empty string to clear the search results
*/
public void showSearchResults(String searchSubstring) {
showSearchResults(searchSubstring, true);
}
/**
* Updates the tree to show search results for any items matching the specified substring.
* If an empty string is provided, then the search results will be cleared.
* @param searchSubstring the substring to be searched for, or an empty string to clear the search results
* @param scrollIntoView if True then the search results will be scrolled into view
*/
public void showSearchResults(String searchSubstring, boolean scrollIntoView) {
// Don't start another search when already searching.
if (searching) {
return;
}
searching = true;
// Trim any leading and trailing spaces from the search substring
searchSubstring = searchSubstring.trim();
// Set the search string in the renderer so that the renderer knows what to highlight
TreeCellRenderer renderer = tree.getCellRenderer();
if (renderer instanceof SearchableTreeCellRenderer) {
((SearchableTreeCellRenderer) renderer).setSearchString(searchSubstring);
}
try {
final DefaultMutableTreeNode rootNode = getRootNode();
tree.saveState();
// Make sure that the search node has been added to the tree.
initSearch();
// Clear the current search, if any.
searchNode.removeAllChildren();
if (searchSubstring == null || searchSubstring.length() == 0) {
// Remove the search node if the search text is cleared.
rootNode.remove(searchNode);
getTreeModel().nodeStructureChanged(rootNode);
tree.restoreSavedState();
// If we didn't search for anything scroll to the all first tree node.
if (rootNode.getChildCount() > 0) {
DefaultMutableTreeNode firstNode = (DefaultMutableTreeNode) rootNode.getFirstChild();
final TreePath firstNodePath = new TreePath(firstNode.getPath());
tree.setSelectionPath(firstNodePath);
tree.scrollPathToTop(firstNodePath);
}
return;
}
// Search for the substring without matching case.
searchSubstring = searchSubstring.toLowerCase();
List<MutableTreeNode> resultNodes = getSearchResults(searchSubstring);
for (final MutableTreeNode mutableTreeNode : resultNodes) {
searchNode.add(mutableTreeNode);
}
// Change the search node text to include the number of search results.
String searchNodeText = messages.getString("Search_Results_TreeNode_Caption") + " (" + resultNodes.size() + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
searchNode.setUserObject(new DisplayOnlyTreeItem(searchNodeText, UIUtilities.LoadImageIcon("/Resources/search_results.gif"))); //$NON-NLS-1$
getTreeModel().nodeStructureChanged(searchNode);
tree.restoreSavedState();
// Expand the search node, select it, and scroll it into view.
final TreePath searchNodePath = new TreePath(searchNode.getPath());
tree.expandPath(searchNodePath);
tree.setSelectionPath(searchNodePath);
if (scrollIntoView) {
// Invoke this later once the tree has synced up with the model.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
tree.scrollPathToTop(searchNodePath);
}
});
}
}
finally {
// Keep track of the last search text.
this.lastSearchText = searchSubstring;
searching = false;
}
}
/**
* Returns a list of nodes for the values matching the search string.
* @param searchString the string to search for
* @return a list of nodes for the values matching the search string
*/
private List<MutableTreeNode> getSearchResults(String searchString) {
final DefaultMutableTreeNode rootNode = getRootNode();
List<MutableTreeNode> resultNodes = new ArrayList<MutableTreeNode>();
if (rootNode != null) {
addNodeSearchResults(searchString, resultNodes, rootNode);
}
// Sort the resulting nodes by their display names (not case sensitive).
Collections.sort(resultNodes, new Comparator<MutableTreeNode>() {
public int compare(MutableTreeNode node1, MutableTreeNode node2) {
return node1.toString().compareToIgnoreCase(node2.toString());
}});
return resultNodes;
}
/**
* A recursive helper function for searching all nodes in a tree.
* @param searchString the search text
* @param resultNodes the current list of result nodes
* @param node the node to be searched
*/
private void addNodeSearchResults(String searchString, List<MutableTreeNode> resultNodes, TreeNode node) {
// Check whether the current node is searchable.
if (node instanceof SearchableTreeNode) {
SearchableTreeNode searchableNode = (SearchableTreeNode) node;
MutableTreeNode searchResult = searchableNode.searchNode(searchString);
if (searchResult != null) {
resultNodes.add(searchResult);
}
}
// Check any child nodes as well.
for (int childN = 0, nChildren = node.getChildCount(); childN < nChildren; ++childN) {
addNodeSearchResults(searchString, resultNodes, node.getChildAt(childN));
}
}
/**
* @see javax.swing.event.TreeModelListener#treeNodesChanged(javax.swing.event.TreeModelEvent)
*/
public void treeNodesChanged(TreeModelEvent e) {
// if (!searching) {
// searchUpdateTimer.restart();
// }
rerunLastSearch();
}
/**
* @see javax.swing.event.TreeModelListener#treeNodesInserted(javax.swing.event.TreeModelEvent)
*/
public void treeNodesInserted(TreeModelEvent e) {
// if (!searching) {
// searchUpdateTimer.restart();
// }
rerunLastSearch();
}
/**
* @see javax.swing.event.TreeModelListener#treeNodesRemoved(javax.swing.event.TreeModelEvent)
*/
public void treeNodesRemoved(TreeModelEvent e) {
// if (!searching) {
// searchUpdateTimer.restart();
// }
rerunLastSearch();
}
/**
* @see javax.swing.event.TreeModelListener#treeStructureChanged(javax.swing.event.TreeModelEvent)
*/
public void treeStructureChanged(TreeModelEvent e) {
// if (!searching) {
// searchUpdateTimer.restart();
// }
rerunLastSearch();
}
}