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
  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 
  79     public AquaTreeUI() {
  80 
  81     }
  82 
  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 
  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      */
  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      */
 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 
 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 
 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 
 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 
 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 
 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 {
 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</code>, or
 183      * <code>insets</code>.
 184      */
 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
 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 
 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 
 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 
 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      */
 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      */
 282     protected boolean isToggleSelectionEvent(final MouseEvent event) {
 283         return SwingUtilities.isLeftMouseButton(event) && event.isMetaDown();
 284     }
 285 
 286     class FocusHandler extends BasicTreeUI.FocusHandler {
 287         public void focusGained(final FocusEvent e) {
 288             super.focusGained(e);
 289             AquaBorder.repaintBorder(tree);
 290         }
 291 
 292         public void focusLost(final FocusEvent e) {
 293             super.focusLost(e);
 294             AquaBorder.repaintBorder(tree);
 295         }
 296     }
 297 
 298     protected PropertyChangeListener createPropertyChangeListener() {
 299         return new MacPropertyChangeHandler();
 300     }
 301 
 302     public class MacPropertyChangeHandler extends PropertyChangeHandler {
 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 
 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 
 365         public void mouseDragged(final MouseEvent e) {
 366             fIsInBounds = fPathBounds.contains(e.getX(), e.getY());
 367                 paintOneControl();
 368             }
 369 
 370         @Override
 371         public void mouseExited(MouseEvent e) {
 372             fIsInBounds = fPathBounds.contains(e.getX(), e.getY());
 373             paintOneControl();
 374         }
 375 
 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 
 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 
 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)
 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 
 458         protected void removeFromSource() {
 459             tree.removeMouseListener(this);
 460             tree.removeMouseMotionListener(this);
 461             }
 462         }
 463 
 464     protected int getRowForPath(final TreePath path) {
 465         return treeState.getRowForPath(path);
 466     }
 467 
 468     /**
 469      * see isLocationInExpandControl for bounds calc
 470      */
 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 
 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          */
 507         public KeyboardExpandCollapseAction(final boolean expand, final boolean recursive) {
 508             this.expand = expand;
 509             this.recursive = recursive;
 510         }
 511 
 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 
 545         public boolean isEnabled() {
 546             return (tree != null && tree.isEnabled());
 547         }
 548     }
 549 
 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 
 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 
 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 
 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 }