From 6212a58d6a16710af1e31e3008b6ff301182c2ad Mon Sep 17 00:00:00 2001 From: Chelsea Salyards Date: Thu, 10 Jan 2019 07:05:22 -0500 Subject: [PATCH] Added LegacyCheckBoxTree component that uses LegacyTristateCheckBox instead of TristateCheckBox. --- .../jidesoft/swing/LegacyCheckBoxTree.java | 560 +++++++++++++ .../swing/LegacyCheckBoxTreeCellRenderer.java | 163 ++++ .../LegacyCheckBoxTreeSelectionModel.java | 741 ++++++++++++++++++ 3 files changed, 1464 insertions(+) create mode 100644 src/com/jidesoft/swing/LegacyCheckBoxTree.java create mode 100644 src/com/jidesoft/swing/LegacyCheckBoxTreeCellRenderer.java create mode 100644 src/com/jidesoft/swing/LegacyCheckBoxTreeSelectionModel.java diff --git a/src/com/jidesoft/swing/LegacyCheckBoxTree.java b/src/com/jidesoft/swing/LegacyCheckBoxTree.java new file mode 100644 index 000000000..216a5aecc --- /dev/null +++ b/src/com/jidesoft/swing/LegacyCheckBoxTree.java @@ -0,0 +1,560 @@ +/* + * @(#)CheckBoxTree.java 8/11/2005 + * + * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved. + */ +package com.jidesoft.swing; + +import javax.swing.*; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.text.Position; +import javax.swing.tree.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Hashtable; +import java.util.Vector; +import java.util.List; +import java.util.ArrayList; + +/** + * This class is deprecated and provided to support compatibility with {@link LegacyTristateCheckBox}. We will not provide support + * for this class. You likely want to use {@link CheckBoxTree}. + *

+ * CheckBoxTree is a special JTree which uses JCheckBox as the tree renderer. In addition to regular JTree's features, + * it also allows you select any number of tree nodes in the tree by selecting the check boxes.

To select an element, + * user can mouse click on the check box, or select one or several tree nodes and press SPACE key to toggle the check + * box selection for all selected tree nodes. + *

+ * In order to retrieve which tree paths are selected, you need to call {@link #getCheckBoxTreeSelectionModel()}. It + * will return the selection model that keeps track of which tree paths have been checked. For example {@link + * CheckBoxTreeSelectionModel#getSelectionPaths()} will give the list of paths which have been checked. + * @deprecated Use {@link CheckBoxTree}. + */ +public class LegacyCheckBoxTree extends JTree { + + public static final String PROPERTY_CHECKBOX_ENABLED = "checkBoxEnabled"; + public static final String PROPERTY_CLICK_IN_CHECKBOX_ONLY = "clickInCheckBoxOnly"; + public static final String PROPERTY_DIG_IN = "digIn"; + + protected LegacyCheckBoxTreeCellRenderer _treeCellRenderer; + + private LegacyCheckBoxTreeSelectionModel _checkBoxTreeSelectionModel; + + private boolean _checkBoxEnabled = true; + private boolean _clickInCheckBoxOnly = true; + private PropertyChangeListener _modelChangeListener; + private LegacyTristateCheckBox _checkBox; + private boolean _selectPartialOnToggling = true; + + public LegacyCheckBoxTree() { + init(); + } + + public LegacyCheckBoxTree(Object[] value) { + super(value); + init(); + } + + public LegacyCheckBoxTree(Vector value) { + super(value); + init(); + } + + public LegacyCheckBoxTree(Hashtable value) { + super(value); + init(); + } + + public LegacyCheckBoxTree(TreeNode root) { + super(root); + init(); + } + + public LegacyCheckBoxTree(TreeNode root, boolean asksAllowsChildren) { + super(root, asksAllowsChildren); + init(); + } + + public LegacyCheckBoxTree(TreeModel newModel) { + super(newModel); + init(); + } + + /** + * Initialize the CheckBoxTree. + */ + protected void init() { + _checkBoxTreeSelectionModel = createCheckBoxTreeSelectionModel(getModel()); + _checkBoxTreeSelectionModel.setTree(this); + Handler handler = createHandler(); + JideSwingUtilities.insertMouseListener(this, handler, 0); + addKeyListener(handler); + _checkBoxTreeSelectionModel.addTreeSelectionListener(handler); + + if (_modelChangeListener == null) { + _modelChangeListener = new PropertyChangeListener() { + public void propertyChange(PropertyChangeEvent evt) { + if (JTree.SELECTION_MODEL_PROPERTY.equals(evt.getPropertyName())) { + updateRowMapper(); + } + if ("model".equals(evt.getPropertyName()) && evt.getNewValue() instanceof TreeModel) { + _checkBoxTreeSelectionModel.setModel((TreeModel) evt.getNewValue()); + } + } + }; + } + addPropertyChangeListener(JTree.SELECTION_MODEL_PROPERTY, _modelChangeListener); + addPropertyChangeListener("model", _modelChangeListener); + updateRowMapper(); + } + + /** + * Creates the CheckBoxTreeSelectionModel. + * + * @param model the tree model. + * @return the CheckBoxTreeSelectionModel. + */ + protected LegacyCheckBoxTreeSelectionModel createCheckBoxTreeSelectionModel(TreeModel model) { + return new LegacyCheckBoxTreeSelectionModel(model); + } + + /** + * RowMapper is necessary for contiguous selection. + */ + private void updateRowMapper() { + _checkBoxTreeSelectionModel.setRowMapper(getSelectionModel().getRowMapper()); + } + + private TreeCellRenderer _defaultRenderer; + + /** + * Gets the cell renderer with check box. + * + * @return CheckBoxTree's own cell renderer which has the check box. The actual cell renderer you set by + * setCellRenderer() can be accessed by using {@link #getActualCellRenderer()}. + */ + @Override + public TreeCellRenderer getCellRenderer() { + TreeCellRenderer cellRenderer = getActualCellRenderer(); + if (cellRenderer == null) { + cellRenderer = getDefaultRenderer(); + } + + if (_treeCellRenderer == null) { + _treeCellRenderer = createCellRenderer(cellRenderer); + } + else { + _treeCellRenderer.setActualTreeRenderer(cellRenderer); + } + return _treeCellRenderer; + } + + private TreeCellRenderer getDefaultRenderer() { + if (_defaultRenderer == null) + _defaultRenderer = new DefaultTreeCellRenderer(); + return _defaultRenderer; + } + + /** + * Gets the actual cell renderer. Since CheckBoxTree has its own check box cell renderer, this method will give you + * access to the actual cell renderer which is either the default tree cell renderer or the cell renderer you set + * using {@link #setCellRenderer(javax.swing.tree.TreeCellRenderer)}. + * + * @return the actual cell renderer + */ + public TreeCellRenderer getActualCellRenderer() { + if (_treeCellRenderer != null) { + return _treeCellRenderer.getActualTreeRenderer(); + } + else { + return super.getCellRenderer(); + } + } + + @Override + public void setCellRenderer(TreeCellRenderer x) { + if (x == null) { + x = getDefaultRenderer(); + } + super.setCellRenderer(x); + if (_treeCellRenderer != null) { + _treeCellRenderer.setActualTreeRenderer(x); + } + } + + + /** + * Creates the cell renderer. + * + * @param renderer the actual renderer for the tree node. This method will return a cell renderer that use a check + * box and put the actual renderer inside it. + * @return the cell renderer. + */ + protected LegacyCheckBoxTreeCellRenderer createCellRenderer(TreeCellRenderer renderer) { + final LegacyCheckBoxTreeCellRenderer checkBoxTreeCellRenderer = new LegacyCheckBoxTreeCellRenderer(renderer, getCheckBox()); + addPropertyChangeListener(CELL_RENDERER_PROPERTY, new PropertyChangeListener() { + public void propertyChange(PropertyChangeEvent evt) { + TreeCellRenderer treeCellRenderer = (TreeCellRenderer) evt.getNewValue(); + if (treeCellRenderer != checkBoxTreeCellRenderer) { + checkBoxTreeCellRenderer.setActualTreeRenderer(treeCellRenderer); + } + else { + checkBoxTreeCellRenderer.setActualTreeRenderer(null); + } + } + }); + return checkBoxTreeCellRenderer; + } + + /** + * Creates the mouse listener and key listener used by CheckBoxTree. + * + * @return the Handler. + */ + protected Handler createHandler() { + return new Handler(this); + } + + /** + * Get the CheckBox used for CheckBoxTreeCellRenderer. + * + * @see #setCheckBox(LegacyTristateCheckBox) + * @return the check box. + */ + public LegacyTristateCheckBox getCheckBox() { + return _checkBox; + } + + /** + * Set the CheckBox used for CheckBoxTreeCellRenderer. + *

+ * By default, it's null. CheckBoxTreeCellRenderer then will create a default TristateCheckBox. + * + * @param checkBox the check box + */ + public void setCheckBox(LegacyTristateCheckBox checkBox) { + if (_checkBox != checkBox) { + _checkBox = checkBox; + _treeCellRenderer = null; + revalidate(); + repaint(); + } + } + + /** + * Gets the flag indicating if toggling should select or deselect the partially selected node. + * + * @return true if select first. Otherwise false. + * @see #setSelectPartialOnToggling(boolean) + */ + public boolean isSelectPartialOnToggling() { + return _selectPartialOnToggling; + } + + /** + * Sets the flag indicating if toggling should select or deselect the partially selected node. + *

+ * By default, the value is true to keep original behavior. + * + * @param selectPartialOnToggling the flag + */ + public void setSelectPartialOnToggling(boolean selectPartialOnToggling) { + _selectPartialOnToggling = selectPartialOnToggling; + } + + protected static class Handler implements MouseListener, KeyListener, TreeSelectionListener { + protected LegacyCheckBoxTree _tree; + int _hotspot = new JCheckBox().getPreferredSize().width; + private int _toggleCount = -1; + + public Handler(LegacyCheckBoxTree tree) { + _tree = tree; + } + + protected TreePath getTreePathForMouseEvent(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) { + return null; + } + + if (!_tree.isCheckBoxEnabled()) { + return null; + } + + TreePath path = _tree.getPathForLocation(e.getX(), e.getY()); + if (path == null) + return null; + + if (clicksInCheckBox(e, path) || !_tree.isClickInCheckBoxOnly()) { + return path; + } + else { + return null; + } + } + + protected boolean clicksInCheckBox(MouseEvent e, TreePath path) { + if (!_tree.isCheckBoxVisible(path)) { + return false; + } + else { + Rectangle bounds = _tree.getPathBounds(path); + if (_tree.getComponentOrientation().isLeftToRight()) { + return e.getX() < bounds.x + _hotspot; + } + else { + return e.getX() > bounds.x + bounds.width - _hotspot; + } + } + } + + private TreePath preventToggleEvent(MouseEvent e) { + TreePath pathForMouseEvent = getTreePathForMouseEvent(e); + if (pathForMouseEvent != null) { + int toggleCount = _tree.getToggleClickCount(); + if (toggleCount != -1) { + _toggleCount = toggleCount; + _tree.setToggleClickCount(-1); + } + } + return pathForMouseEvent; + } + + public void mouseClicked(MouseEvent e) { + if (e.isConsumed()) { + return; + } + + preventToggleEvent(e); + } + + public void mousePressed(MouseEvent e) { + if (e.isConsumed()) { + return; + } + + TreePath path = preventToggleEvent(e); + if (path != null) { + toggleSelections(new TreePath[] {path}); + e.consume(); + } + } + + public void mouseReleased(MouseEvent e) { + if (e.isConsumed()) { + return; + } + + TreePath path = preventToggleEvent(e); + if (path != null) { + e.consume(); + } + if (_toggleCount != -1) { + _tree.setToggleClickCount(_toggleCount); + } + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void keyPressed(KeyEvent e) { + if (e.isConsumed()) { + return; + } + + if (!_tree.isCheckBoxEnabled()) { + return; + } + + if (e.getModifiers() == 0 && e.getKeyChar() == KeyEvent.VK_SPACE) + toggleSelections(); + } + + public void keyTyped(KeyEvent e) { + } + + public void keyReleased(KeyEvent e) { + } + + public void valueChanged(TreeSelectionEvent e) { + _tree.treeDidChange(); + } + + protected void toggleSelections() { + TreePath[] treePaths = _tree.getSelectionPaths(); + toggleSelections(treePaths); + } + + private void toggleSelections(TreePath[] treePaths) { + if (treePaths == null || treePaths.length == 0 || !_tree.isEnabled()) { + return; + } + if (treePaths.length == 1 && !_tree.isCheckBoxEnabled(treePaths[0])) { + return; + } + LegacyCheckBoxTreeSelectionModel selectionModel = _tree.getCheckBoxTreeSelectionModel(); + List pathToAdded = new ArrayList(); + List pathToRemoved = new ArrayList(); + for (TreePath treePath : treePaths) { + boolean selected = selectionModel.isPathSelected(treePath, selectionModel.isDigIn()); + if (selected) { + pathToRemoved.add(treePath); + } + else { + if (!_tree.isSelectPartialOnToggling() && selectionModel.isPartiallySelected(treePath)) { + TreePath[] selectionPaths = selectionModel.getSelectionPaths(); + if (selectionPaths != null) { + for (TreePath selectionPath : selectionPaths) { + if (selectionModel.isDescendant(selectionPath, treePath)) { + pathToRemoved.add(selectionPath); + } + } + } + } + else { + pathToAdded.add(treePath); + } + } + } + selectionModel.removeTreeSelectionListener(this); + try { + if (pathToAdded.size() > 0) { + selectionModel.addSelectionPaths(pathToAdded.toArray(new TreePath[pathToAdded.size()])); + } + if (pathToRemoved.size() > 0) { + selectionModel.removeSelectionPaths(pathToRemoved.toArray(new TreePath[pathToRemoved.size()])); + } + } + finally { + selectionModel.addTreeSelectionListener(this); + _tree.treeDidChange(); + } + } + } + + @Override + public TreePath getNextMatch(String prefix, int startingRow, Position.Bias bias) { + return null; + } + + /** + * Gets the selection model for the check boxes. To retrieve the state of check boxes, you should use this selection + * model. + * + * @return the selection model for the check boxes. + */ + public LegacyCheckBoxTreeSelectionModel getCheckBoxTreeSelectionModel() { + return _checkBoxTreeSelectionModel; + } + + /** + * Gets the value of property checkBoxEnabled. If true, user can click on check boxes on each tree node to select + * and deselect. If false, user can't click but you as developer can programmatically call API to select/deselect + * it. + * + * @return the value of property checkBoxEnabled. + */ + public boolean isCheckBoxEnabled() { + return _checkBoxEnabled; + } + + /** + * Sets the value of property checkBoxEnabled. + * + * @param checkBoxEnabled true to allow to check the check box. False to disable it which means user can see whether + * a row is checked or not but they cannot change it. + */ + public void setCheckBoxEnabled(boolean checkBoxEnabled) { + if (checkBoxEnabled != _checkBoxEnabled) { + Boolean oldValue = _checkBoxEnabled ? Boolean.TRUE : Boolean.FALSE; + Boolean newValue = checkBoxEnabled ? Boolean.TRUE : Boolean.FALSE; + _checkBoxEnabled = checkBoxEnabled; + firePropertyChange(PROPERTY_CHECKBOX_ENABLED, oldValue, newValue); + repaint(); + } + } + + /** + * Checks if check box is enabled. There is no setter for it. The only way is to override this method to return true + * or false. + *

+ * However, in digIn mode, user can still select the disabled node by selecting all children nodes of that node. + * Also if user selects the parent node, the disabled children nodes will be selected too. + * + * @param path the tree path. + * @return true or false. If false, the check box on the particular tree path will be disabled. + */ + public boolean isCheckBoxEnabled(TreePath path) { + return true; + } + + /** + * Checks if check box is visible. There is no setter for it. The only way is to override this method to return true + * or false. + * + * @param path the tree path. + * @return true or false. If false, the check box on the particular tree path will be disabled. + */ + public boolean isCheckBoxVisible(TreePath path) { + return true; + } + + /** + * Gets the dig-in mode. If the CheckBoxTree is in dig-in mode, checking the parent node will check all the + * children. Correspondingly, getSelectionPaths() will only return the parent tree path. If not in dig-in mode, each + * tree node can be checked or unchecked independently + * + * @return true or false. + */ + public boolean isDigIn() { + return getCheckBoxTreeSelectionModel().isDigIn(); + } + + /** + * Sets the dig-in mode. If the CheckBoxTree is in dig-in mode, checking the parent node will check all the + * children. Correspondingly, getSelectionPaths() will only return the parent tree path. If not in dig-in mode, each + * tree node can be checked or unchecked independently + * + * @param digIn the new digIn mode. + */ + public void setDigIn(boolean digIn) { + boolean old = isDigIn(); + if (old != digIn) { + getCheckBoxTreeSelectionModel().setDigIn(digIn); + firePropertyChange(PROPERTY_DIG_IN, old, digIn); + } + } + + /** + * Gets the value of property clickInCheckBoxOnly. If true, user can click on check boxes on each tree node to + * select and deselect. If false, user can't click but you as developer can programmatically call API to + * select/deselect it. + * + * @return the value of property clickInCheckBoxOnly. + */ + public boolean isClickInCheckBoxOnly() { + return _clickInCheckBoxOnly; + } + + /** + * Sets the value of property clickInCheckBoxOnly. + * + * @param clickInCheckBoxOnly true to allow to check the check box. False to disable it which means user can see + * whether a row is checked or not but they cannot change it. + */ + public void setClickInCheckBoxOnly(boolean clickInCheckBoxOnly) { + if (clickInCheckBoxOnly != _clickInCheckBoxOnly) { + boolean old = _clickInCheckBoxOnly; + _clickInCheckBoxOnly = clickInCheckBoxOnly; + firePropertyChange(PROPERTY_CLICK_IN_CHECKBOX_ONLY, old, _clickInCheckBoxOnly); + } + } +} diff --git a/src/com/jidesoft/swing/LegacyCheckBoxTreeCellRenderer.java b/src/com/jidesoft/swing/LegacyCheckBoxTreeCellRenderer.java new file mode 100644 index 000000000..bba326e61 --- /dev/null +++ b/src/com/jidesoft/swing/LegacyCheckBoxTreeCellRenderer.java @@ -0,0 +1,163 @@ +/* + * @(#)CheckBoxTreeCellRenderer.java 8/11/2005 + * + * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved. + */ + +package com.jidesoft.swing; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.io.Serializable; + +/** + * This class is deprecated and provided to support compatibility with {@link LegacyTristateCheckBox}. We will not provide support + * for this class. You likely want to use {@link CheckBoxTreeCellRenderer}. + *

+ * Renderers an item in a tree using JCheckBox. + * @deprecated Use {@link CheckBoxTreeCellRenderer}. + */ +public class LegacyCheckBoxTreeCellRenderer extends JPanel implements TreeCellRenderer, Serializable { + private static final long serialVersionUID = 30207434500313004L; + + /** + * The checkbox that is used to paint the check box in cell renderer + */ + protected LegacyTristateCheckBox _checkBox = null; + protected JComponent _emptyBox = null; + protected JCheckBox _protoType; + + /** + * The label which appears after the check box. + */ + protected TreeCellRenderer _actualTreeRenderer; + + /** + * Constructs a default renderer object for an item in a list. + */ + public LegacyCheckBoxTreeCellRenderer() { + this(null); + } + + public LegacyCheckBoxTreeCellRenderer(TreeCellRenderer renderer) { + this(renderer, null); + } + + public LegacyCheckBoxTreeCellRenderer(TreeCellRenderer renderer, LegacyTristateCheckBox checkBox) { + _protoType = new LegacyTristateCheckBox(); + if (checkBox == null) { + _checkBox = createCheckBox(); + } + else { + _checkBox = checkBox; + } + _emptyBox = (JComponent) Box.createHorizontalStrut(_protoType.getPreferredSize().width); + setLayout(new BorderLayout(0, 0)); + setOpaque(false); + _actualTreeRenderer = renderer; + } + + /** + * Create the check box in the cell. + *

+ * By default, it creates a LegacyTristateCheckBox and set opaque to false. + * + * @return the check box instance. + */ + protected LegacyTristateCheckBox createCheckBox() { + LegacyTristateCheckBox checkBox = new LegacyTristateCheckBox(); + checkBox.setOpaque(false); + return checkBox; + } + + public TreeCellRenderer getActualTreeRenderer() { + return _actualTreeRenderer; + } + + public void setActualTreeRenderer(TreeCellRenderer actualTreeRenderer) { + _actualTreeRenderer = actualTreeRenderer; + } + + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { + removeAll(); + _checkBox.setPreferredSize(new Dimension(_protoType.getPreferredSize().width, 0)); + _emptyBox.setPreferredSize(new Dimension(_protoType.getPreferredSize().width, 0)); + applyComponentOrientation(tree.getComponentOrientation()); + + TreePath path = tree.getPathForRow(row); + if (path != null && tree instanceof LegacyCheckBoxTree) { + LegacyCheckBoxTreeSelectionModel selectionModel = ((LegacyCheckBoxTree) tree).getCheckBoxTreeSelectionModel(); + if (selectionModel != null) { + boolean enabled = tree.isEnabled() && ((LegacyCheckBoxTree) tree).isCheckBoxEnabled() && ((LegacyCheckBoxTree) tree).isCheckBoxEnabled(path); + if (!enabled && !selected) { + if (getBackground() != null) { + setForeground(getBackground().darker()); + } + } + _checkBox.setEnabled(enabled); + updateCheckBoxState(_checkBox, path, selectionModel); + } + } + + if (_actualTreeRenderer != null) { + JComponent treeCellRendererComponent = (JComponent) _actualTreeRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + Border border = treeCellRendererComponent.getBorder(); + setBorder(border); + treeCellRendererComponent.setBorder(BorderFactory.createEmptyBorder()); + if (path == null || !(tree instanceof LegacyCheckBoxTree) || ((LegacyCheckBoxTree) tree).isCheckBoxVisible(path)) { + remove(_emptyBox); + add(_checkBox, BorderLayout.BEFORE_LINE_BEGINS); + } + else { + remove(_checkBox); + add(_emptyBox, BorderLayout.AFTER_LINE_ENDS); // expand the tree node size to be the same as the one with check box. + } + add(treeCellRendererComponent); + } + + return this; + } + + /** + * Updates the check box state based on the selection in the selection model. By default, we check if the path is + * selected. If yes, we mark the check box as TristateCheckBox.SELECTED. If not, we will check if the path is + * partially selected, if yes, we set the check box as null or TristateCheckBox.DONT_CARE to indicate the path is + * partially selected. Otherwise, we set it to TristateCheckBox.NOT_SELECTED. + * + * @param checkBox the TristateCheckBox for the particular tree path. + * @param path the tree path. + * @param selectionModel the CheckBoxTreeSelectionModel. + */ + protected void updateCheckBoxState(LegacyTristateCheckBox checkBox, TreePath path, LegacyCheckBoxTreeSelectionModel selectionModel) { + if (selectionModel.isPathSelected(path, selectionModel.isDigIn())) + checkBox.setState(LegacyTristateCheckBox.SELECTED); + else + checkBox.setState(selectionModel.isDigIn() && selectionModel.isPartiallySelected(path) ? null : LegacyTristateCheckBox.NOT_SELECTED); + } + + @Override + public String getToolTipText(MouseEvent event) { + if (_actualTreeRenderer instanceof JComponent) { + Point p = event.getPoint(); + p.translate(-_checkBox.getWidth(), 0); + MouseEvent newEvent = new MouseEvent(((JComponent) _actualTreeRenderer), event.getID(), + event.getWhen(), + event.getModifiers(), + p.x, p.y, event.getClickCount(), + event.isPopupTrigger()); + + String tip = ((JComponent) _actualTreeRenderer).getToolTipText( + newEvent); + + if (tip != null) { + return tip; + } + } + return super.getToolTipText(event); + } + +} diff --git a/src/com/jidesoft/swing/LegacyCheckBoxTreeSelectionModel.java b/src/com/jidesoft/swing/LegacyCheckBoxTreeSelectionModel.java new file mode 100644 index 000000000..023de69fe --- /dev/null +++ b/src/com/jidesoft/swing/LegacyCheckBoxTreeSelectionModel.java @@ -0,0 +1,741 @@ +package com.jidesoft.swing; + +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.util.*; + +/** + * This class is deprecated and provided to support compatibility with {@link LegacyTristateCheckBox}. We will not provide support + * for this class. You likely want to use {@link CheckBoxTreeSelectionModel}. + *

+ * CheckBoxTreeSelectionModel is a selection _model based on {@link DefaultTreeSelectionModel} and use in + * {@link CheckBoxTree} to keep track of the checked tree paths. + * + * @author Santhosh Kumar T + * @deprecated Use {@link CheckBoxTreeSelectionModel}. + */ +public class LegacyCheckBoxTreeSelectionModel extends DefaultTreeSelectionModel implements TreeModelListener { + private TreeModel _model; + private boolean _digIn = true; + private LegacyCheckBoxTree _tree; + /** + * Used in {@link #areSiblingsSelected(javax.swing.tree.TreePath)} for those paths pending added so that they are not + * in the selection model right now. + */ + protected Set _pathHasAdded; + + private boolean _singleEventMode = false; + private static final long serialVersionUID = 1368502059666946634L; + + public LegacyCheckBoxTreeSelectionModel(TreeModel model) { + setModel(model); + setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); + } + + void setTree(LegacyCheckBoxTree tree) { + _tree = tree; + } + + public LegacyCheckBoxTreeSelectionModel(TreeModel model, boolean digIn) { + setModel(model); + _digIn = digIn; + } + + public TreeModel getModel() { + return _model; + } + + public void setModel(TreeModel model) { + if (_model != model) { + if (_model != null) { + _model.removeTreeModelListener(this); + } + _model = model; + if (_model != null) { + _model.addTreeModelListener(this); + } + } + } + + /** + * Gets the dig-in mode. If the CheckBoxTree is in dig-in mode, checking the parent node will check all the + * children. Correspondingly, getSelectionPaths() will only return the parent tree path. If not in dig-in mode, each + * tree node can be checked or unchecked independently + * + * @return true or false. + */ + public boolean isDigIn() { + return _digIn; + } + + /** + * Sets the dig-in mode. If the CheckBoxTree is in dig-in mode, checking the parent node will check all the + * children. Correspondingly, getSelectionPaths() will only return the parent tree path. If not in dig-in mode, each + * tree node can be checked or unchecked independently + * + * @param digIn true to enable dig-in mode. False to disable it. + */ + public void setDigIn(boolean digIn) { + _digIn = digIn; + } + + /** + * Tests whether there is any unselected node in the subtree of given path. + *

+ * Inherited from JTree, the TreePath must be a path instance inside the tree model. If you populate a new TreePath + * instance on the fly, it would not work. + * + * @param path check if the path is partially selected. + * @return true if partially. Otherwise false. + */ + public boolean isPartiallySelected(TreePath path) { + if (!isDigIn()) { + return isPathSelected(path, false); + } + if (isPathSelected(path, true)) + return false; + TreePath[] selectionPaths = getSelectionPaths(); + if (selectionPaths == null) + return false; + for (TreePath selectionPath : selectionPaths) { + if (isDescendant(selectionPath, path)) + return true; + } + return false; + } + + @Override + public boolean isRowSelected(int row) { + return isPathSelected(_tree.getPathForRow(row), _tree.isDigIn()); + } + + /** + * Check if the parent path is really selected. + *

+ * The default implementation is just return true. In filterable scenario, you could override this method to check + * more. + *

+ * Inherited from JTree, the TreePath must be a path instance inside the tree model. If you populate a new TreePath + * instance on the fly, it would not work. + * + * @param path the original path to be checked + * @param parent the parent part which is closest to the original path and is selected + * @return true if the path is actually selected without any doubt. Otherwise false. + */ + protected boolean isParentActuallySelected(TreePath path, TreePath parent) { + return true; + } + + /** + * Tells whether given path is selected. if dig is true, then a path is assumed to be selected, if one of its + * ancestor is selected. + *

+ * Inherited from JTree, the TreePath must be a path instance inside the tree model. If you populate a new TreePath + * instance on the fly, it would not work. + * + * @param path check if the path is selected. + * @param digIn whether we will check its descendants. + * @return true if the path is selected. + */ + public boolean isPathSelected(TreePath path, boolean digIn) { + if (path == null) { + return false; + } + + if (!digIn) + return super.isPathSelected(path); + + TreePath parent = path; + while (parent != null && !super.isPathSelected(parent)) { + parent = parent.getParentPath(); + } + + if (parent != null) { + return isParentActuallySelected(path, parent); + } + + if (_model == null) { + return true; + } + + Object node = path.getLastPathComponent(); + if (getChildrenCount(node) == 0) { + return false; + } + + // find out if all children are selected + boolean allChildrenSelected = true; + for (int i = 0; i < getChildrenCount(node); i++) { + Object childNode = getChild(node, i); + if (!isPathSelected(path.pathByAddingChild(childNode), true)) { + allChildrenSelected = false; + break; + } + } + // if all children are selected, let's select the parent path only + if (_tree.isCheckBoxVisible(path) && allChildrenSelected) { + addSelectionPaths(new TreePath[]{path}, false); + } + return allChildrenSelected; + } + + /** + * is path1 descendant of path2. + *

+ * Inherited from JTree, the TreePath must be a path instance inside the tree model. If you populate a new TreePath + * instance on the fly, it would not work. + * + * @param path1 the first path + * @param path2 the second path + * @return true if the first path is the descendant of the second path. + */ + boolean isDescendant(TreePath path1, TreePath path2) { + Object obj1[] = path1.getPath(); + Object obj2[] = path2.getPath(); + if (obj1.length < obj2.length) + return false; + for (int i = 0; i < obj2.length; i++) { + if (obj1[i] != obj2[i]) + return false; + } + return true; + } + + private boolean _fireEvent = true; + + @SuppressWarnings({"RawUseOfParameterizedType"}) + @Override + protected void notifyPathChange(Vector changedPaths, TreePath oldLeadSelection) { + if (_fireEvent) { + super.notifyPathChange(changedPaths, oldLeadSelection); + } + } + + /** + * Overrides the method in DefaultTreeSelectionModel to consider digIn mode. + *

+ * Inherited from JTree, the TreePath must be a path instance inside the tree model. If you populate a new TreePath + * instance on the fly, it would not work. + * + * @param pPaths the tree paths to be selected. + */ + @Override + public void setSelectionPaths(TreePath[] pPaths) { + if (!isDigIn() || selectionMode == TreeSelectionModel.SINGLE_TREE_SELECTION) { + super.setSelectionPaths(pPaths); + } + else { + clearSelection(); + addSelectionPaths(pPaths); + } + } + + /** + * Overrides the method in DefaultTreeSelectionModel to consider digIn mode. + *

+ * Inherited from JTree, the TreePath must be a path instance inside the tree model. If you populate a new TreePath + * instance on the fly, it would not work. + * + * @param paths the tree paths to be added to selection paths. + */ + @Override + public void addSelectionPaths(TreePath[] paths) { + addSelectionPaths(paths, true); + } + + /** + * Add the selection paths. + * + * @param paths the paths to be added + * @param needCheckPathSelection the flag to indicating if the path selection should be checked to improve performance + */ + protected void addSelectionPaths(TreePath[] paths, boolean needCheckPathSelection) { + if (!isDigIn()) { + super.addSelectionPaths(paths); + return; + } + + setBatchMode(true); + boolean fireEventAtTheEnd = false; + if (isSingleEventMode() && _fireEvent) { + _fireEvent = false; + fireEventAtTheEnd = true; + } + + try { + if (needCheckPathSelection) { + _pathHasAdded = new HashSet(); + for (TreePath path : paths) { + if (isPathSelected(path, isDigIn())) { + continue; // for non batch mode scenario, check if it is already selected by adding its parent possibly + } + // if the path itself is added by other insertion, just remove it + if (_toBeAdded.contains(path)) { + addToExistingSet(_pathHasAdded, path); + continue; + } + // check if its ancestor has already been added. If so, do nothing + boolean findAncestor = false; + for (TreePath addPath : _pathHasAdded) { + if (addPath.isDescendant(path)) { + findAncestor = true; + break; + } + } + if (findAncestor) { + continue; + } + TreePath temp = null; + // if all siblings are selected then deselect them and select parent recursively + // otherwise just select that path. + while (areSiblingsSelected(path)) { + temp = path; + if (path.getParentPath() == null) + break; + path = path.getParentPath(); + } + if (temp != null) { + if (temp.getParentPath() != null) { + delegateAddSelectionPaths(new TreePath[] {temp.getParentPath()}); + } + else { + delegateAddSelectionPaths(new TreePath[]{temp}); + } + } + else { + delegateAddSelectionPaths(new TreePath[]{path}); + } + addToExistingSet(_pathHasAdded, path); + } + // deselect all descendants of paths[] + List toBeRemoved = new ArrayList(); + for (TreePath path : _toBeAdded) { + TreePath[] selectionPaths = getSelectionPaths(); + if (selectionPaths == null) + break; + for (TreePath selectionPath : selectionPaths) { + if (isDescendant(selectionPath, path)) + toBeRemoved.add(selectionPath); + } + } + if (toBeRemoved.size() > 0) { + delegateRemoveSelectionPaths(toBeRemoved.toArray(new TreePath[toBeRemoved.size()])); + } + } + else { + // deselect all descendants of paths[] + List toBeRemoved = new ArrayList(); + for (TreePath path : paths) { + TreePath[] selectionPaths = getSelectionPaths(); + if (selectionPaths == null) + break; + for (TreePath selectionPath : selectionPaths) { + if (isDescendant(selectionPath, path)) + toBeRemoved.add(selectionPath); + } + } + if (toBeRemoved.size() > 0) { + delegateRemoveSelectionPaths(toBeRemoved.toArray(new TreePath[toBeRemoved.size()])); + } + + // if all siblings are selected then deselect them and select parent recursively + // otherwise just select that path. + for (TreePath path : paths) { + TreePath temp = null; + while (areSiblingsSelected(path)) { + temp = path; + if (path.getParentPath() == null) + break; + path = path.getParentPath(); + } + if (temp != null) { + if (temp.getParentPath() != null) { + addSelectionPath(temp.getParentPath()); + } + else { + if (!isSelectionEmpty()) { + removeSelectionPaths(getSelectionPaths(), !fireEventAtTheEnd); + } + delegateAddSelectionPaths(new TreePath[]{temp}); + } + } + else { + delegateAddSelectionPaths(new TreePath[]{path}); + } + } + } + } + finally { + _fireEvent = true; + setBatchMode(false); + if (isSingleEventMode() && fireEventAtTheEnd) { + notifyPathChange(paths, true, paths[0]); + } + } + } + + /** + * tells whether all siblings of given path are selected. + *

+ * Inherited from JTree, the TreePath must be a path instance inside the tree model. If you populate a new TreePath + * instance on the fly, it would not work. + * + * @param path the tree path + * @return true if the siblings are all selected. + */ + protected boolean areSiblingsSelected(TreePath path) { + TreePath parent = path.getParentPath(); + if (parent == null) + return true; + Object node = path.getLastPathComponent(); + Object parentNode = parent.getLastPathComponent(); + + int childCount = getChildrenCount(parentNode); + for (int i = 0; i < childCount; i++) { + Object childNode = getChild(parentNode, i); + if (childNode == node) + continue; + TreePath childPath = parent.pathByAddingChild(childNode); + if (_tree != null && !_tree.isCheckBoxVisible(childPath)) { + // if the checkbox is not visible, we check its children + if (!isPathSelected(childPath, true) && (_pathHasAdded == null || !_pathHasAdded.contains(childPath))) { + return false; + } + } + if (!isPathSelected(childPath) && (_pathHasAdded == null || !_pathHasAdded.contains(childPath))) { + return false; + } + } + return true; + } + + @Override + public void removeSelectionPaths(TreePath[] paths) { + removeSelectionPaths(paths, true); + } + + public void removeSelectionPaths(TreePath[] paths, boolean doFireEvent) { + if (!isDigIn()) { + super.removeSelectionPaths(paths); + return; + } + + boolean fireEventAtTheEnd = false; + if (doFireEvent) { + if (isSingleEventMode() && _fireEvent) { + _fireEvent = false; + fireEventAtTheEnd = true; + } + } + setBatchMode(true); + try { + Set pathHasRemoved = new HashSet(); + for (TreePath path : paths) { + if (!isPathSelected(path, isDigIn())) { + continue; // for non batch mode scenario, check if it is already deselected by removing its parent possibly + } + TreePath upperMostSelectedAncestor = null; + if (_toBeAdded.contains(path)) { + _toBeAdded.remove(path); + addToExistingSet(pathHasRemoved, path); + continue; + } + // check if its ancestor has already been removed. If so, do nothing + boolean findAncestor = false; + for (TreePath removedPath : pathHasRemoved) { + if (removedPath.isDescendant(path)) { + findAncestor = true; + break; + } + } + if (findAncestor) { + continue; + } + // remove all children path added by other removal + Set pathToRemoved = new HashSet(); + for (TreePath pathToAdded : _toBeAdded) { + if (path.isDescendant(pathToAdded)) { + pathToRemoved.add(pathToAdded); + } + } + _toBeAdded.removeAll(pathToRemoved); + // find a parent path added by other removal, then use that parent to do following actions + for (TreePath pathToAdded : _toBeAdded) { + if (pathToAdded.isDescendant(path)) { + upperMostSelectedAncestor = pathToAdded; + break; + } + } + TreePath parent = path.getParentPath(); + Stack stack = new Stack(); + while (parent != null && (upperMostSelectedAncestor == null ? !isPathSelected(parent) : parent != upperMostSelectedAncestor)) { + stack.push(parent); + parent = parent.getParentPath(); + } + if (parent != null) + stack.push(parent); + else { + delegateRemoveSelectionPaths(new TreePath[]{path}); + addToExistingSet(pathHasRemoved, path); + continue; + } + + List toBeAdded = new ArrayList(); + while (!stack.isEmpty()) { + TreePath temp = stack.pop(); + TreePath peekPath = stack.isEmpty() ? path : stack.peek(); + Object node = temp.getLastPathComponent(); + Object peekNode = peekPath.getLastPathComponent(); + int childCount = getChildrenCount(node); + for (int i = 0; i < childCount; i++) { + Object childNode = getChild(node, i); + if (childNode != peekNode) { + TreePath treePath = temp.pathByAddingChild(childNode); + toBeAdded.add(treePath); + } + } + } + if (toBeAdded.size() > 0) { + delegateAddSelectionPaths(toBeAdded.toArray(new TreePath[toBeAdded.size()])); + } + delegateRemoveSelectionPaths(new TreePath[]{parent}); + addToExistingSet(pathHasRemoved, path); + } + } + finally { + _fireEvent = true; + setBatchMode(false); + if (isSingleEventMode() && fireEventAtTheEnd) { + notifyPathChange(paths, false, paths[0]); + } + } + } + + /** + * Get the child of node in the designated index. + * + * @param node the parent node + * @param i the child index + * @return the child node + */ + protected Object getChild(Object node, int i) { + return _model.getChild(node, i); + } + + /** + * Get the children count + * + * @param node the parent node + * @return the children count of the parent node. + */ + protected int getChildrenCount(Object node) { + return _model.getChildCount(node); + } + + private void addToExistingSet(Set pathHasOperated, TreePath pathToOperate) { + if (pathHasOperated.contains(pathToOperate)) { + return; // it is already removed + } + for (TreePath path : pathHasOperated) { + if (path.isDescendant(pathToOperate)) { + return; // its parent is removed, no need to add it + } + } + // remove all children path exists in the set + Set duplicatePathToErase = new HashSet(); + for (TreePath path : pathHasOperated) { + if (pathToOperate.isDescendant(path)) { + duplicatePathToErase.add(path); + } + } + pathHasOperated.removeAll(duplicatePathToErase); + pathHasOperated.add(pathToOperate); + } + + public boolean isSingleEventMode() { + return _singleEventMode; + } + + /** + * Single event mode is a mode that always fires only one event when you select or deselect a tree node. + *

+ * Taking this tree as an example, + *

+ *

+     * A -- a
+     *   |- b
+     *   |- c
+     * 
+ * Case 1: Assuming b and c are selected at this point, you click on a.
+ *

+ * Case 2: Assuming none of the nodes are selected, you click on A. In this case, both modes result in the same + * behavior.

Case 3: Assuming b and c are selected and now you click on A. As you can see, single event mode will always fire the event on the nodes you + * select. However it doesn't reflect what really happened inside the selection model. So if you want to get a + * complete picture of the selection state inside selection model, you should use {@link #getSelectionPaths()} to + * find out. In non-single event mode, the events reflect what happened inside the selection model. So you can get a + * complete picture of the exact state without asking the selection model. The downside is it will generate too many + * events. With this option, you can decide which mode you want to use that is the best for your case. + *

+ * By default, singleEventMode is set to false to be compatible with the older versions that don't have this + * option. + * + * @param singleEventMode true or false. + */ + public void setSingleEventMode(boolean singleEventMode) { + _singleEventMode = singleEventMode; + } + + /** + * Notifies listeners of a change in path. changePaths should contain instances of PathPlaceHolder. + * + * @param changedPaths the paths that are changed. + * @param isNew is it a new path. + * @param oldLeadSelection the old selection. + */ + protected void notifyPathChange(TreePath[] changedPaths, boolean isNew, TreePath oldLeadSelection) { + if (_fireEvent) { + int cPathCount = changedPaths.length; + boolean[] newness = new boolean[cPathCount]; + + for (int counter = 0; counter < cPathCount; counter++) { + newness[counter] = isNew; + } + + TreeSelectionEvent event = new TreeSelectionEvent + (this, changedPaths, newness, oldLeadSelection, leadPath); + + fireValueChanged(event); + } + } + + // do not use it for now + private boolean _batchMode = false; + + boolean isBatchMode() { + return _batchMode; + } + + public void setBatchMode(boolean batchMode) { + _batchMode = batchMode; + if (!_batchMode) { + TreePath[] treePaths = _toBeAdded.toArray(new TreePath[_toBeAdded.size()]); + _toBeAdded.clear(); + super.addSelectionPaths(treePaths); + treePaths = _toBeRemoved.toArray(new TreePath[_toBeRemoved.size()]); + _toBeRemoved.clear(); + super.removeSelectionPaths(treePaths); + } + } + + private Set _toBeAdded = new HashSet(); + private Set _toBeRemoved = new HashSet(); + + private void delegateRemoveSelectionPaths(TreePath[] paths) { + if (!isBatchMode()) { + super.removeSelectionPaths(paths); + } + else { + for (TreePath path : paths) { + _toBeRemoved.add(path); + _toBeAdded.remove(path); + } + } + } + +// private void delegateRemoveSelectionPath(TreePath path) { +// if (!isBatchMode()) { +// super.removeSelectionPath(path); +// } +// else { +// _toBeRemoved.add(path); +// _toBeAdded.remove(path); +// } +// +// } +// + private void delegateAddSelectionPaths(TreePath[] paths) { + if (!isBatchMode()) { + super.addSelectionPaths(paths); + } + else { + for (TreePath path : paths) { + addToExistingSet(_toBeAdded, path); + _toBeRemoved.remove(path); + } + } + } + +// private void delegateAddSelectionPath(TreePath path) { +// if (!isBatchMode()) { +// super.addSelectionPath(path); +// } +// else { +// _toBeAdded.add(path); +// _toBeRemoved.remove(path); +// } +// } +// + public void treeNodesChanged(TreeModelEvent e) { + revalidateSelectedTreePaths(); + } + + public void treeNodesInserted(TreeModelEvent e) { + + } + + public void treeNodesRemoved(TreeModelEvent e) { + revalidateSelectedTreePaths(); + } + + private boolean isTreePathValid(TreePath path) { + Object parent = _model.getRoot(); + for (int i = 0; i < path.getPathCount(); i++) { + Object pathComponent = path.getPathComponent(i); + if (i == 0) { + if (pathComponent != parent) { + return false; + } + } + else { + boolean found = false; + for (int j = 0; j < getChildrenCount(parent); j++) { + Object child = getChild(parent, j); + if (child == pathComponent) { + found = true; + break; + } + } + if (!found) { + return false; + } + parent = pathComponent; + } + } + return true; + } + + public void treeStructureChanged(TreeModelEvent e) { + revalidateSelectedTreePaths(); + } + + private void revalidateSelectedTreePaths() { + TreePath[] treePaths = getSelectionPaths(); + if (treePaths != null) { + for (TreePath treePath : treePaths) { + if (treePath != null && !isTreePathValid(treePath)) { + super.removeSelectionPath(treePath); + } + } + } + } +} \ No newline at end of file