package org.rr.commons.swing.components.tree; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.event.AncestorEvent; import javax.swing.event.AncestorListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.plaf.TreeUI; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.rr.commons.swing.SwingUtils; import org.rr.commons.utils.MathUtils; public class JRTree extends JTree { private boolean toggleExpandOnDoubleClick = false; private boolean autoMoveHorizontalSliders = false; private boolean repaintAllOnChange = false; public JRTree() { super(); this.addMouseListener(new ToggleExpandOnDoubleClickMouseListener()); this.getSelectionModel().addTreeSelectionListener(new RepaintChangeListener()); final AutoMoveHorizontalMouseListener autoMoveHorizontalMouseListener = new AutoMoveHorizontalMouseListener(); this.addMouseMotionListener(autoMoveHorizontalMouseListener); this.addAncestorListener(new AncestorListener() { @Override public void ancestorRemoved(AncestorEvent event) { } @Override public void ancestorMoved(AncestorEvent event) { } @Override public void ancestorAdded(AncestorEvent event) { JScrollPane surroundingScrollPane = SwingUtils.getSurroundingScrollPane(JRTree.this); surroundingScrollPane.addMouseWheelListener(autoMoveHorizontalMouseListener); } }); } /** * Scrolls to the given path. Scroll is only performed in the y direction. */ public void scrollPathToVisibleVertical(TreePath path, boolean select) { super.scrollPathToVisible(path); JScrollPane surroundingScrollPane = SwingUtils.getSurroundingScrollPane(this); if (surroundingScrollPane != null) { int rowForPath = getRowForPath(path); int y = rowForPath * getRowHeight(); int halfHeight = surroundingScrollPane.getPreferredSize().height / 2; surroundingScrollPane.getVerticalScrollBar().setValue(y - halfHeight); surroundingScrollPane.getHorizontalScrollBar().setValue(0); if(select) { setSelectionPath(path); } } } public boolean isToggleExpandOnDoubleClick() { return toggleExpandOnDoubleClick; } public void setToggleExpandOnDoubleClick(boolean expandOnDoubleClick) { this.toggleExpandOnDoubleClick = expandOnDoubleClick; } private class ToggleExpandOnDoubleClickMouseListener extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { Point point = e.getPoint(); int row = JRTree.this.getRowForLocation(0, point.y); if(toggleExpandOnDoubleClick && e.getClickCount() == 2) { if(JRTree.this.isExpanded(row)) { JRTree.this.collapseRow(row); } else { JRTree.this.expandRow(row); } } } } public boolean isAutoMoveHorizontalSliders() { return autoMoveHorizontalSliders; } public void setAutoMoveHorizontalSliders(boolean autoMoveHorizontalSliders) { this.autoMoveHorizontalSliders = autoMoveHorizontalSliders; } private class AutoMoveHorizontalMouseListener extends MouseAdapter { private int latestX; private int latestRow; @Override public void mouseMoved(MouseEvent e) { if(isAutoMoveHorizontalSliders()) { move(e); } } @Override public void mouseWheelMoved(MouseWheelEvent e) { if(isAutoMoveHorizontalSliders()) { if(e.getSource() instanceof JScrollPane) { int value = ((JScrollPane)e.getSource()).getVerticalScrollBar().getValue(); e = new MouseWheelEvent((Component) e.getSource(), e.getID(), e.getWhen(), e.getModifiers(), e.getX(), e.getY() + value, e.getClickCount(), e.isPopupTrigger(), e.getScrollType(), e.getScrollAmount(), e.getWheelRotation()); } move(e); } } private void move(MouseEvent e) { int row = JRTree.this.getRowForLocation(0, e.getX()); for (int paddingLeft = 0; row < 0 && paddingLeft < JRTree.this.getWidth(); paddingLeft += 10) { row = JRTree.this.getRowForLocation(paddingLeft, e.getY()); if(row >= 0) { int movement = latestX - e.getX(); if(latestRow != row || !MathUtils.between(movement, -8, +8)) { //row change or a min move by 8 pix in one direction. final JScrollPane surroundingScrollPane = SwingUtils.getSurroundingScrollPane(JRTree.this); int value = calculateScrollBarLocation(e.getLocationOnScreen().x, surroundingScrollPane, paddingLeft, row); int aboveValue = value; int underValue = value; if(row > 0) { aboveValue = calculateScrollBarLocation(e.getLocationOnScreen().x, surroundingScrollPane, paddingLeft, row - 1); } if(row < JRTree.this.getRowCount()) { underValue = calculateScrollBarLocation(e.getLocationOnScreen().x, surroundingScrollPane, paddingLeft, row + 1); } if(value >= 0) { value = Math.max(value, aboveValue); value = Math.max(value, underValue); int oldValueDiff = surroundingScrollPane.getHorizontalScrollBar().getValue() - value; if(!MathUtils.between(oldValueDiff, 11, -11)) { surroundingScrollPane.getHorizontalScrollBar().setValue(value); } } latestX = e.getX(); latestRow = row; } } } } private int calculateScrollBarLocation(int locationOnScreenX, JScrollPane surroundingScrollPane, int paddingLeft, final int row) { final TreePath pathForLocation = JRTree.this.getPathForRow(row); if(pathForLocation == null) { return -1; } final TreeNode node = (TreeNode) pathForLocation.getLastPathComponent(); final boolean isLeaf = node.isLeaf(); final Component treeCellRendererComponent = JRTree.this.getCellRenderer().getTreeCellRendererComponent(JRTree.this, pathForLocation.getLastPathComponent(), false, isExpanded(row), isLeaf, row, false); final int rendererWidth = treeCellRendererComponent.getPreferredSize().width; if(surroundingScrollPane != null) { final int visibleComponentWidth = surroundingScrollPane.getBounds().width ; final int scrollBarWidth = surroundingScrollPane.getVerticalScrollBar().getPreferredSize().width; final int leafPadding = (isLeaf ? 15 : 25); final int visibleWidth = visibleComponentWidth - paddingLeft - rendererWidth - scrollBarWidth; // a minus value for the hidden width if(visibleWidth < -10) { //scroll to hidden int rendererBegin = paddingLeft - leafPadding; //begin of the renderer location // int rendererEnd = (paddingLeft - rendererWidth) * -1; //end of the renderer location // int horizontalScrollbarLocation = surroundingScrollPane.getHorizontalScrollBar().getValue(); // if(horizontalScrollbarLocation < rendererEnd) { if(locationOnScreenX + 50 > (surroundingScrollPane.getLocationOnScreen().x + visibleComponentWidth - scrollBarWidth)) { int value = rendererBegin + (rendererWidth + leafPadding - visibleComponentWidth + scrollBarWidth + 15); if(surroundingScrollPane.getHorizontalScrollBar().getValue() < value) { //scroll to the end of the renderer component return value; } } else { //scroll to the beginning of the renderer component return rendererBegin; } // } } else if(visibleWidth > 10) { int rendererBegin = paddingLeft - leafPadding; //begin of the renderer location int horizontalScrollbarLocation = surroundingScrollPane.getHorizontalScrollBar().getValue(); if(rendererBegin < horizontalScrollbarLocation -10) { return rendererBegin; } } } return -1; } } /** * Tells if all tree nodes are repainted after the selection has changed. * @see #setRepaintAllOnChange */ public boolean isRepaintAllOnChange() { return repaintAllOnChange; } /** * Enables / disables that the all tree nodes are repainted after * the selection has changed. */ public void setRepaintAllOnChange(boolean repaintAllOnChnage) { this.repaintAllOnChange = repaintAllOnChnage; } private class RepaintChangeListener implements TreeSelectionListener { @Override public void valueChanged(TreeSelectionEvent e) { if(isRepaintAllOnChange()) { JRTree.this.repaint(); } } } /** * Returns the row that displays the node identified by the specified * path. * * @param path the <code>TreePath</code> identifying a node * @return an integer specifying the display row, where 0 is the first * row in the display, or -1 if any of the elements in path * are hidden under a collapsed parent. */ public int getRowForPath(TreePath path) { TreeUI tree = getUI(); if(tree != null) { int row = tree.getRowForPath(this, path); if(row >= 0) { return row; } } return -1; } /** * Returns a TreePath for each visible row in the tree. * @return All TreePath elements for each visible row. */ public List<TreePath> getPathForRows() { int rowCount = getRowCount(); List<TreePath> resultPath = new ArrayList<>(rowCount); for(int i = 0; i < rowCount; i++) { resultPath.add(getPathForRow(i)); } return resultPath; } /** Expands all the nodes in this tree. */ public void expandAll() { expandOrCollapsePath(new TreePath(getModel().getRoot()), true); } /** Collapses all the nodes in this tree. */ public void collapseAll() { expandOrCollapsePath(new TreePath(getModel().getRoot()), false); } /** Expands or collapses all nodes beneath the given path represented as an array of nodes. */ public void expandOrCollapsePath(TreeNode[] nodes, boolean expand) { expandOrCollapsePath(new TreePath(nodes), expand); } /** Expands or collapses all nodes beneath the given path. */ private void expandOrCollapsePath(TreePath parent, boolean expand) { TreeNode node = (TreeNode) parent.getLastPathComponent(); if (node.getChildCount() >= 0) { for (Enumeration<?> e = node.children(); e.hasMoreElements(); ) { TreeNode n = (TreeNode) e.nextElement(); TreePath path = parent.pathByAddingChild(n); expandOrCollapsePath(path, expand); } } if (expand) { expandPath(parent); } else { collapsePath(parent); } } /** * Makes JTree's implementation less width-greedy. Left to JTree, we'll * grow to be wide enough to show our widest node without using a scroll * bar. While this is seemingly widely acceptable (ho ho), it's no good * in Evergreen's "Find in Files" dialog. If long lines match, next time you * open the dialog, it can be so wide it doesn't fit on the screen. Here, * we go for the minimum width, and assume that an ETree is never packed * on its own (in which case, it might end up rather narrow by default). */ public Dimension getPreferredScrollableViewportSize() { Dimension size = super.getPreferredScrollableViewportSize(); size.width = getMinimumSize().width; return size; } /** * Selects the nodes matching the given string. The matching is * a case-insensitive substring match. The selection is not cleared * first; you must do this yourself if it's the behavior you want. * * If ensureVisible is true, the first selected node in the model * will be made visible via scrollPathToVisible. */ public void selectNodesMatching(String string, boolean ensureVisible) { TreePath path = new TreePath(getModel().getRoot()); selectNodesMatching(path, string.toLowerCase()); if (ensureVisible) { scrollPathToVisible(getSelectionPath()); } } private void selectNodesMatching(TreePath parent, String string) { TreeNode node = (TreeNode) parent.getLastPathComponent(); if (node.getChildCount() >= 0) { for (Enumeration<?> e = node.children(); e.hasMoreElements(); ) { TreeNode n = (TreeNode) e.nextElement(); TreePath path = parent.pathByAddingChild(n); selectNodesMatching(path, string); } } if (node.toString().toLowerCase().contains(string)) { addSelectionPath(parent); } } /** Scrolls the path to the middle of the scroll pane. */ public void scrollPathToVisible(TreePath path) { if (path == null) { return; } makeVisible(path); Rectangle pathBounds = getPathBounds(path); if (pathBounds != null) { Rectangle visibleRect = getVisibleRect(); if (getHeight() > visibleRect.height) { int y = pathBounds.y - visibleRect.height / 2; visibleRect.y = Math.min(Math.max(0, y), getHeight() - visibleRect.height); scrollRectToVisible(visibleRect); } } } /** * Returns the path for the node at the specified location. * * @param x an integer giving the number of pixels horizontally from * the left edge of the display area, minus any left margin * @param y an integer giving the number of pixels vertically from * the top of the display area, minus any top margin * @return the <code>TreePath</code> for the node at that location */ public TreePath getPathForLocation(int x, int y) { TreePath closestPath = getClosestPathForLocation(x, y); if (closestPath != null) { Rectangle pathBounds = getPathBounds(closestPath); if (pathBounds != null && y >= pathBounds.y && y < (pathBounds.y + pathBounds.height)) { return closestPath; } } return null; } }