Web Inspector: TreeOutline should re-use multiple-selection logic from Table
authormattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 28 Nov 2018 02:55:42 +0000 (02:55 +0000)
committermattbaker@apple.com <mattbaker@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 28 Nov 2018 02:55:42 +0000 (02:55 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191483
<rdar://problem/45953305>

Reviewed by Devin Rousso.

Update TreeOutline to use SelectionController. Adopting SelectionController
in TreeOutline is not as straightforward as it was in Table. Selected items
are tracked by index, and TreeElement lacks an explicit index. As a consequence
TreeElement indexes are calcualted as needed and cached. The cache is cleared
whenever an element is added or removed.

* UserInterface/Controllers/SelectionController.js:
(WI.SelectionController.prototype.didInsertItem):
(WI.SelectionController.prototype.didRemoveItem):
(WI.SelectionController.prototype.handleKeyDown):
Drive-by syntax error fix.
(WI.SelectionController.prototype._adjustIndexesAfter):
(WI.SelectionController):

* UserInterface/Views/DOMTreeElement.js:
(WI.DOMTreeElement.prototype.canSelectOnMouseDown):
(WI.DOMTreeElement.prototype.selectOnMouseDown): Deleted.

* UserInterface/Views/DOMTreeOutline.js:
(WI.DOMTreeOutline.prototype._onmousedown):
Item selection is now handled by SelectionController.

* UserInterface/Views/ShaderProgramTreeElement.js:
(WI.ShaderProgramTreeElement.prototype.canSelectOnMouseDown):
(WI.ShaderProgramTreeElement.prototype.selectOnMouseDown): Deleted.

* UserInterface/Views/TreeElement.js:
(WI.TreeElement.prototype.canSelectOnMouseDown):
(WI.TreeElement.prototype._attach):
(WI.TreeElement.prototype.select):
(WI.TreeElement.prototype.deselect):
Route item selection through the parent TreeOutline, in order to go though
the TreeOutline's SelectionController.

(WI.TreeElement.treeElementMouseDown): Deleted.
Moved handler to TreeOutline, which owns the SelectionController that
needs to respond to mouse events.

* UserInterface/Views/TreeOutline.js:
(WI.TreeOutline):
(WI.TreeOutline.prototype.get allowsMultipleSelection):
(WI.TreeOutline.prototype.set allowsMultipleSelection):
(WI.TreeOutline.prototype.get selectedTreeElement):
(WI.TreeOutline.prototype.set selectedTreeElement):
(WI.TreeOutline.prototype.insertChild):
(WI.TreeOutline.prototype.removeChildAtIndex):
(WI.TreeOutline.prototype._rememberTreeElement):
(WI.TreeOutline.prototype._forgetTreeElement):
(WI.TreeOutline.prototype._treeKeyDown):
(WI.TreeOutline.prototype.selectionControllerNumberOfItems):
(WI.TreeOutline.prototype.selectionControllerSelectionDidChange):
(WI.TreeOutline.prototype.selectionControllerNextSelectableIndex):
(WI.TreeOutline.prototype.selectionControllerPreviousSelectableIndex):
(WI.TreeOutline.prototype.selectTreeElementInternal):
(WI.TreeOutline._generateStyleRulesIfNeeded._indexOfTreeElement.previousElement):
(WI.TreeOutline._generateStyleRulesIfNeeded):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@238599 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Controllers/SelectionController.js
Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js
Source/WebInspectorUI/UserInterface/Views/DOMTreeOutline.js
Source/WebInspectorUI/UserInterface/Views/ShaderProgramTreeElement.js
Source/WebInspectorUI/UserInterface/Views/TreeElement.js
Source/WebInspectorUI/UserInterface/Views/TreeOutline.js

index 4cb8d8e..2e6ecdc 100644 (file)
@@ -1,3 +1,68 @@
+2018-11-27  Matt Baker  <mattbaker@apple.com>
+
+        Web Inspector: TreeOutline should re-use multiple-selection logic from Table
+        https://bugs.webkit.org/show_bug.cgi?id=191483
+        <rdar://problem/45953305>
+
+        Reviewed by Devin Rousso.
+
+        Update TreeOutline to use SelectionController. Adopting SelectionController
+        in TreeOutline is not as straightforward as it was in Table. Selected items
+        are tracked by index, and TreeElement lacks an explicit index. As a consequence
+        TreeElement indexes are calcualted as needed and cached. The cache is cleared
+        whenever an element is added or removed.
+
+        * UserInterface/Controllers/SelectionController.js:
+        (WI.SelectionController.prototype.didInsertItem):
+        (WI.SelectionController.prototype.didRemoveItem):
+        (WI.SelectionController.prototype.handleKeyDown):
+        Drive-by syntax error fix.
+        (WI.SelectionController.prototype._adjustIndexesAfter):
+        (WI.SelectionController):
+
+        * UserInterface/Views/DOMTreeElement.js:
+        (WI.DOMTreeElement.prototype.canSelectOnMouseDown):
+        (WI.DOMTreeElement.prototype.selectOnMouseDown): Deleted.
+
+        * UserInterface/Views/DOMTreeOutline.js:
+        (WI.DOMTreeOutline.prototype._onmousedown):
+        Item selection is now handled by SelectionController.
+
+        * UserInterface/Views/ShaderProgramTreeElement.js:
+        (WI.ShaderProgramTreeElement.prototype.canSelectOnMouseDown):
+        (WI.ShaderProgramTreeElement.prototype.selectOnMouseDown): Deleted.
+
+        * UserInterface/Views/TreeElement.js:
+        (WI.TreeElement.prototype.canSelectOnMouseDown):
+        (WI.TreeElement.prototype._attach):
+        (WI.TreeElement.prototype.select):
+        (WI.TreeElement.prototype.deselect):
+        Route item selection through the parent TreeOutline, in order to go though
+        the TreeOutline's SelectionController.
+
+        (WI.TreeElement.treeElementMouseDown): Deleted.
+        Moved handler to TreeOutline, which owns the SelectionController that
+        needs to respond to mouse events.
+
+        * UserInterface/Views/TreeOutline.js:
+        (WI.TreeOutline):
+        (WI.TreeOutline.prototype.get allowsMultipleSelection):
+        (WI.TreeOutline.prototype.set allowsMultipleSelection):
+        (WI.TreeOutline.prototype.get selectedTreeElement):
+        (WI.TreeOutline.prototype.set selectedTreeElement):
+        (WI.TreeOutline.prototype.insertChild):
+        (WI.TreeOutline.prototype.removeChildAtIndex):
+        (WI.TreeOutline.prototype._rememberTreeElement):
+        (WI.TreeOutline.prototype._forgetTreeElement):
+        (WI.TreeOutline.prototype._treeKeyDown):
+        (WI.TreeOutline.prototype.selectionControllerNumberOfItems):
+        (WI.TreeOutline.prototype.selectionControllerSelectionDidChange):
+        (WI.TreeOutline.prototype.selectionControllerNextSelectableIndex):
+        (WI.TreeOutline.prototype.selectionControllerPreviousSelectableIndex):
+        (WI.TreeOutline.prototype.selectTreeElementInternal):
+        (WI.TreeOutline._generateStyleRulesIfNeeded._indexOfTreeElement.previousElement):
+        (WI.TreeOutline._generateStyleRulesIfNeeded):
+
 2018-11-27  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Experimental Computed panel is unreadable in Dark Mode
index ab2417b..ddfbf53 100644 (file)
@@ -192,15 +192,17 @@ WI.SelectionController = class SelectionController extends WI.Object
         this._selectedIndexes.clear();
     }
 
+    didInsertItem(index)
+    {
+        this._adjustIndexesAfter(index - 1, 1);
+    }
+
     didRemoveItem(index)
     {
         if (this.hasSelectedItem(index))
             this.deselectItem(index);
 
-        while (index = this._selectedIndexes.indexGreaterThan(index)) {
-            this._selectedIndexes.delete(index);
-            this._selectedIndexes.add(index - 1);
-        }
+        this._adjustIndexesAfter(index, -1);
     }
 
     handleKeyDown(event)
@@ -208,7 +210,7 @@ WI.SelectionController = class SelectionController extends WI.Object
         if (!this.numberOfItems)
             return false;
 
-        if (event.key === "a" && event.commandOrControlKey()) {
+        if (event.key === "a" && event.commandOrControlKey) {
             this.selectAll();
             return true;
         }
@@ -372,4 +374,12 @@ WI.SelectionController = class SelectionController extends WI.Object
         let selectedItems = indexes.difference(oldSelectedIndexes);
         this._delegate.selectionControllerSelectionDidChange(this, deselectedItems, selectedItems);
     }
+
+    _adjustIndexesAfter(index, delta)
+    {
+        while (index = this._selectedIndexes.indexGreaterThan(index)) {
+            this._selectedIndexes.delete(index);
+            this._selectedIndexes.add(index + delta);
+        }
+    }
 };
index 75e7721..6c97a0a 100644 (file)
@@ -669,16 +669,16 @@ WI.DOMTreeElement = class DOMTreeElement extends WI.TreeElement
         return true;
     }
 
-    selectOnMouseDown(event)
+    canSelectOnMouseDown(event)
     {
-        super.selectOnMouseDown(event);
-
         if (this._editing)
-            return;
+            return false;
 
         // Prevent selecting the nearest word on double click.
         if (event.detail >= 2)
-            event.preventDefault();
+            return false;
+
+        return true;
     }
 
     ondblclick(event)
index 4ad432e..d5eaa16 100644 (file)
@@ -298,8 +298,6 @@ WI.DOMTreeOutline = class DOMTreeOutline extends WI.TreeOutline
             event.preventDefault();
             return;
         }
-
-        element.select();
     }
 
     _onmousemove(event)
index 92da682..fef6917 100644 (file)
@@ -48,12 +48,9 @@ WI.ShaderProgramTreeElement = class ShaderProgramTreeElement extends WI.GeneralT
         this.element.addEventListener("mouseout", this._handleMouseOut.bind(this));
     }
 
-    selectOnMouseDown(event)
+    canSelectOnMouseDown(event)
     {
-        if (this._statusElement.contains(event.target))
-            return;
-
-        super.selectOnMouseDown(event);
+        return !this._statusElement.contains(event.target);
     }
 
     // Private
index 5c09a2f..4e4c744 100644 (file)
@@ -187,6 +187,12 @@ WI.TreeElement = class TreeElement extends WI.Object
             this.expand();
     }
 
+    canSelectOnMouseDown(event)
+    {
+        // Overridden by subclasses if needed.
+        return true;
+    }
+
     _fireDidChange()
     {
         if (this.treeOutline)
@@ -239,7 +245,6 @@ WI.TreeElement = class TreeElement extends WI.Object
             if (this.selected)
                 this._listItemNode.classList.add("selected");
 
-            this._listItemNode.addEventListener("mousedown", WI.TreeElement.treeElementMouseDown);
             this._listItemNode.addEventListener("click", WI.TreeElement.treeElementToggled);
             this._listItemNode.addEventListener("dblclick", WI.TreeElement.treeElementDoubleClicked);
 
@@ -279,20 +284,6 @@ WI.TreeElement = class TreeElement extends WI.Object
             this.treeOutline.soon.updateVirtualizedElements();
     }
 
-    static treeElementMouseDown(event)
-    {
-        var element = event.currentTarget;
-        if (!element || !element.treeElement || !element.treeElement.selectable)
-            return;
-
-        if (element.treeElement.isEventWithinDisclosureTriangle(event)) {
-            event.preventDefault();
-            return;
-        }
-
-        element.treeElement.selectOnMouseDown(event);
-    }
-
     static treeElementToggled(event)
     {
         let element = event.currentTarget;
@@ -526,29 +517,11 @@ WI.TreeElement = class TreeElement extends WI.Object
 
         treeOutline.processingSelectionChange = true;
 
-        // Prevent dispatching a SelectionDidChange event for the deselected element if
-        // it will be dispatched for the selected element.
-        if (!suppressOnSelect)
-            suppressOnDeselect = true;
-
-        let deselectedElement = treeOutline.selectedTreeElement;
-        if (!this.selected) {
-            if (treeOutline.selectedTreeElement)
-                treeOutline.selectedTreeElement.deselect(suppressOnDeselect);
-
-            this.selected = true;
-            treeOutline.selectedTreeElement = this;
-
-            if (this._listItemNode)
-                this._listItemNode.classList.add("selected");
-        }
-
-        if (!suppressOnSelect) {
-            if (this.onselect)
-                this.onselect(this, selectedByUser);
+        this.selected = true;
+        treeOutline.selectTreeElementInternal(this, suppressOnSelect, selectedByUser);
 
-            treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.SelectionDidChange, {selectedByUser});
-        }
+        if (!suppressOnSelect && this.onselect)
+            this.onselect(this, selectedByUser);
 
         treeOutline.processingSelectionChange = false;
 
@@ -571,17 +544,10 @@ WI.TreeElement = class TreeElement extends WI.Object
             return false;
 
         this.selected = false;
-        this.treeOutline.selectedTreeElement = null;
-
-        if (this._listItemNode)
-            this._listItemNode.classList.remove("selected");
-
-        if (!suppressOnDeselect) {
-            if (this.ondeselect)
-                this.ondeselect(this);
+        this.treeOutline.selectTreeElementInternal(null, suppressOnDeselect);
 
-            this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.SelectionDidChange);
-        }
+        if (!suppressOnDeselect && this.ondeselect)
+            this.ondeselect(this);
 
         return true;
     }
index 21ac3c2..da2d2bd 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007, 2013, 2015 Apple Inc.  All rights reserved.
+ * Copyright (C) 2007-2018 Apple Inc.  All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -37,7 +37,6 @@ WI.TreeOutline = class TreeOutline extends WI.Object
         this.element.addEventListener("contextmenu", this._handleContextmenu.bind(this));
 
         this.children = [];
-        this.selectedTreeElement = null;
         this._childrenListNode = this.element;
         this._childrenListNode.removeChildren();
         this._knownTreeElements = [];
@@ -55,6 +54,14 @@ WI.TreeOutline = class TreeOutline extends WI.Object
         this._customIndent = false;
         this._selectable = selectable;
 
+        this._cachedNumberOfDescendents = 0;
+        this._selectionController = new WI.SelectionController(this);
+        this._treeElementIndexCache = new Map;
+
+        this._itemWasSelectedByUser = false;
+        this._processingSelectionControllerSelectionDidChange = false;
+        this._suppressNextSelectionDidChangeEvent = false;
+
         this._virtualizedVisibleTreeElements = null;
         this._virtualizedAttachedTreeElements = null;
         this._virtualizedScrollContainer = null;
@@ -64,6 +71,7 @@ WI.TreeOutline = class TreeOutline extends WI.Object
 
         this._childrenListNode.tabIndex = 0;
         this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true);
+        this._childrenListNode.addEventListener("mousedown", this._handleMouseDown.bind(this));
 
         WI.TreeOutline._generateStyleRulesIfNeeded();
 
@@ -73,6 +81,28 @@ WI.TreeOutline = class TreeOutline extends WI.Object
 
     // Public
 
+    get allowsMultipleSelection()
+    {
+        return this._selectionController.allowsMultipleSelection;
+    }
+
+    set allowsMultipleSelection(flag)
+    {
+        this._selectionController.allowsMultipleSelection = flag;
+    }
+
+    get selectedTreeElement()
+    {
+        let selectedIndex = this._selectionController.lastSelectedItem;
+        return this._treeElementAtIndex(selectedIndex) || null;
+    }
+
+    set selectedTreeElement(treeElement)
+    {
+        let index = this._indexOfTreeElement(treeElement);
+        this._selectionController.selectItem(index);
+    }
+
     get hidden()
     {
         return this._hidden;
@@ -242,6 +272,9 @@ WI.TreeOutline = class TreeOutline extends WI.Object
 
         if (isFirstChild && this.expanded)
             this.expand();
+
+        let insertionIndex = this.treeOutline._indexOfTreeElement(child.previousSibling) || 0;
+        this.treeOutline._selectionController.didInsertItem(insertionIndex);
     }
 
     removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling)
@@ -272,6 +305,7 @@ WI.TreeOutline = class TreeOutline extends WI.Object
         if (treeOutline) {
             treeOutline._forgetTreeElement(child);
             treeOutline._forgetChildrenRecursive(child);
+            treeOutline._selectionController.didRemoveItem(childIndex);
         }
 
         child._detach();
@@ -378,6 +412,9 @@ WI.TreeOutline = class TreeOutline extends WI.Object
 
     _rememberTreeElement(element)
     {
+        this._treeElementIndexCache.clear();
+        this._cachedNumberOfDescendents++;
+
         if (!this._knownTreeElements[element.identifier])
             this._knownTreeElements[element.identifier] = [];
 
@@ -392,6 +429,9 @@ WI.TreeOutline = class TreeOutline extends WI.Object
 
     _forgetTreeElement(element)
     {
+        this._treeElementIndexCache.clear();
+        this._cachedNumberOfDescendents--;
+
         if (this.selectedTreeElement === element) {
             element.deselect(true);
             this.selectedTreeElement = null;
@@ -523,24 +563,15 @@ WI.TreeOutline = class TreeOutline extends WI.Object
         if (event.target !== this._childrenListNode)
             return;
 
-        if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
+        if (!this.selectedTreeElement || event.commandOrControlKey)
             return;
 
         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
 
         var handled = false;
         var nextSelectedElement;
-        if (event.keyIdentifier === "Up" && !event.altKey) {
-            nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
-            while (nextSelectedElement && !nextSelectedElement.selectable)
-                nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(true);
-            handled = nextSelectedElement ? true : false;
-        } else if (event.keyIdentifier === "Down" && !event.altKey) {
-            nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
-            while (nextSelectedElement && !nextSelectedElement.selectable)
-                nextSelectedElement = nextSelectedElement.traverseNextTreeElement(true);
-            handled = nextSelectedElement ? true : false;
-        } else if ((!isRTL && event.keyIdentifier === "Left") || (isRTL && event.keyIdentifier === "Right")) {
+
+        if ((!isRTL && event.keyIdentifier === "Left") || (isRTL && event.keyIdentifier === "Right")) {
             if (this.selectedTreeElement.expanded) {
                 if (event.altKey)
                     this.selectedTreeElement.collapseRecursively();
@@ -563,7 +594,12 @@ WI.TreeOutline = class TreeOutline extends WI.Object
                 handled = true;
             } else if (this.selectedTreeElement.hasChildren) {
                 handled = true;
-                if (!this.selectedTreeElement.expanded) {
+                if (this.selectedTreeElement.expanded) {
+                    nextSelectedElement = this.selectedTreeElement.children[0];
+                    while (nextSelectedElement && !nextSelectedElement.selectable)
+                        nextSelectedElement = nextSelectedElement.nextSibling;
+                    handled = nextSelectedElement ? true : false;
+                } else {
                     if (event.altKey)
                         this.selectedTreeElement.expandRecursively();
                     else
@@ -587,6 +623,9 @@ WI.TreeOutline = class TreeOutline extends WI.Object
                 handled = this.treeOutline.onspace(this.selectedTreeElement);
         }
 
+        if (!handled)
+            handled = this._selectionController.handleKeyDown(event);
+
         if (nextSelectedElement) {
             nextSelectedElement.reveal();
             nextSelectedElement.select(false, true);
@@ -757,8 +796,96 @@ WI.TreeOutline = class TreeOutline extends WI.Object
             this._virtualizedScrollContainer.scrollTop = (firstItem + extraRows) * this._virtualizedTreeItemHeight;
     }
 
+    // SelectionController delegate
+
+    selectionControllerNumberOfItems(controller)
+    {
+        return this._cachedNumberOfDescendents;
+    }
+
+    selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems)
+    {
+        this._processingSelectionControllerSelectionDidChange = true;
+
+        for (let index of deselectedItems) {
+            let treeElement = this._treeElementAtIndex(index);
+            console.assert(treeElement, "Missing TreeElement for deselected index " + index);
+            if (treeElement) {
+                treeElement.listItemElement.classList.remove("selected");
+                if (!this._suppressNextSelectionDidChangeEvent)
+                    treeElement.deselect();
+            }
+        }
+
+        for (let index of selectedItems) {
+            let treeElement = this._treeElementAtIndex(index);
+            console.assert(treeElement, "Missing TreeElement for selected index " + index);
+            if (treeElement) {
+                treeElement.listItemElement.classList.add("selected");
+                if (!this._suppressNextSelectionDidChangeEvent)
+                    treeElement.select();
+            }
+        }
+
+        this._processingSelectionControllerSelectionDidChange = false;
+
+        this._dispatchSelectionDidChangeEvent();
+    }
+
+    selectionControllerNextSelectableIndex(controller, index)
+    {
+        let treeElement = this._treeElementAtIndex(index);
+        if (!treeElement)
+            return NaN;
+
+        const skipUnrevealed = true;
+        const stayWithin = null;
+        const dontPopulate = true;
+
+        while (treeElement = treeElement.traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate)) {
+            if (treeElement.selectable)
+                return this._indexOfTreeElement(treeElement);
+        }
+
+        return NaN;
+    }
+
+    selectionControllerPreviousSelectableIndex(controller, index)
+    {
+        let treeElement = this._treeElementAtIndex(index);
+        if (!treeElement)
+            return NaN;
+
+        const skipUnrevealed = true;
+        const stayWithin = null;
+        const dontPopulate = true;
+
+        while (treeElement = treeElement.traversePreviousTreeElement(skipUnrevealed, stayWithin, dontPopulate)) {
+            if (treeElement.selectable)
+                return this._indexOfTreeElement(treeElement);
+        }
+
+        return NaN;
+    }
+
     // Protected
 
+    selectTreeElementInternal(treeElement, suppressNotification = false, selectedByUser = false)
+    {
+        if (this._processingSelectionControllerSelectionDidChange)
+            return;
+
+        this._itemWasSelectedByUser = selectedByUser;
+        this._suppressNextSelectionDidChangeEvent = suppressNotification;
+
+        if (this.allowsRepeatSelection && this.selectedTreeElement === treeElement) {
+            this._dispatchSelectionDidChangeEvent();
+            return;
+        }
+
+        this.selectedTreeElement = treeElement;
+    }
+
     treeElementFromEvent(event)
     {
         let scrollContainer = this.element.parentElement;
@@ -855,6 +982,93 @@ WI.TreeOutline = class TreeOutline extends WI.Object
         let contextMenu = WI.ContextMenu.createFromEvent(event);
         this.populateContextMenu(contextMenu, event, treeElement);
     }
+
+    _handleMouseDown(event)
+    {
+        let treeElement = this.treeElementFromEvent(event);
+        if (!treeElement || !treeElement.selectable)
+            return;
+
+        if (treeElement.isEventWithinDisclosureTriangle(event)) {
+            event.preventDefault();
+            return;
+        }
+
+        if (!treeElement.canSelectOnMouseDown(event)) {
+            event.preventDefault();
+            return;
+        }
+
+        let index = this._indexOfTreeElement(treeElement);
+        if (isNaN(index))
+            return;
+
+        this._selectionController.handleItemMouseDown(index, event);
+    }
+
+    _indexOfTreeElement(treeElement)
+    {
+        function previousElement(element) {
+            if (element.previousSibling) {
+                element = element.previousSibling;
+                if (element.children.length)
+                    element = element.children.lastValue;
+            } else
+                element = element.parent && element.parent.root ? null : element.parent;
+            return element;
+        }
+
+        let index = 0;
+        let current = treeElement;
+        while (current) {
+            let closestIndex = this._treeElementIndexCache.get(current);
+            if (!isNaN(closestIndex)) {
+                index += closestIndex;
+                break;
+            }
+
+            current = previousElement(current);
+            if (current)
+                index++;
+        }
+
+        if (!this._treeElementIndexCache.has(treeElement))
+            this._treeElementIndexCache.set(treeElement, index);
+
+        return index;
+    }
+
+    _treeElementAtIndex(index)
+    {
+        const skipUnrevealed = false;
+        const stayWithin = null;
+        const dontPopulate = true;
+
+        let current = 0;
+        let treeElement = this.children[0];
+        while (treeElement) {
+            if (current === index)
+                return treeElement;
+
+            treeElement = treeElement.traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate);
+            ++current;
+        }
+
+        return null;
+    }
+
+    _dispatchSelectionDidChangeEvent()
+    {
+        let selectedByUser = this._itemWasSelectedByUser;
+        this._itemWasSelectedByUser = false;
+
+        if (this._suppressNextSelectionDidChangeEvent) {
+            this._suppressNextSelectionDidChangeEvent = false;
+            return;
+        }
+
+        this.dispatchEventToListeners(WI.TreeOutline.Event.SelectionDidChange, {selectedByUser});
+    }
 };
 
 WI.TreeOutline._styleElement = null;