1 /*
2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package com.apple.laf;
27 
28 import java.awt.*;
29 import java.awt.event.*;
30 import java.beans.*;
31 
32 import javax.swing.*;
33 import javax.swing.event.MouseInputAdapter;
34 import javax.swing.plaf.*;
35 import javax.swing.plaf.basic.BasicTreeUI;
36 import javax.swing.tree.*;
37 
38 import com.apple.laf.AquaUtils.RecyclableSingleton;
39 
40 import apple.laf.*;
41 import apple.laf.JRSUIConstants.*;
42 import apple.laf.JRSUIState.AnimationFrameState;
43 
44 /**
45  * AquaTreeUI supports the client property "value-add" system of customization See MetalTreeUI
46  * This is heavily based on the 1.3.1 AquaTreeUI implementation.
47  */
48 public class AquaTreeUI extends BasicTreeUI {
49 
50     // Create PLAF
createUI(final JComponent c)51     public static ComponentUI createUI(final JComponent c) {
52         return new AquaTreeUI();
53     }
54 
55     // Begin Line Stuff from Metal
56 
57     private static final String LINE_STYLE = "JTree.lineStyle";
58 
59     private static final String LEG_LINE_STYLE_STRING = "Angled";
60     private static final String HORIZ_STYLE_STRING = "Horizontal";
61     private static final String NO_STYLE_STRING = "None";
62 
63     private static final int LEG_LINE_STYLE = 2;
64     private static final int HORIZ_LINE_STYLE = 1;
65     private static final int NO_LINE_STYLE = 0;
66 
67     private int lineStyle = HORIZ_LINE_STYLE;
68     private final PropertyChangeListener lineStyleListener = new LineListener();
69 
70     // mouse tracking state
71     protected TreePath fTrackingPath;
72     protected boolean fIsPressed = false;
73     protected boolean fIsInBounds = false;
74     protected int fAnimationFrame = -1;
75     protected TreeArrowMouseInputHandler fMouseHandler;
76 
77     protected final AquaPainter<AnimationFrameState> painter = AquaPainter.create(JRSUIStateFactory.getDisclosureTriangle());
78 
AquaTreeUI()79     public AquaTreeUI() {
80 
81     }
82 
installUI(final JComponent c)83     public void installUI(final JComponent c) {
84         super.installUI(c);
85 
86         final Object lineStyleFlag = c.getClientProperty(LINE_STYLE);
87         decodeLineStyle(lineStyleFlag);
88         c.addPropertyChangeListener(lineStyleListener);
89     }
90 
uninstallUI(final JComponent c)91     public void uninstallUI(final JComponent c) {
92         c.removePropertyChangeListener(lineStyleListener);
93         super.uninstallUI(c);
94     }
95 
96     /**
97      * Creates the focus listener to repaint the focus ring
98      */
createFocusListener()99     protected FocusListener createFocusListener() {
100         return new AquaTreeUI.FocusHandler();
101     }
102 
103     /**
104      * this function converts between the string passed into the client property and the internal representation
105      * (currently an int)
106      */
decodeLineStyle(final Object lineStyleFlag)107     protected void decodeLineStyle(final Object lineStyleFlag) {
108         if (lineStyleFlag == null || NO_STYLE_STRING.equals(lineStyleFlag)) {
109             lineStyle = NO_LINE_STYLE; // default case
110             return;
111         }
112 
113         if (LEG_LINE_STYLE_STRING.equals(lineStyleFlag)) {
114             lineStyle = LEG_LINE_STYLE;
115         } else if (HORIZ_STYLE_STRING.equals(lineStyleFlag)) {
116             lineStyle = HORIZ_LINE_STYLE;
117         }
118     }
119 
getClosestPathForLocation(final JTree treeLocal, final int x, final int y)120     public TreePath getClosestPathForLocation(final JTree treeLocal, final int x, final int y) {
121         if (treeLocal == null || treeState == null) return null;
122 
123         Insets i = treeLocal.getInsets();
124         if (i == null) i = new Insets(0, 0, 0, 0);
125         return treeState.getPathClosestTo(x - i.left, y - i.top);
126     }
127 
paint(final Graphics g, final JComponent c)128     public void paint(final Graphics g, final JComponent c) {
129         super.paint(g, c);
130 
131         // Paint the lines
132         if (lineStyle == HORIZ_LINE_STYLE && !largeModel) {
133             paintHorizontalSeparators(g, c);
134         }
135     }
136 
paintHorizontalSeparators(final Graphics g, final JComponent c)137     protected void paintHorizontalSeparators(final Graphics g, final JComponent c) {
138         g.setColor(UIManager.getColor("Tree.line"));
139 
140         final Rectangle clipBounds = g.getClipBounds();
141 
142         final int beginRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y));
143         final int endRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y + clipBounds.height - 1));
144 
145         if (beginRow <= -1 || endRow <= -1) { return; }
146 
147         for (int i = beginRow; i <= endRow; ++i) {
148             final TreePath path = getPathForRow(tree, i);
149 
150             if (path != null && path.getPathCount() == 2) {
151                 final Rectangle rowBounds = getPathBounds(tree, getPathForRow(tree, i));
152 
153                 // Draw a line at the top
154                 if (rowBounds != null) g.drawLine(clipBounds.x, rowBounds.y, clipBounds.x + clipBounds.width, rowBounds.y);
155             }
156         }
157     }
158 
paintVerticalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final TreePath path)159     protected void paintVerticalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final TreePath path) {
160         if (lineStyle == LEG_LINE_STYLE) {
161             super.paintVerticalPartOfLeg(g, clipBounds, insets, path);
162         }
163     }
164 
paintHorizontalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf)165     protected void paintHorizontalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf) {
166         if (lineStyle == LEG_LINE_STYLE) {
167             super.paintHorizontalPartOfLeg(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
168         }
169     }
170 
171     /** This class listens for changes in line style */
172     class LineListener implements PropertyChangeListener {
propertyChange(final PropertyChangeEvent e)173         public void propertyChange(final PropertyChangeEvent e) {
174             final String name = e.getPropertyName();
175             if (name.equals(LINE_STYLE)) {
176                 decodeLineStyle(e.getNewValue());
177             }
178         }
179     }
180 
181     /**
182      * Paints the expand (toggle) part of a row. The receiver should NOT modify {@code clipBounds}, or
183      * {@code insets}.
184      */
paintExpandControl(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf)185     protected void paintExpandControl(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf) {
186         final Object value = path.getLastPathComponent();
187 
188         // Draw icons if not a leaf and either hasn't been loaded,
189         // or the model child count is > 0.
190         if (isLeaf || (hasBeenExpanded && treeModel.getChildCount(value) <= 0)) return;
191 
192         final boolean isLeftToRight = AquaUtils.isLeftToRight(tree); // Basic knows, but keeps it private
193 
194         final State state = getState(path);
195 
196         // if we are not animating, do the expected thing, and use the icon
197         // also, if there is a custom (non-LaF defined) icon - just use that instead
198         if (fAnimationFrame == -1 && state != State.PRESSED) {
199             super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
200             return;
201         }
202 
203         // Both icons are the same size
204         final Icon icon = isExpanded ? getExpandedIcon() : getCollapsedIcon();
205         if (!(icon instanceof UIResource)) {
206             super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
207             return;
208         }
209 
210         // if painting a right-to-left knob, we ensure that we are only painting when
211         // the clipbounds rect is set to the exact size of the knob, and positioned correctly
212         // (this code is not the same as metal)
213         int middleXOfKnob;
214         if (isLeftToRight) {
215             middleXOfKnob = bounds.x - (getRightChildIndent() - 1);
216         } else {
217             middleXOfKnob = clipBounds.x + clipBounds.width / 2;
218         }
219 
220         // Center vertically
221         final int middleYOfKnob = bounds.y + (bounds.height / 2);
222 
223         final int x = middleXOfKnob - icon.getIconWidth() / 2;
224         final int y = middleYOfKnob - icon.getIconHeight() / 2;
225         final int height = icon.getIconHeight(); // use the icon height so we don't get drift  we modify the bounds (by changing row height)
226         final int width = 20; // this is a hardcoded value from our default icon (since we are only at this point for animation)
227 
228         setupPainter(state, isExpanded, isLeftToRight);
229         painter.paint(g, tree, x, y, width, height);
230     }
231 
232     @Override
getCollapsedIcon()233     public Icon getCollapsedIcon() {
234         final Icon icon = super.getCollapsedIcon();
235         if (AquaUtils.isLeftToRight(tree)) return icon;
236         if (!(icon instanceof UIResource)) return icon;
237         return UIManager.getIcon("Tree.rightToLeftCollapsedIcon");
238     }
239 
setupPainter(State state, final boolean isExpanded, final boolean leftToRight)240     protected void setupPainter(State state, final boolean isExpanded, final boolean leftToRight) {
241         if (!fIsInBounds && state == State.PRESSED) state = State.ACTIVE;
242 
243         painter.state.set(state);
244         if (JRSUIUtils.Tree.useLegacyTreeKnobs()) {
245             if (fAnimationFrame == -1) {
246                 painter.state.set(isExpanded ? Direction.DOWN : Direction.RIGHT);
247             } else {
248                 painter.state.set(Direction.NONE);
249                 painter.state.setAnimationFrame(fAnimationFrame - 1);
250             }
251         } else {
252             painter.state.set(getDirection(isExpanded, leftToRight));
253             painter.state.setAnimationFrame(fAnimationFrame);
254         }
255     }
256 
getDirection(final boolean isExpanded, final boolean isLeftToRight)257     protected Direction getDirection(final boolean isExpanded, final boolean isLeftToRight) {
258         if (isExpanded && (fAnimationFrame == -1)) return Direction.DOWN;
259         return isLeftToRight ? Direction.RIGHT : Direction.LEFT;
260     }
261 
getState(final TreePath path)262     protected State getState(final TreePath path) {
263         if (!tree.isEnabled()) return State.DISABLED;
264         if (fIsPressed) {
265             if (fTrackingPath.equals(path)) return State.PRESSED;
266         }
267         return State.ACTIVE;
268     }
269 
270     /**
271      * Misnamed - this is called on mousePressed Macs shouldn't react till mouseReleased
272      * We install a motion handler that gets removed after.
273      * See super.MouseInputHandler & super.startEditing for why
274      */
handleExpandControlClick(final TreePath path, final int mouseX, final int mouseY)275     protected void handleExpandControlClick(final TreePath path, final int mouseX, final int mouseY) {
276         fMouseHandler = new TreeArrowMouseInputHandler(path);
277     }
278 
279     /**
280      * Returning true signifies a mouse event on the node should toggle the selection of only the row under mouse.
281      */
isToggleSelectionEvent(final MouseEvent event)282     protected boolean isToggleSelectionEvent(final MouseEvent event) {
283         return SwingUtilities.isLeftMouseButton(event) && event.isMetaDown();
284     }
285 
286     class FocusHandler extends BasicTreeUI.FocusHandler {
focusGained(final FocusEvent e)287         public void focusGained(final FocusEvent e) {
288             super.focusGained(e);
289             AquaBorder.repaintBorder(tree);
290         }
291 
focusLost(final FocusEvent e)292         public void focusLost(final FocusEvent e) {
293             super.focusLost(e);
294             AquaBorder.repaintBorder(tree);
295         }
296     }
297 
createPropertyChangeListener()298     protected PropertyChangeListener createPropertyChangeListener() {
299         return new MacPropertyChangeHandler();
300     }
301 
302     public class MacPropertyChangeHandler extends PropertyChangeHandler {
propertyChange(final PropertyChangeEvent e)303         public void propertyChange(final PropertyChangeEvent e) {
304             final String prop = e.getPropertyName();
305             if (prop.equals(AquaFocusHandler.FRAME_ACTIVE_PROPERTY)) {
306                 AquaBorder.repaintBorder(tree);
307                 AquaFocusHandler.swapSelectionColors("Tree", tree, e.getNewValue());
308             } else {
309                 super.propertyChange(e);
310             }
311         }
312     }
313 
314     /**
315      * TreeArrowMouseInputHandler handles passing all mouse events the way a Mac should - hilite/dehilite on enter/exit,
316      * only perform the action if released in arrow.
317      *
318      * Just like super.MouseInputHandler, this is removed once it's not needed, so they won't clash with each other
319      */
320     // The Adapters take care of defining all the empties
321     class TreeArrowMouseInputHandler extends MouseInputAdapter {
322         protected Rectangle fPathBounds = new Rectangle();
323 
324         // Values needed for paintOneControl
325         protected boolean fIsLeaf, fIsExpanded, fHasBeenExpanded;
326         protected Rectangle fBounds, fVisibleRect;
327         int fTrackingRow;
328         Insets fInsets;
329         Color fBackground;
330 
TreeArrowMouseInputHandler(final TreePath path)331         TreeArrowMouseInputHandler(final TreePath path) {
332             fTrackingPath = path;
333             fIsPressed = true;
334             fIsInBounds = true;
335             this.fPathBounds = getPathArrowBounds(path);
336             tree.addMouseListener(this);
337             tree.addMouseMotionListener(this);
338             fBackground = tree.getBackground();
339             if (!tree.isOpaque()) {
340                 final Component p = tree.getParent();
341                 if (p != null) fBackground = p.getBackground();
342             }
343 
344             // Set up values needed to paint the triangle - see
345             // BasicTreeUI.paint
346             fVisibleRect = tree.getVisibleRect();
347             fInsets = tree.getInsets();
348 
349             if (fInsets == null) fInsets = new Insets(0, 0, 0, 0);
350             fIsLeaf = treeModel.isLeaf(path.getLastPathComponent());
351             if (fIsLeaf) fIsExpanded = fHasBeenExpanded = false;
352             else {
353                 fIsExpanded = treeState.getExpandedState(path);
354                 fHasBeenExpanded = tree.hasBeenExpanded(path);
355             }
356             final Rectangle boundsBuffer = new Rectangle();
357             fBounds = treeState.getBounds(fTrackingPath, boundsBuffer);
358             fBounds.x += fInsets.left;
359             fBounds.y += fInsets.top;
360             fTrackingRow = getRowForPath(fTrackingPath);
361 
362             paintOneControl();
363         }
364 
mouseDragged(final MouseEvent e)365         public void mouseDragged(final MouseEvent e) {
366             fIsInBounds = fPathBounds.contains(e.getX(), e.getY());
367                 paintOneControl();
368             }
369 
370         @Override
mouseExited(MouseEvent e)371         public void mouseExited(MouseEvent e) {
372             fIsInBounds = fPathBounds.contains(e.getX(), e.getY());
373             paintOneControl();
374         }
375 
mouseReleased(final MouseEvent e)376         public void mouseReleased(final MouseEvent e) {
377             if (tree == null) return;
378 
379             if (fIsPressed) {
380                 final boolean wasInBounds = fIsInBounds;
381 
382                 fIsPressed = false;
383                 fIsInBounds = false;
384 
385                 if (wasInBounds) {
386                     fIsExpanded = !fIsExpanded;
387                     paintAnimation(fIsExpanded);
388                     if (e.isAltDown()) {
389                         if (fIsExpanded) {
390                             expandNode(fTrackingRow, true);
391                         } else {
392                             collapseNode(fTrackingRow, true);
393                         }
394                     } else {
395                         toggleExpandState(fTrackingPath);
396                     }
397                 }
398             }
399             fTrackingPath = null;
400             removeFromSource();
401         }
402 
paintAnimation(final boolean expanding)403         protected void paintAnimation(final boolean expanding) {
404             if (expanding) {
405                 paintAnimationFrame(1);
406                 paintAnimationFrame(2);
407                 paintAnimationFrame(3);
408             } else {
409                 paintAnimationFrame(3);
410                 paintAnimationFrame(2);
411                 paintAnimationFrame(1);
412             }
413             fAnimationFrame = -1;
414         }
415 
paintAnimationFrame(final int frame)416         protected void paintAnimationFrame(final int frame) {
417             fAnimationFrame = frame;
418             paintOneControl();
419             try { Thread.sleep(20); } catch (final InterruptedException e) { }
420         }
421 
422         // Utility to paint just one widget while it's being tracked
423         // Just doing "repaint" runs into problems if someone does "translate" on the graphics
424         // (ie, Sun's JTreeTable example, which is used by Moneydance - see Radar 2697837)
paintOneControl()425         void paintOneControl() {
426             if (tree == null) return;
427             final Graphics g = tree.getGraphics();
428             if (g == null) {
429                 // i.e. source is not displayable
430                 return;
431             }
432 
433             try {
434                 g.setClip(fVisibleRect);
435                 // If we ever wanted a callback for drawing the arrow between
436                 // transition stages
437                 // the code between here and paintExpandControl would be it
438                 g.setColor(fBackground);
439                 g.fillRect(fPathBounds.x, fPathBounds.y, fPathBounds.width, fPathBounds.height);
440 
441                 // if there is no tracking path, we don't need to paint anything
442                 if (fTrackingPath == null) return;
443 
444                 // draw the vertical line to the parent
445                 final TreePath parentPath = fTrackingPath.getParentPath();
446                 if (parentPath != null) {
447                     paintVerticalPartOfLeg(g, fPathBounds, fInsets, parentPath);
448                     paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf);
449                 } else if (isRootVisible() && fTrackingRow == 0) {
450                     paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf);
451                 }
452                 paintExpandControl(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf);
453             } finally {
454                 g.dispose();
455             }
456         }
457 
removeFromSource()458         protected void removeFromSource() {
459             tree.removeMouseListener(this);
460             tree.removeMouseMotionListener(this);
461             }
462         }
463 
getRowForPath(final TreePath path)464     protected int getRowForPath(final TreePath path) {
465         return treeState.getRowForPath(path);
466     }
467 
468     /**
469      * see isLocationInExpandControl for bounds calc
470      */
getPathArrowBounds(final TreePath path)471     protected Rectangle getPathArrowBounds(final TreePath path) {
472         final Rectangle bounds = getPathBounds(tree, path); // Gives us the y values, but x is adjusted for the contents
473         final Insets i = tree.getInsets();
474 
475         if (getExpandedIcon() != null) bounds.width = getExpandedIcon().getIconWidth();
476         else bounds.width = 8;
477 
478         int boxLeftX = (i != null) ? i.left : 0;
479         if (AquaUtils.isLeftToRight(tree)) {
480             boxLeftX += (((path.getPathCount() + depthOffset - 2) * totalChildIndent) + getLeftChildIndent()) - bounds.width / 2;
481         } else {
482             boxLeftX += tree.getWidth() - 1 - ((path.getPathCount() - 2 + depthOffset) * totalChildIndent) - getLeftChildIndent() - bounds.width / 2;
483         }
484         bounds.x = boxLeftX;
485         return bounds;
486     }
487 
installKeyboardActions()488     protected void installKeyboardActions() {
489         super.installKeyboardActions();
490         tree.getActionMap().put("aquaExpandNode", new KeyboardExpandCollapseAction(true, false));
491         tree.getActionMap().put("aquaCollapseNode", new KeyboardExpandCollapseAction(false, false));
492         tree.getActionMap().put("aquaFullyExpandNode", new KeyboardExpandCollapseAction(true, true));
493         tree.getActionMap().put("aquaFullyCollapseNode", new KeyboardExpandCollapseAction(false, true));
494     }
495 
496     @SuppressWarnings("serial") // Superclass is not serializable across versions
497     class KeyboardExpandCollapseAction extends AbstractAction {
498         /**
499          * Determines direction to traverse, 1 means expand, -1 means collapse.
500          */
501         final boolean expand;
502         final boolean recursive;
503 
504         /**
505          * True if the selection is reset, false means only the lead path changes.
506          */
KeyboardExpandCollapseAction(final boolean expand, final boolean recursive)507         public KeyboardExpandCollapseAction(final boolean expand, final boolean recursive) {
508             this.expand = expand;
509             this.recursive = recursive;
510         }
511 
actionPerformed(final ActionEvent e)512         public void actionPerformed(final ActionEvent e) {
513             if (tree == null || 0 > getRowCount(tree)) return;
514 
515             final TreePath[] selectionPaths = tree.getSelectionPaths();
516             if (selectionPaths == null) return;
517 
518             for (int i = selectionPaths.length - 1; i >= 0; i--) {
519                 final TreePath path = selectionPaths[i];
520 
521                 /*
522                  * Try and expand the node, otherwise go to next node.
523                  */
524                 if (expand) {
525                     expandNode(tree.getRowForPath(path), recursive);
526                     continue;
527                 }
528                 // else collapse
529 
530                 // in the special case where there is only one row selected,
531                 // we want to do what the Cocoa does, and select the parent
532                 if (selectionPaths.length == 1 && tree.isCollapsed(path)) {
533                     final TreePath parentPath = path.getParentPath();
534                     if (parentPath != null && (!(parentPath.getParentPath() == null) || tree.isRootVisible())) {
535                         tree.scrollPathToVisible(parentPath);
536                         tree.setSelectionPath(parentPath);
537                     }
538                     continue;
539                 }
540 
541                 collapseNode(tree.getRowForPath(path), recursive);
542             }
543         }
544 
isEnabled()545         public boolean isEnabled() {
546             return (tree != null && tree.isEnabled());
547         }
548     }
549 
expandNode(final int row, final boolean recursive)550     void expandNode(final int row, final boolean recursive) {
551         final TreePath path = getPathForRow(tree, row);
552         if (path == null) return;
553 
554         tree.expandPath(path);
555         if (!recursive) return;
556 
557         expandAllNodes(path, row + 1);
558     }
559 
expandAllNodes(final TreePath parent, final int initialRow)560     void expandAllNodes(final TreePath parent, final int initialRow) {
561         for (int i = initialRow; true; i++) {
562             final TreePath path = getPathForRow(tree, i);
563             if (!parent.isDescendant(path)) return;
564 
565             tree.expandPath(path);
566         }
567     }
568 
collapseNode(final int row, final boolean recursive)569     void collapseNode(final int row, final boolean recursive) {
570         final TreePath path = getPathForRow(tree, row);
571         if (path == null) return;
572 
573         if (recursive) {
574             collapseAllNodes(path, row + 1);
575         }
576 
577         tree.collapsePath(path);
578     }
579 
collapseAllNodes(final TreePath parent, final int initialRow)580     void collapseAllNodes(final TreePath parent, final int initialRow) {
581         int lastRow = -1;
582         for (int i = initialRow; lastRow == -1; i++) {
583             final TreePath path = getPathForRow(tree, i);
584             if (!parent.isDescendant(path)) {
585                 lastRow = i - 1;
586             }
587         }
588 
589         for (int i = lastRow; i >= initialRow; i--) {
590             final TreePath path = getPathForRow(tree, i);
591             tree.collapsePath(path);
592         }
593     }
594 }
595