blob: 304e50a8c6aa794eed2fc95f51abf7a6df0e1152 [file] [log] [blame]
package net.sf.taverna.biocatalogue.ui.tristatetree;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import net.sf.taverna.biocatalogue.model.ResourceManager;
import net.sf.taverna.t2.workbench.icons.WorkbenchIcons;
/**
* @author Sergejs Aleksejevs
*/
@SuppressWarnings("serial")
public class JTriStateTree extends JTree
{
// This is used to manage location and padding of tooltips on long items
// that don't fit into the visible part of this tree.
private static final int JCHECKBOX_WIDTH = 16;
private JTriStateTree instanceOfSelf;
private JPopupMenu contextualMenu;
private TriStateTreeNode root;
// will enable/disable checkboxes - when disabled the selection
// will remain, but will appear as read-only
private boolean bCheckingEnabled;
private List<Action> contextualMenuActions;
private Action expandAllAction;
private Action collapseAllAction;
private Action selectAllAction;
private Action deselectAllAction;
private Set<TriStateTreeCheckingListener> checkingListeners;
@SuppressWarnings("serial")
public JTriStateTree(TriStateTreeNode root)
{
super(root);
this.root = root;
this.instanceOfSelf = this;
// by default checking is allowed
this.bCheckingEnabled = true;
// initially, no checking listeners
checkingListeners = new HashSet<TriStateTreeCheckingListener>();
// initially set to show the [+]/[-] icons for expanding collapsing top-level nodes
this.setShowsRootHandles(true);
// use the cell rendered which understands the three states of the
// nodes of this tree
this.setCellRenderer(new TriStateCheckBoxTreeCellRenderer());
// create all necessary actions for the popup menu: selecting/deselecting and expanding/collapsing all nodes
this.selectAllAction = new AbstractAction("Select all", ResourceManager.getImageIcon(ResourceManager.SELECT_ALL_ICON))
{
// Tooltip
{ this.putValue(SHORT_DESCRIPTION, "Select all nodes in the tree"); }
public void actionPerformed(ActionEvent e) {
selectAllNodes(true);
}
};
// Use the Taverna untick icon
this.deselectAllAction = new AbstractAction("Deselect all", ResourceManager.getImageIcon(ResourceManager.UNCHECKED_ICON))
{
// Tooltip
{ this.putValue(SHORT_DESCRIPTION, "Deselect all nodes in the tree"); }
public void actionPerformed(ActionEvent e) {
selectAllNodes(false);
}
};
// this.expandAllAction = new AbstractAction("Expand all", ResourceManager.getImageIcon(ResourceManager.EXPAND_ALL_ICON))
// Use the standard Taverna plus icon
this.expandAllAction = new AbstractAction("Expand all", WorkbenchIcons.plusIcon)
{
// Tooltip
{ this.putValue(SHORT_DESCRIPTION, "Expand all nodes in the tree"); }
public void actionPerformed(ActionEvent e) {
expandAll();
}
};
// this.collapseAllAction = new AbstractAction("Collapse all", ResourceManager.getImageIcon(ResourceManager.COLLAPSE_ALL_ICON))
// Use the standard Taverna minus icon
this.collapseAllAction = new AbstractAction("Collapse all", WorkbenchIcons.minusIcon)
{
// Tooltip
{ this.putValue(SHORT_DESCRIPTION, "Collapse all expanded nodes in the tree"); }
public void actionPerformed(ActionEvent e) {
collapseAll();
}
};
// populate the popup menu with created menu items
contextualMenuActions = Arrays.asList(new Action[] {expandAllAction, collapseAllAction, deselectAllAction});
contextualMenu = new JPopupMenu();
contextualMenu.add(expandAllAction);
contextualMenu.add(collapseAllAction);
contextualMenu.add(new JPopupMenu.Separator());
//contextualMenu.add(selectAllAction);
contextualMenu.add(deselectAllAction);
this.addMouseListener(new MouseAdapter() {
// use mousePressed, not mouseClicked to make sure that
// quick successive clicks get processed correctly, otherwise
// some clicks are disregarded
public void mousePressed(MouseEvent e)
{
// only listen to checkbox checking requests if this is
// a correct type of mouse event for this
if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1)
{
int clickedRow = instanceOfSelf.getRowForLocation(e.getX(), e.getY());
// only make changes to node selections if checking is enabled in the tree and
// it was a node which was clicked, not [+]/[-] or blank space
if (bCheckingEnabled && clickedRow != -1)
{
Object clickedObject = instanceOfSelf.getPathForRow(clickedRow).getLastPathComponent();
if (clickedObject instanceof TriStateTreeNode) {
TriStateTreeNode node = ((TriStateTreeNode)clickedObject);
// toggle state of the clicked node + propagate the changes to
// the checking state of all nodes
node.toggleState(true);
// repaint the whole tree
SwingUtilities.invokeLater(new Runnable() {
public void run() {
instanceOfSelf.repaint();
}
});
// notify all listeners
notifyCheckingListeners();
}
}
}
else {
// not a checking action - instead, bring up a popup menu
contextualMenu.show(instanceOfSelf, e.getX(), e.getY());
}
}
public void mouseReleased(MouseEvent e)
{
if (e.isPopupTrigger()) {
// another way a popup menu may be called on different systems
contextualMenu.show(instanceOfSelf, e.getX(), e.getY());
}
}
/**
* This method enables tooltips on this instance of JTriStateTree
* when mouse enters its bounds. Custom tooltips will be used, but
* this notifies ToolTipManager that tooltips must be shown on this
* tree.
*/
public void mouseEntered(MouseEvent e) {
instanceOfSelf.setToolTipText("Filter tree");
}
/**
* Removes tooltips from this JTriStateTree when mouse leaves its bounds.
*/
public void mouseExited(MouseEvent e) {
instanceOfSelf.setToolTipText(null);
}
});
}
/**
* This method is used to determine tooltip location.
*
* Helps to ensure that the tooltip appears directly over the
* text in the row over which the mouse currently hovers.
*/
public Point getToolTipLocation(MouseEvent e)
{
int iRowIndex = this.getRowForLocation(e.getX(), e.getY());
if (iRowIndex != -1) {
// mouse hovers over one of the rows - make top-left corner of
// the tooltip to appear over the top-left corner of that row
Rectangle bounds = this.getRowBounds(iRowIndex);
return (new Point(bounds.x + JCHECKBOX_WIDTH, bounds.y));
}
else {
// let ToolTipManager determine where to show the tooltip (if it will be shown)
return null;
}
}
/**
* Supports dynamic tooltips for the contents of this JTriStateTree -
* the tooltips will only be shown for those tree nodes that don't
* fully fit within the visible bounds of the tree.
*
* For other nodes no tooltip will be shown.
*/
public String getToolTipText(MouseEvent e)
{
String strTooltip = null;
Object correspondingObject = getTreeNodeObject(e);
if (correspondingObject != null) {
// mouse is hovering over some row in the tree, not a blank space --
// obtain a component that is identical to the one which is currently displayed at the identified row in the tree
Component rendering = this.getCellRenderer().getTreeCellRendererComponent(this, correspondingObject, false, false,
true, this.getRowForLocation(e.getX(), e.getY()), false);
if (rendering.getPreferredSize().width + getToolTipLocation(e).x - JCHECKBOX_WIDTH > this.getVisibleRect().width) {
// if the component is not fully visible, the tooltip will be displayed -
// tooltip text matches the one for this row in the tree, will just be shown in full
strTooltip = correspondingObject.toString();
}
}
// return either tooltip text or 'null' if no tooltip is currently required
return (strTooltip);
}
/**
* Check whether a {@link MouseEvent} happened in such a location
* in the {@link JTriStateTree} that corresponds to some node or a
* blank space.
*
* @param e
* @return Object contained in the tree node that corresponds to the
* location of specified {@link MouseEvent} <code>e</code>;
* or <code>null</code> if the event happened over a blank space.
*/
public Object getTreeNodeObject(MouseEvent e)
{
int iRowIndex = this.getRowForLocation(e.getX(), e.getY());
if (iRowIndex != -1) {
// mouse is hovering over some row in the tree, not a blank space
return (this.getPathForRow(iRowIndex).getLastPathComponent());
}
return (null);
}
/**
* @return List of the highest-level nodes of the tree that have full (not partial) selection and,
* therefore, act as roots of checked paths.
*/
public List<TreePath> getRootsOfCheckedPaths()
{
return getRootsOfCheckedPaths(this.root);
}
/**
* A recursive version of the getCheckedRootsOfCheckedPaths().
* Performs all the work for a given node and returns result to
* the caller.
*
* @param startNode Node to start with.
* @return
*/
private List<TreePath> getRootsOfCheckedPaths(TriStateTreeNode startNode)
{
ArrayList<TreePath> pathsToRootsOfCheckings = new ArrayList<TreePath>();
Object currentNode = null;
for (Enumeration e = startNode.children(); e.hasMoreElements(); )
{
currentNode = e.nextElement();
if (currentNode instanceof TriStateTreeNode) {
TriStateTreeNode curTriStateNode = (TriStateTreeNode)currentNode;
if (curTriStateNode.getState().equals(TriStateCheckBox.State.CHECKED)) {
pathsToRootsOfCheckings.add(new TreePath(curTriStateNode.getPath()));
}
else if (curTriStateNode.getState().equals(TriStateCheckBox.State.PARTIAL)) {
pathsToRootsOfCheckings.addAll(getRootsOfCheckedPaths(curTriStateNode));
}
}
}
return (pathsToRootsOfCheckings);
}
/**
* @return List of TreePath objects, where the last component in each
* path is the root of an unchecked path in this tree. In other
* words each of those last components is either an unchecked
* leaf node or a node, none of whose children (and itself as
* well) are checked.
*/
public List<TreePath> getRootsOfUncheckedPaths()
{
return (getRootsOfUncheckedPaths(this.root));
}
/**
* Recursive worker method for <code>getRootsOfUncheckedPaths()</code>.
*/
private List<TreePath> getRootsOfUncheckedPaths(TreeNode startNode)
{
List<TreePath> rootNodesOfUncheckedPaths = new ArrayList<TreePath>();
Object currentNode = null;
for (Enumeration e = startNode.children(); e.hasMoreElements(); )
{
currentNode = e.nextElement();
if (!(currentNode instanceof TriStateTreeNode) ||
((TriStateTreeNode)currentNode).getState().equals(TriStateCheckBox.State.UNCHECKED))
{
rootNodesOfUncheckedPaths.add(new TreePath(((DefaultMutableTreeNode)currentNode).getPath()));
}
else {
rootNodesOfUncheckedPaths.addAll(getRootsOfUncheckedPaths((TreeNode)currentNode));
}
}
return (rootNodesOfUncheckedPaths);
}
/**
* @return List of TreePath objects, that point to all "leaf"
* nodes in the tree that are checked - in other words
* this method returns a collection of paths to all "deepest"
* nodes in this tree that are checked and do not have any
* (checked) children.
*/
public List<TreePath> getLeavesOfCheckedPaths() {
return (getLeavesOfCheckedPaths(this.root));
}
/**
* Recursive worker method for {@link JTriStateTree#getLeavesOfCheckedPaths()}
*/
private List<TreePath> getLeavesOfCheckedPaths(TriStateTreeNode startNode)
{
List<TreePath> leavesOfCheckedPaths = new ArrayList<TreePath>();
// this node is only relevant if it is checked itself - if not,
// it must be the first-level child of another node that is checked
// and is only considered here on the recursive pass (but will be discarded)
if (startNode.getState().equals(TriStateCheckBox.State.CHECKED) ||
startNode.getState().equals(TriStateCheckBox.State.PARTIAL))
{
// "ask" all children to do the same...
Object currentNode = null;
for (Enumeration e = startNode.children(); e.hasMoreElements(); ) {
currentNode = e.nextElement();
if (currentNode instanceof TriStateTreeNode) {
leavesOfCheckedPaths.addAll(getLeavesOfCheckedPaths((TriStateTreeNode)currentNode));
}
}
// ...if we have a list of leaf nodes, then this node can't be a leaf;
// -> but alternatively, if the list is empty, it means that this node is
// itself a leaf node and must be added to the result
if (leavesOfCheckedPaths.isEmpty()) {
leavesOfCheckedPaths.add(new TreePath(startNode.getPath()));
}
}
return (leavesOfCheckedPaths);
}
/**
* @return List of all contextual menu actions that are available for this tree.
*/
public List<Action> getContextualMenuActions() {
return this.contextualMenuActions;
}
/**
* Enables or disables all actions in the contextual menu
* @param actionsAreEnabled
*/
public void enableAllContextualMenuAction(boolean actionsAreEnabled) {
for (Action a : getContextualMenuActions()) {
a.setEnabled(actionsAreEnabled);
}
}
/**
* Selects or deselects all nodes.
* @param selectAll True - to select all; false - to reset all selections.
*/
public void selectAllNodes(boolean selectAll) {
root.setState(selectAll ? TriStateCheckBox.State.CHECKED : TriStateCheckBox.State.UNCHECKED);
root.updateStateOfRelatedNodes();
this.repaint();
// even though this isn't a click in the tree, the selection has changed -
// notify all listeners
notifyCheckingListeners();
}
/**
* TODO - this method doesn't take into account a possibility that the
* filter tree might have changed
*
* @param rootsOfCheckedPaths A list of TreePath objects which represent a checking state of
* the nodes in this tree (as returned by <code>getRootsOfCheckedPaths()</code>).
*
* The last node of each path is the one that should have <code>TriStateCheckBox.State.CHECKED</code>
* state (so that last node is a root of checked path that start at that node). Related partial
* checkings for the UI can be computed from that by the tree checking model.
*
* Therefore, a single "real" checking per provided TreePath from <code>rootsOfCheckedPaths</code> is
* made.
*/
public void restoreFilterCheckingSettings(List<TreePath> rootsOfCheckedPaths)
{
// start with removing all selections
this.selectAllNodes(false);
for (TreePath p : rootsOfCheckedPaths) {
restoreTreePathCheckingSettings(this.root, p);
}
}
/**
* A worker method for <code>restoreFilterCheckingSettings(List<TreePath> rootsOfCheckedPaths)</code>.
* See that method for further details.
*
* @param startNode A node of this tree.
* @param pathFromStartNode A TreePath object from the stored filter, where the first node must be
* equals to <code>startNode</code> (based on the <code>userObject</code>,
* but not the checking state), should the traversal of the tree result in
* checking the last node of this TreePath eventually - which is the goal
* of this method.
* @return True if traversal of <code>pathFromStartNode</code> succeeded and a node in this tree was checked;
* false if traversal couldn't find a matching node in this tree, and so no checking was made.
*/
private boolean restoreTreePathCheckingSettings(TriStateTreeNode startNode, TreePath pathFromStartNode)
{
if (startNode == null || pathFromStartNode == null || pathFromStartNode.getPathCount() == 0) {
// no match - no data to work with
return (false);
}
// compare the "roots" - provided start node and the root of the provided path
// (based on the 'user object', but not the selection state)
if (startNode.equals(pathFromStartNode.getPathComponent(0)))
{
if (pathFromStartNode.getPathCount() == 1) {
// provided startNode is equals to the only node in the provided tree path -
// so it is the node to mark as checked; also - make sure that this selection
// propagates through tree
startNode.setState(TriStateCheckBox.State.CHECKED, true);
// we've found the required node in this path - no further search needed,
// so terminate this method
return (true);
}
else {
// provided startNode is equals to the first node of the provided tree path -
// meaning that at this stage we need to traverse all children of the startNode
// and look for the child that would match the next element in the provided tree path
//
// to do this, produce a new tree path from the provided one that doesn't contain
// the first node - then proceed recursively
Object[] currentPathComponents = pathFromStartNode.getPath();
Object[] reducedPathComponents = new Object[currentPathComponents.length - 1];
System.arraycopy(currentPathComponents, 1, reducedPathComponents, 0, currentPathComponents.length - 1);
Enumeration children = startNode.children();
while (children.hasMoreElements()) {
TriStateTreeNode currentChild = (TriStateTreeNode)children.nextElement();
// if recursive call succeeds, no need to iterate any further
if (restoreTreePathCheckingSettings(currentChild, new TreePath(reducedPathComponents))) return (true);
}
}
}
// the startNode doesn't match the the first element in the provided tree path
// or no match could be found during recursive search for the node to "check"
return (false);
}
/**
* Expands all paths in this tree.
*/
public void expandAll()
{
// this simply expands all tree nodes
// TODO - this actually "freezes" the UI if there are many nodes in the tree
// some better solution to be found (e.g. expand the nodes in the model, then update UI, or similar)
for (int i = 0; i < getRowCount(); i++) {
instanceOfSelf.expandRow(i);
}
}
/**
* Collapses all paths in this tree.
*/
public void collapseAll()
{
// this simply collapses all expanded nodes - this is very quick, execute just as it is
for (int i = getRowCount() - 1; i >= 0; i--) {
instanceOfSelf.collapseRow(i);
}
}
/**
* Removes all nodes in this tree that are unchecked.
*
* It doesn't iterate through *all* nodes - if some node is
* indeed unchecked, it removes that node and any children that
* it has (because unchecked node is the root of an unchecked path).
*/
public void removeAllUncheckedNodes()
{
// get the tree model first - will be used to remove the nodes
DefaultTreeModel theTreeModel = (DefaultTreeModel)this.treeModel;
// remove unchecked nodes
List<TreePath> allNodesToRemove = this.getRootsOfUncheckedPaths();
for (TreePath p : allNodesToRemove) {
theTreeModel.removeNodeFromParent((MutableTreeNode)p.getLastPathComponent());
}
}
/**
* Provides access to the contextual menu of this JTriStateTree.
*
* @return Reference to the contextual menu.
*/
public JPopupMenu getContextualMenu() {
return contextualMenu;
}
public void setCheckingEnabled(boolean bCheckingEnabled) {
this.bCheckingEnabled = bCheckingEnabled;
}
/**
* @return True if the current state of this JTriStateTree
* allows making changes to checking of checkboxes
* in its nodes.
*/
public boolean isCheckingEnabled() {
return bCheckingEnabled;
}
/**
* @param listener New tree checking listener to register for updates
* to tree node selections.
*/
public void addCheckingListener(TriStateTreeCheckingListener listener) {
if (listener != null) {
this.checkingListeners.add(listener);
}
}
/**
* @param listener Tree checking listener to remove.
*/
public void removeCheckingListener(TriStateTreeCheckingListener listener) {
if (listener != null) {
this.checkingListeners.remove(listener);
}
}
/**
* Sends a signal to all listeners to check the state of the tree,
* as it has changed.
*/
private void notifyCheckingListeners() {
for (TriStateTreeCheckingListener listener : this.checkingListeners) {
listener.triStateTreeCheckingChanged(instanceOfSelf);
}
}
}