![JAR search and dependency download from the Maven repository](/logo.png)
org.zaproxy.zap.view.JCheckBoxTree Maven / Gradle / Ivy
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2014 The ZAP Development Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.zaproxy.zap.view;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
/**
* Originally based on code from
* http://stackoverflow.com/questions/21847411/java-swing-need-a-good-quality-developed-jtree-with-checkboxes
* Applied the JCheckBoxTree.this fix suggested in the comments Also added expandAll(),
* collapseAll() and setCheckBoxEnabled(..)
*
* Still TODO: Support tri-state checkboxes Proper fix for top level node getting truncated
*
* @author simon
*/
public class JCheckBoxTree extends JTree {
private static final long serialVersionUID = -4194122328392241790L;
// Defining data structure that will enable to fast check-indicate the state of each node
// It totally replaces the "selection" mechanism of the JTree
private class CheckedNode {
boolean isSelected;
boolean hasChildren;
boolean allChildrenSelected;
boolean isCheckBoxEnabled = true;
public CheckedNode(
boolean isSelected_, boolean hasChildren_, boolean allChildrenSelected_) {
isSelected = isSelected_;
hasChildren = hasChildren_;
allChildrenSelected = allChildrenSelected_;
}
}
HashMap nodesCheckingState;
HashSet checkedPaths = new HashSet<>();
// Defining a new event type for the checking mechanism and preparing event-handling mechanism
public class CheckChangeEvent extends EventObject {
private static final long serialVersionUID = -8100230309044193368L;
public CheckChangeEvent(Object source) {
super(source);
}
}
public interface CheckChangeEventListener extends EventListener {
public void checkStateChanged(CheckChangeEvent event);
}
public void addCheckChangeEventListener(CheckChangeEventListener listener) {
listenerList.add(CheckChangeEventListener.class, listener);
}
public void removeCheckChangeEventListener(CheckChangeEventListener listener) {
listenerList.remove(CheckChangeEventListener.class, listener);
}
void fireCheckChangeEvent(CheckChangeEvent evt) {
Object[] listeners = listenerList.getListenerList();
// Note that the listeners list contains "pair of entries",
// the first is the class and the following the instance
for (int i = 0; i < listeners.length - 1; i += 2) {
if (listeners[i] == CheckChangeEventListener.class) {
((CheckChangeEventListener) listeners[i + 1]).checkStateChanged(evt);
}
}
}
// Override
@Override
public void setModel(TreeModel newModel) {
resetCheckingState(newModel != null ? (DefaultMutableTreeNode) newModel.getRoot() : null);
super.setModel(newModel);
}
// New method that returns only the checked paths (totally ignores original "selection"
// mechanism)
public TreePath[] getCheckedPaths() {
return checkedPaths.toArray(new TreePath[checkedPaths.size()]);
}
public boolean isChecked(TreePath path) {
CheckedNode cn = nodesCheckingState.get(path);
return cn.isSelected;
}
// Returns true in case that the node is selected, has children but not all of them are selected
public boolean isSelectedPartially(TreePath path) {
CheckedNode cn = nodesCheckingState.get(path);
return cn.isSelected && cn.hasChildren && !cn.allChildrenSelected;
}
private void resetCheckingState(DefaultMutableTreeNode rootNode) {
nodesCheckingState = new HashMap<>();
checkedPaths = new HashSet<>();
if (rootNode == null) {
return;
}
addSubtreeToCheckingStateTracking(rootNode);
}
// Creating data structure of the current model for the checking mechanism
private void addSubtreeToCheckingStateTracking(DefaultMutableTreeNode node) {
TreeNode[] path = node.getPath();
TreePath tp = new TreePath(path);
CheckedNode cn = new CheckedNode(false, node.getChildCount() > 0, false);
nodesCheckingState.put(tp, cn);
for (int i = 0; i < node.getChildCount(); i++) {
addSubtreeToCheckingStateTracking(
(DefaultMutableTreeNode)
tp.pathByAddingChild(node.getChildAt(i)).getLastPathComponent());
}
}
// Overriding cell renderer by a class that ignores the original "selection" mechanism
// It decides how to show the nodes due to the checking-mechanism
private class CheckBoxCellRenderer extends JPanel implements TreeCellRenderer {
private static final long serialVersionUID = -7341833835878991719L;
JCheckBox checkBox;
JLabel altLabel;
public CheckBoxCellRenderer() {
super();
this.setLayout(new BorderLayout());
checkBox = new JCheckBox();
altLabel = new JLabel("");
altLabel.setOpaque(true);
add(checkBox, BorderLayout.CENTER);
add(altLabel, BorderLayout.EAST);
setOpaque(false);
}
@Override
public Component getTreeCellRendererComponent(
JTree tree,
Object value,
boolean selected,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
Object obj = node.getUserObject();
TreePath tp = new TreePath(node.getPath());
altLabel.setText(obj != null ? obj.toString() : "");
altLabel.setForeground(
UIManager.getColor(
selected ? "Tree.selectionForeground" : "Tree.textForeground"));
altLabel.setBackground(
UIManager.getColor(
selected ? "Tree.selectionBackground" : "Tree.textBackground"));
CheckedNode cn = nodesCheckingState.get(tp);
if (cn == null) {
checkBox.setVisible(false);
return this;
}
if (cn.isCheckBoxEnabled) {
checkBox.setSelected(cn.isSelected);
checkBox.setOpaque(cn.isSelected && cn.hasChildren && !cn.allChildrenSelected);
checkBox.setVisible(true);
checkBox.setEnabled(true);
/* Looks ok, but doesn't work correctly
if (cn.isSelected && cn.hasChildren && ! cn.allChildrenSelected) {
checkBox.getModel().setPressed(true);
checkBox.getModel().setArmed(true);
} else {
checkBox.getModel().setPressed(false);
checkBox.getModel().setArmed(false);
}
*/
} else {
checkBox.setVisible(false);
checkBox.setEnabled(false);
}
return this;
}
}
public JCheckBoxTree() {
super();
// Disabling toggling by double-click
this.setToggleClickCount(0);
// Overriding cell renderer by new one defined above
CheckBoxCellRenderer cellRenderer = new CheckBoxCellRenderer();
this.setCellRenderer(cellRenderer);
// Overriding selection model by an empty one
DefaultTreeSelectionModel dtsm =
new DefaultTreeSelectionModel() {
private static final long serialVersionUID = -8190634240451667286L;
// Totally disabling the selection mechanism
@Override
public void setSelectionPath(TreePath path) {}
@Override
public void addSelectionPath(TreePath path) {}
@Override
public void removeSelectionPath(TreePath path) {}
@Override
public void setSelectionPaths(TreePath[] pPaths) {}
};
// Calling checking mechanism on mouse click
this.addMouseListener(
new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent arg0) {
TreePath tp =
JCheckBoxTree.this.getPathForLocation(arg0.getX(), arg0.getY());
if (tp == null) {
return;
}
if (!nodesCheckingState.get(tp).isCheckBoxEnabled) {
return;
}
boolean checkMode = !nodesCheckingState.get(tp).isSelected;
checkSubTree(tp, checkMode);
updatePredecessorsWithCheckMode(tp, checkMode);
// Firing the check change event
fireCheckChangeEvent(new CheckChangeEvent(new Object()));
// Repainting tree after the data structures were updated
JCheckBoxTree.this.repaint();
}
});
this.setSelectionModel(dtsm);
}
// When a node is checked/unchecked, updating the states of the predecessors
protected void updatePredecessorsWithCheckMode(TreePath tp, boolean check) {
TreePath parentPath = tp.getParentPath();
// If it is the root, stop the recursive calls and return
if (parentPath == null) {
return;
}
CheckedNode parentCheckedNode = nodesCheckingState.get(parentPath);
DefaultMutableTreeNode parentNode =
(DefaultMutableTreeNode) parentPath.getLastPathComponent();
parentCheckedNode.allChildrenSelected = true;
parentCheckedNode.isSelected = false;
for (int i = 0; i < parentNode.getChildCount(); i++) {
TreePath childPath = parentPath.pathByAddingChild(parentNode.getChildAt(i));
CheckedNode childCheckedNode = nodesCheckingState.get(childPath);
// It is enough that even one subtree is not fully selected
// to determine that the parent is not fully selected
if (!allSelected(childCheckedNode)) {
parentCheckedNode.allChildrenSelected = false;
}
// If at least one child is selected, selecting also the parent
if (childCheckedNode.isSelected) {
parentCheckedNode.isSelected = true;
}
}
if (parentCheckedNode.isSelected) {
checkedPaths.add(parentPath);
} else {
checkedPaths.remove(parentPath);
}
// Go to upper predecessor
updatePredecessorsWithCheckMode(parentPath, check);
}
private boolean allSelected(CheckedNode checkedNode) {
if (!checkedNode.isSelected) {
return false;
}
if (checkedNode.hasChildren) {
return checkedNode.allChildrenSelected;
}
return true;
}
// Recursively checks/unchecks a subtree
public void checkSubTree(TreePath tp, boolean check) {
CheckedNode cn = nodesCheckingState.get(tp);
cn.isSelected = check;
DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp.getLastPathComponent();
for (int i = 0; i < node.getChildCount(); i++) {
checkSubTree(tp.pathByAddingChild(node.getChildAt(i)), check);
}
cn.allChildrenSelected = check;
if (check) {
checkedPaths.add(tp);
} else {
checkedPaths.remove(tp);
}
updatePredecessorsAllChildrenSelectedState(tp);
}
public void expandAll() {
for (int i = 0; i < getRowCount(); i++) {
expandRow(i);
}
}
public void collapseAll() {
for (int i = getRowCount(); i >= 0; i--) {
this.collapseRow(i);
}
}
public boolean isSelectedFully(TreePath path) {
CheckedNode cn = nodesCheckingState.get(path);
return allSelected(cn);
}
// Recursively checks/unchecks a subtree
public void check(TreePath tp, boolean check) {
CheckedNode cn = nodesCheckingState.get(tp);
cn.isSelected = check;
if (check) {
checkedPaths.add(tp);
} else {
checkedPaths.remove(tp);
}
updatePredecessorsAllChildrenSelectedState(tp);
}
private void updatePredecessorsAllChildrenSelectedState(TreePath tp) {
TreePath parentPath = tp.getParentPath();
if (parentPath == null) {
return;
}
CheckedNode parentCheckedNode = nodesCheckingState.get(parentPath);
parentCheckedNode.allChildrenSelected = true;
DefaultMutableTreeNode parentNode =
(DefaultMutableTreeNode) parentPath.getLastPathComponent();
for (int i = 0; i < parentNode.getChildCount(); i++) {
TreePath childPath = parentPath.pathByAddingChild(parentNode.getChildAt(i));
CheckedNode childCheckedNode = nodesCheckingState.get(childPath);
if (!allSelected(childCheckedNode)) {
parentCheckedNode.allChildrenSelected = false;
break;
}
}
updatePredecessorsAllChildrenSelectedState(parentPath);
}
public void setCheckBoxEnabled(TreePath tp, boolean enabled) {
nodesCheckingState.get(tp).isCheckBoxEnabled = enabled;
JCheckBoxTree.this.repaint();
}
public static void main(String[] params) {
// Simple test code
JFrame f = new JFrame();
f.setSize(new Dimension(500, 500));
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.setSize(new Dimension(500, 500));
f.getContentPane().add(p);
JCheckBoxTree cbt = new JCheckBoxTree();
cbt.setShowsRootHandles(true);
JScrollPane scroll = new JScrollPane();
scroll.setViewportView(cbt);
p.add(scroll, BorderLayout.CENTER);
DefaultMutableTreeNode root = new DefaultMutableTreeNode("Tech");
DefaultMutableTreeNode db = new DefaultMutableTreeNode("Db");
root.add(db);
db.add(new DefaultMutableTreeNode("HypersonicSQL"));
db.add(new DefaultMutableTreeNode("MsSQL"));
db.add(new DefaultMutableTreeNode("MySQL"));
db.add(new DefaultMutableTreeNode("Oracle"));
db.add(new DefaultMutableTreeNode("PostgreSQL"));
DefaultMutableTreeNode os = new DefaultMutableTreeNode("OS");
root.add(os);
os.add(new DefaultMutableTreeNode("Linux"));
DefaultMutableTreeNode ws = new DefaultMutableTreeNode("WS");
root.add(ws);
DefaultTreeModel model = new DefaultTreeModel(root);
cbt.setModel(model);
f.setVisible(true);
}
}