Web Insspector: Storage: cannot select multiple local storage entries
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 24 Apr 2020 01:05:34 +0000 (01:05 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 24 Apr 2020 01:05:34 +0000 (01:05 +0000)
https://bugs.webkit.org/show_bug.cgi?id=210876

Reviewed by Brian Burg.

Source/WebInspectorUI:

Support multiple selection using `WI.DataGrid`.

* UserInterface/Views/DataGrid.js:
(WI.DataGrid):
(WI.DataGrid.prototype.get allowsMultipleSelection): Added.
(WI.DataGrid.prototype.set allowsMultipleSelection): Added.
(WI.DataGrid.prototype.get selectedNode):
(WI.DataGrid.prototype.set selectedNode):
(WI.DataGrid.prototype.get selectedDataGridNodes): Added.
(WI.DataGrid.prototype._keyDown):
(WI.DataGrid.prototype.selectNodes):
(WI.DataGrid.prototype._mouseDownInDataTable):
(WI.DataGrid.prototype._contextMenuInDataTable):
(WI.DataGrid.prototype.handleCopyEvent):
(WI.DataGrid.prototype._copyRow):
(WI.DataGrid.prototype._copyTable):
(WI.DataGrid.prototype._hasCopyableData):
(WI.DataGrid.prototype.selectDataGridNodeInternal): Added.
(WI.DataGrid.prototype.deselectDataGridNodeInternal): Added.
(WI.DataGrid.prototype._dispatchSelectedNodeChangedEvent): Added.
(WI.DataGrid.prototype.dataGridNodeForSelectionItem): Added.
(WI.DataGrid.prototype.selectionItemForDataGridNode): Added.
(WI.DataGrid.prototype.selectionControllerSelectionDidChange): Added.
(WI.DataGrid.prototype.selectionControllerFirstSelectableItem): Added.
(WI.DataGrid.prototype.selectionControllerLastSelectableItem): Added.
(WI.DataGrid.prototype.selectionControllerPreviousSelectableItem): Added.
(WI.DataGrid.prototype.selectionControllerNextSelectableItem): Added.
* UserInterface/Views/DataGridNode.js:
(WI.DataGridNode.prototype.select):
(WI.DataGridNode.prototype.deselect):
Replace `selectedNode` with a `WI.SelectionController` that behaves like a `WI.TreeOutline`.
Use the `WI.SelectionController.Operation` to ensure that `WI.PlaceholderDataGridNode` are
not selected unless directly chosen (i.e. not during shift selection or ⌘A). Add logic such
that `WI.PlaceholderDataGridNode` are not copied. Prefer `_rows` instead of `children` as
the latter is not sorted/filtered.

* UserInterface/Controllers/SelectionController.js:
(WI.SelectionController.createTreeComparator): Added.
(WI.SelectionController.createListComparator): Added.
Create `static` helper functions for common comparators.

(WI.SelectionController.prototype.deselectItem):
(WI.SelectionController.prototype.selectAll):
(WI.SelectionController.prototype.removeSelectedItems):
(WI.SelectionController.prototype.handleItemMouseDown):
(WI.SelectionController.prototype._selectItemsFromArrowKey):
(WI.SelectionController.prototype._firstSelectableItem):
(WI.SelectionController.prototype._lastSelectableItem):
(WI.SelectionController.prototype._previousSelectableItem):
(WI.SelectionController.prototype._nextSelectableItem):
(WI.SelectionController.prototype._addRange):
(WI.SelectionController.prototype._deleteRange):
Introduce a `WI.SelectionController.Operation` which is used to tell the `_delegate` about
why it's being asked for information.

* UserInterface/Views/DOMStorageContentView.js:
(WI.DOMStorageContentView):
(WI.DOMStorageContentView.prototype._deleteCallback):
Support multiple selection, including deleting multiple rows at once.

* UserInterface/Views/Table.js:
(WI.Table):
* UserInterface/Views/TreeOutline.js:
(WI.TreeOutline):
(WI.TreeOutline.prototype.selectionControllerNumberOfItems): Deleted.
Removed unused `selectionControllerNumberOfItems`.

* UserInterface/Views/ProfileView.js:
(WI.ProfileView):
(WI.ProfileView.prototype._dataGridNodeSelected):
Maintain a `_selectedDataGridNode` so that `oldSelectedNode` doesn't have to be included
when dispatching `WI.DataGrid.Event.SelectedNodeChanged`.

LayoutTests:

* inspector/tree-outline/selection-controller-tree-comparator.html: Added.
* inspector/tree-outline/selection-controller-tree-comparator-expected.txt: Added.

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

LayoutTests/ChangeLog
LayoutTests/inspector/tree-outline/selection-controller-tree-comparator-expected.txt [new file with mode: 0644]
LayoutTests/inspector/tree-outline/selection-controller-tree-comparator.html [new file with mode: 0644]
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Controllers/SelectionController.js
Source/WebInspectorUI/UserInterface/Views/DOMStorageContentView.js
Source/WebInspectorUI/UserInterface/Views/DataGrid.js
Source/WebInspectorUI/UserInterface/Views/DataGridNode.js
Source/WebInspectorUI/UserInterface/Views/ProfileView.js
Source/WebInspectorUI/UserInterface/Views/Table.js
Source/WebInspectorUI/UserInterface/Views/TreeOutline.js

index 7561cd8..13242cc 100644 (file)
@@ -1,3 +1,13 @@
+2020-04-23  Devin Rousso  <drousso@apple.com>
+
+        Web Insspector: Storage: cannot select multiple local storage entries
+        https://bugs.webkit.org/show_bug.cgi?id=210876
+
+        Reviewed by Brian Burg.
+
+        * inspector/tree-outline/selection-controller-tree-comparator.html: Added.
+        * inspector/tree-outline/selection-controller-tree-comparator-expected.txt: Added.
+
 2020-04-23  Alex Christensen  <achristensen@webkit.org>
 
         Allow credentials for same-origin css mask images
diff --git a/LayoutTests/inspector/tree-outline/selection-controller-tree-comparator-expected.txt b/LayoutTests/inspector/tree-outline/selection-controller-tree-comparator-expected.txt
new file mode 100644 (file)
index 0000000..c6405c3
--- /dev/null
@@ -0,0 +1,50 @@
+Tests for WI.SelectionController.createTreeComparator.
+
+
+== Running test suite: SelectionController.createTreeComparator
+-- Running test case: SelectionController.createTreeComparator.ValidTree
+Creating tree...
+Shuffling tree...
+Sorting tree...
+
+  root
+  1
+  1 > 1
+  1 > 1 > 1
+  1 > 1 > 2
+  1 > 1 > 3
+  1 > 2
+  1 > 2 > 1
+  1 > 2 > 2
+  1 > 2 > 3
+  1 > 3
+  1 > 3 > 1
+  1 > 3 > 2
+  1 > 3 > 3
+  2
+  2 > 1
+  2 > 1 > 1
+  2 > 1 > 2
+  2 > 1 > 3
+  2 > 2
+  2 > 2 > 1
+  2 > 2 > 2
+  2 > 2 > 3
+  2 > 3
+  2 > 3 > 1
+  2 > 3 > 2
+  2 > 3 > 3
+  3
+  3 > 1
+  3 > 1 > 1
+  3 > 1 > 2
+  3 > 1 > 3
+  3 > 2
+  3 > 2 > 1
+  3 > 2 > 2
+  3 > 2 > 3
+  3 > 3
+  3 > 3 > 1
+  3 > 3 > 2
+  3 > 3 > 3
+
diff --git a/LayoutTests/inspector/tree-outline/selection-controller-tree-comparator.html b/LayoutTests/inspector/tree-outline/selection-controller-tree-comparator.html
new file mode 100644 (file)
index 0000000..ea6b090
--- /dev/null
@@ -0,0 +1,161 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    function itemForRepresentedObject(x) {
+        return x;
+    }
+
+    function createTree(data) {
+        let node = {
+            ...data,
+            parent: null,
+            children: data.children || [],
+        };
+
+        node.children = node.children.map((childData) => {
+            let childNode = createTree(childData);
+            childNode.parent = node;
+            return childNode;
+        });
+
+        return node;
+    }
+
+    function flatten(node) {
+        return [node, ...node.children.flatMap(flatten)];
+    }
+
+    function shuffle(array) {
+        let shuffled = [];
+        while (array.length)
+            shuffled.push(array.splice(Math.floor(Math.random() * array.length), 1)[0]);
+        return shuffled;
+    }
+
+    let suite = InspectorTest.createSyncSuite("SelectionController.createTreeComparator");
+
+    suite.addTestCase({
+        name: "SelectionController.createTreeComparator.ValidTree",
+        description: "Check that createTreeComparator works with a valid tree.",
+        test() {
+            let comparator = WI.SelectionController.createTreeComparator(itemForRepresentedObject);
+
+            InspectorTest.log("Creating tree...");
+            let root = createTree({
+                id: "root",
+                children: [
+                    {
+                        id: "1",
+                        children: [
+                            {
+                                id: "1 > 1",
+                                children: [
+                                    {id: "1 > 1 > 1"},
+                                    {id: "1 > 1 > 2"},
+                                    {id: "1 > 1 > 3"},
+                                ],
+                            },
+                            {
+                                id: "1 > 2",
+                                children: [
+                                    {id: "1 > 2 > 1"},
+                                    {id: "1 > 2 > 2"},
+                                    {id: "1 > 2 > 3"},
+                                ],
+                            },
+                            {
+                                id: "1 > 3",
+                                children: [
+                                    {id: "1 > 3 > 1"},
+                                    {id: "1 > 3 > 2"},
+                                    {id: "1 > 3 > 3"},
+                                ],
+                            },
+                            
+                        ],
+                    },
+                    {
+                        id: "2",
+                        children: [
+                            {
+                                id: "2 > 1",
+                                children: [
+                                    {id: "2 > 1 > 1"},
+                                    {id: "2 > 1 > 2"},
+                                    {id: "2 > 1 > 3"},
+                                ],
+                            },
+                            {
+                                id: "2 > 2",
+                                children: [
+                                    {id: "2 > 2 > 1"},
+                                    {id: "2 > 2 > 2"},
+                                    {id: "2 > 2 > 3"},
+                                ],
+                            },
+                            {
+                                id: "2 > 3",
+                                children: [
+                                    {id: "2 > 3 > 1"},
+                                    {id: "2 > 3 > 2"},
+                                    {id: "2 > 3 > 3"},
+                                ],
+                            },
+                        ],
+                    },
+                    {
+                        id: "3",
+                        children: [
+                            {
+                                id: "3 > 1",
+                                children: [
+                                    {id: "3 > 1 > 1"},
+                                    {id: "3 > 1 > 2"},
+                                    {id: "3 > 1 > 3"},
+                                ],
+                            },
+                            {
+                                id: "3 > 2",
+                                children: [
+                                    {id: "3 > 2 > 1"},
+                                    {id: "3 > 2 > 2"},
+                                    {id: "3 > 2 > 3"},
+                                ],
+                            },
+                            {
+                                id: "3 > 3",
+                                children: [
+                                    {id: "3 > 3 > 1"},
+                                    {id: "3 > 3 > 2"},
+                                    {id: "3 > 3 > 3"},
+                                ],
+                            },
+                        ],
+                    },
+                ],
+            });
+
+            InspectorTest.log("Shuffling tree...");
+            let nodes = shuffle(flatten(root));
+
+            InspectorTest.log("Sorting tree...");
+            nodes.sort(comparator);
+
+            InspectorTest.newline();
+            for (let node of nodes)
+                InspectorTest.log("  " + node.id);
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onLoad="runTest()">
+    <p>Tests for WI.SelectionController.createTreeComparator.</p>
+</body>
+</html>
index dd70c02..44780dc 100644 (file)
@@ -1,5 +1,85 @@
 2020-04-23  Devin Rousso  <drousso@apple.com>
 
+        Web Insspector: Storage: cannot select multiple local storage entries
+        https://bugs.webkit.org/show_bug.cgi?id=210876
+
+        Reviewed by Brian Burg.
+
+        Support multiple selection using `WI.DataGrid`.
+
+        * UserInterface/Views/DataGrid.js:
+        (WI.DataGrid):
+        (WI.DataGrid.prototype.get allowsMultipleSelection): Added.
+        (WI.DataGrid.prototype.set allowsMultipleSelection): Added.
+        (WI.DataGrid.prototype.get selectedNode):
+        (WI.DataGrid.prototype.set selectedNode):
+        (WI.DataGrid.prototype.get selectedDataGridNodes): Added.
+        (WI.DataGrid.prototype._keyDown):
+        (WI.DataGrid.prototype.selectNodes):
+        (WI.DataGrid.prototype._mouseDownInDataTable):
+        (WI.DataGrid.prototype._contextMenuInDataTable):
+        (WI.DataGrid.prototype.handleCopyEvent):
+        (WI.DataGrid.prototype._copyRow):
+        (WI.DataGrid.prototype._copyTable):
+        (WI.DataGrid.prototype._hasCopyableData):
+        (WI.DataGrid.prototype.selectDataGridNodeInternal): Added.
+        (WI.DataGrid.prototype.deselectDataGridNodeInternal): Added.
+        (WI.DataGrid.prototype._dispatchSelectedNodeChangedEvent): Added.
+        (WI.DataGrid.prototype.dataGridNodeForSelectionItem): Added.
+        (WI.DataGrid.prototype.selectionItemForDataGridNode): Added.
+        (WI.DataGrid.prototype.selectionControllerSelectionDidChange): Added.
+        (WI.DataGrid.prototype.selectionControllerFirstSelectableItem): Added.
+        (WI.DataGrid.prototype.selectionControllerLastSelectableItem): Added.
+        (WI.DataGrid.prototype.selectionControllerPreviousSelectableItem): Added.
+        (WI.DataGrid.prototype.selectionControllerNextSelectableItem): Added.
+        * UserInterface/Views/DataGridNode.js:
+        (WI.DataGridNode.prototype.select):
+        (WI.DataGridNode.prototype.deselect):
+        Replace `selectedNode` with a `WI.SelectionController` that behaves like a `WI.TreeOutline`.
+        Use the `WI.SelectionController.Operation` to ensure that `WI.PlaceholderDataGridNode` are
+        not selected unless directly chosen (i.e. not during shift selection or ⌘A). Add logic such
+        that `WI.PlaceholderDataGridNode` are not copied. Prefer `_rows` instead of `children` as
+        the latter is not sorted/filtered.
+
+        * UserInterface/Controllers/SelectionController.js:
+        (WI.SelectionController.createTreeComparator): Added.
+        (WI.SelectionController.createListComparator): Added.
+        Create `static` helper functions for common comparators.
+
+        (WI.SelectionController.prototype.deselectItem):
+        (WI.SelectionController.prototype.selectAll):
+        (WI.SelectionController.prototype.removeSelectedItems):
+        (WI.SelectionController.prototype.handleItemMouseDown):
+        (WI.SelectionController.prototype._selectItemsFromArrowKey):
+        (WI.SelectionController.prototype._firstSelectableItem):
+        (WI.SelectionController.prototype._lastSelectableItem):
+        (WI.SelectionController.prototype._previousSelectableItem):
+        (WI.SelectionController.prototype._nextSelectableItem):
+        (WI.SelectionController.prototype._addRange):
+        (WI.SelectionController.prototype._deleteRange):
+        Introduce a `WI.SelectionController.Operation` which is used to tell the `_delegate` about
+        why it's being asked for information.
+
+        * UserInterface/Views/DOMStorageContentView.js:
+        (WI.DOMStorageContentView):
+        (WI.DOMStorageContentView.prototype._deleteCallback):
+        Support multiple selection, including deleting multiple rows at once.
+
+        * UserInterface/Views/Table.js:
+        (WI.Table):
+        * UserInterface/Views/TreeOutline.js:
+        (WI.TreeOutline):
+        (WI.TreeOutline.prototype.selectionControllerNumberOfItems): Deleted.
+        Removed unused `selectionControllerNumberOfItems`.
+
+        * UserInterface/Views/ProfileView.js:
+        (WI.ProfileView):
+        (WI.ProfileView.prototype._dataGridNodeSelected):
+        Maintain a `_selectedDataGridNode` so that `oldSelectedNode` doesn't have to be included
+        when dispatching `WI.DataGrid.Event.SelectedNodeChanged`.
+
+2020-04-23  Devin Rousso  <drousso@apple.com>
+
         Web Inspector: REGRESSION: Elements: Styles: color functions are missing swatches
         https://bugs.webkit.org/show_bug.cgi?id=210930
 
index dbb0ae7..b0b2ea1 100644 (file)
@@ -48,6 +48,64 @@ WI.SelectionController = class SelectionController extends WI.Object
         console.assert(this._delegate.selectionControllerPreviousSelectableItem, "SelectionController delegate must implement selectionControllerPreviousSelectableItem.");
     }
 
+    // Static
+
+    static createTreeComparator(itemForRepresentedObject)
+    {
+        return (a, b) => {
+            a = itemForRepresentedObject(a);
+            b = itemForRepresentedObject(b);
+            if (!a || !b)
+                return 0;
+
+            let getLevel = (item) => {
+                let level = 0;
+                while (item = item.parent)
+                    level++;
+                return level;
+            };
+
+            let compareSiblings = (s, t) => {
+                return s.parent.children.indexOf(s) - s.parent.children.indexOf(t);
+            };
+
+            if (a.parent === b.parent)
+                return compareSiblings(a, b);
+
+            let aLevel = getLevel(a);
+            let bLevel = getLevel(b);
+            while (aLevel > bLevel) {
+                if (a.parent === b)
+                    return 1;
+                a = a.parent;
+                aLevel--;
+            }
+            while (bLevel > aLevel) {
+                if (b.parent === a)
+                    return -1;
+                b = b.parent;
+                bLevel--;
+            }
+
+            while (a.parent !== b.parent) {
+                a = a.parent;
+                b = b.parent;
+            }
+
+            console.assert(a.parent === b.parent, "Missing common ancestor.", a, b);
+            return compareSiblings(a, b);
+        };
+    }
+
+    static createListComparator(indexForRepresentedObject)
+    {
+        console.assert(indexForRepresentedObject);
+
+        return (a, b) => {
+            return indexForRepresentedObject(a) - indexForRepresentedObject(b);
+        };
+    }
+
     // Public
 
     get delegate() { return this._delegate; }
@@ -129,17 +187,21 @@ WI.SelectionController = class SelectionController extends WI.Object
             this._lastSelectedItem = null;
 
             if (newItems.size) {
+                console.assert(this._allowsMultipleSelection);
+
+                const operation = WI.SelectionController.Operation.Extend;
+
                 // Find selected item closest to deselected item.
                 let previous = item;
                 let next = item;
                 while (!this._lastSelectedItem && previous && next) {
-                    previous = this._previousSelectableItem(previous);
+                    previous = this._previousSelectableItem(previous, operation);
                     if (this.hasSelectedItem(previous)) {
                         this._lastSelectedItem = previous;
                         break;
                     }
 
-                    next = this._nextSelectableItem(next);
+                    next = this._nextSelectableItem(next, operation);
                     if (this.hasSelectedItem(next)) {
                         this._lastSelectedItem = next;
                         break;
@@ -159,10 +221,10 @@ WI.SelectionController = class SelectionController extends WI.Object
         if (!this._allowsMultipleSelection)
             return;
 
-        this.reset();
+        const operation = WI.SelectionController.Operation.Extend;
 
         let newItems = new Set;
-        this._addRange(newItems, this._firstSelectableItem(), this._lastSelectableItem());
+        this._addRange(newItems, this._firstSelectableItem(operation), this._lastSelectableItem(operation));
         this.selectItems(newItems);
     }
 
@@ -176,22 +238,24 @@ WI.SelectionController = class SelectionController extends WI.Object
         if (!this._selectedItems.size)
             return;
 
+        let operation = this._allowsMultipleSelection ? WI.SelectionController.Operation.Extend : WI.SelectionController.Operation.Direct;
+
         let orderedSelection = Array.from(this._selectedItems).sort(this._comparator);
 
         // Try selecting the item preceding the selection.
         let firstSelectedItem = orderedSelection[0];
-        let itemToSelect = this._previousSelectableItem(firstSelectedItem);
+        let itemToSelect = this._previousSelectableItem(firstSelectedItem, operation);
         if (!itemToSelect) {
             // If no item exists before the first item in the selection, try selecting
             // a deselected item (hole) within the selection.
             itemToSelect = firstSelectedItem;
             while (itemToSelect && this.hasSelectedItem(itemToSelect))
-                itemToSelect = this._nextSelectableItem(itemToSelect);
+                itemToSelect = this._nextSelectableItem(itemToSelect, operation);
 
             if (!itemToSelect || this.hasSelectedItem(itemToSelect)) {
                 // If the selection contains no holes, try selecting the item
                 // following the selection.
-                itemToSelect = this._nextSelectableItem(orderedSelection.lastValue);
+                itemToSelect = this._nextSelectableItem(orderedSelection.lastValue, operation);
             }
         }
 
@@ -265,7 +329,7 @@ WI.SelectionController = class SelectionController extends WI.Object
         // through the clicked item to be selected.
         if (!newItems.size) {
             this._lastSelectedItem = item;
-            this._shiftAnchorItem = this._firstSelectableItem();
+            this._shiftAnchorItem = this._firstSelectableItem(WI.SelectionController.Operation.Extend);
 
             this._addRange(newItems, this._shiftAnchorItem, this._lastSelectedItem);
             this._updateSelectedItems(newItems);
@@ -320,16 +384,18 @@ WI.SelectionController = class SelectionController extends WI.Object
 
     _selectItemsFromArrowKey(goingUp, shiftKey)
     {
+        let extendSelection = shiftKey && this._allowsMultipleSelection;
+        let operation = extendSelection ? WI.SelectionController.Operation.Extend : WI.SelectionController.Operation.Direct;
+
         if (!this._selectedItems.size) {
-            this.selectItem(goingUp ? this._lastSelectableItem() : this._firstSelectableItem());
+            this.selectItem(goingUp ? this._lastSelectableItem(operation) : this._firstSelectableItem(operation));
             return;
         }
 
-        let item = goingUp ? this._previousSelectableItem(this._lastSelectedItem) : this._nextSelectableItem(this._lastSelectedItem);
+        let item = goingUp ? this._previousSelectableItem(this._lastSelectedItem, operation) : this._nextSelectableItem(this._lastSelectedItem, operation);
         if (!item)
             return;
 
-        let extendSelection = shiftKey && this._allowsMultipleSelection;
         if (!extendSelection || !this.hasSelectedItem(item)) {
             this.selectItem(item, extendSelection);
             return;
@@ -338,7 +404,7 @@ WI.SelectionController = class SelectionController extends WI.Object
         // Since the item in the direction of movement is selected, we are either
         // extending the selection into the item, or deselecting. Determine which
         // by checking whether the item opposite the anchor item is selected.
-        let priorItem = goingUp ? this._nextSelectableItem(this._lastSelectedItem) : this._previousSelectableItem(this._lastSelectedItem);
+        let priorItem = goingUp ? this._nextSelectableItem(this._lastSelectedItem, operation) : this._previousSelectableItem(this._lastSelectedItem, operation);
         if (!priorItem || !this.hasSelectedItem(priorItem)) {
             this.deselectItem(this._lastSelectedItem);
             return;
@@ -354,28 +420,28 @@ WI.SelectionController = class SelectionController extends WI.Object
             }
 
             this._lastSelectedItem = item;
-            item = goingUp ? this._previousSelectableItem(item) : this._nextSelectableItem(item);
+            item = goingUp ? this._previousSelectableItem(item, operation) : this._nextSelectableItem(item, operation);
         }
     }
 
-    _firstSelectableItem()
+    _firstSelectableItem(operation)
     {
-        return this._delegate.selectionControllerFirstSelectableItem(this);
+        return this._delegate.selectionControllerFirstSelectableItem(this, operation);
     }
 
-    _lastSelectableItem()
+    _lastSelectableItem(operation)
     {
-        return this._delegate.selectionControllerLastSelectableItem(this);
+        return this._delegate.selectionControllerLastSelectableItem(this, operation);
     }
 
-    _previousSelectableItem(item)
+    _previousSelectableItem(item, operation)
     {
-        return this._delegate.selectionControllerPreviousSelectableItem(this, item);
+        return this._delegate.selectionControllerPreviousSelectableItem(this, item, operation);
     }
 
-    _nextSelectableItem(item)
+    _nextSelectableItem(item, operation)
     {
-        return this._delegate.selectionControllerNextSelectableItem(this, item);
+        return this._delegate.selectionControllerNextSelectableItem(this, item, operation);
     }
 
     _updateSelectedItems(items)
@@ -394,12 +460,16 @@ WI.SelectionController = class SelectionController extends WI.Object
 
     _addRange(items, firstItem, lastItem)
     {
+        console.assert(this._allowsMultipleSelection);
+
+        const operation = WI.SelectionController.Operation.Extend;
+
         let current = firstItem;
         while (current) {
             items.add(current);
             if (current === lastItem)
                 break;
-            current = this._nextSelectableItem(current);
+            current = this._nextSelectableItem(current, operation);
         }
 
         console.assert(!lastItem || items.has(lastItem), "End of range could not be reached.");
@@ -407,14 +477,23 @@ WI.SelectionController = class SelectionController extends WI.Object
 
     _deleteRange(items, firstItem, lastItem)
     {
+        console.assert(this._allowsMultipleSelection);
+
+        const operation = WI.SelectionController.Operation.Extend;
+
         let current = firstItem;
         while (current) {
             items.delete(current);
             if (current === lastItem)
                 break;
-            current = this._nextSelectableItem(current);
+            current = this._nextSelectableItem(current, operation);
         }
 
         console.assert(!lastItem || !items.has(lastItem), "End of range could not be reached.");
     }
 };
+
+WI.SelectionController.Operation = {
+    Direct: Symbol("selection-operation-direct"),
+    Extend: Symbol("selection-operation-extend"),
+};
index 57599a7..35e4bb4 100644 (file)
@@ -48,6 +48,7 @@ WI.DOMStorageContentView = class DOMStorageContentView extends WI.ContentView
         });
         this._dataGrid.sortOrder = WI.DataGrid.SortOrder.Ascending;
         this._dataGrid.sortColumnIdentifier = "key";
+        this._dataGrid.allowsMultipleSelection = true;
         this._dataGrid.createSettings("dom-storage-content-view");
         this._dataGrid.addEventListener(WI.DataGrid.Event.SortChanged, this._sortDataGrid, this);
         this.addSubview(this._dataGrid);
@@ -193,13 +194,14 @@ WI.DOMStorageContentView = class DOMStorageContentView extends WI.ContentView
         this._dataGrid.sortNodesImmediately(comparator);
     }
 
-    _deleteCallback(node)
+    _deleteCallback()
     {
-        if (!node || node.isPlaceholderNode)
-            return;
-
-        this._dataGrid.removeChild(node);
-        this.representedObject.removeItem(node.data["key"]);
+        for (let dataGridNode of this._dataGrid.selectedDataGridNodes) {
+            if (dataGridNode.isPlaceholderNode)
+                continue;
+            this._dataGrid.removeChild(dataGridNode);
+            this.representedObject.removeItem(dataGridNode.data["key"]);
+        }
     }
 
     _editingCallback(editingNode, columnIdentifier, oldText, newText, moveDirection)
index bc5ba75..29bfba5 100644 (file)
@@ -44,7 +44,6 @@ WI.DataGrid = class DataGrid extends WI.View
         this._rows = [];
 
         this.children = [];
-        this.selectedNode = null;
         this.expandNodesWhenArrowing = false;
         this.root = true;
         this.hasChildren = false;
@@ -68,6 +67,13 @@ WI.DataGrid = class DataGrid extends WI.View
         this._filterDelegate = null;
         this._filterDidModifyNodeWhileProcessingItems = false;
 
+        let itemForRepresentedObject = this.dataGridNodeForSelectionItem.bind(this);
+        let selectionComparator = WI.SelectionController.createTreeComparator(itemForRepresentedObject);
+        this._selectionController = new WI.SelectionController(this, selectionComparator);
+
+        this._processingSelectionChange = false;
+        this._suppressNextSelectionDidChangeEvent = false;
+
         this.element.className = "data-grid";
         this.element.tabIndex = 0;
         this.element.addEventListener("keydown", this._keyDown.bind(this), false);
@@ -318,6 +324,45 @@ WI.DataGrid = class DataGrid extends WI.View
         this._updateScrollListeners();
     }
 
+    get allowsMultipleSelection()
+    {
+        return this._selectionController.allowsMultipleSelection;
+    }
+
+    set allowsMultipleSelection(flag)
+    {
+        this._selectionController.allowsMultipleSelection = flag;
+    }
+
+    get selectedNode()
+    {
+        return this.dataGridNodeForSelectionItem(this._selectionController.lastSelectedItem);
+    }
+
+    set selectedNode(dataGridNode)
+    {
+        if (dataGridNode)
+            this._selectionController.selectItem(this.selectionItemForDataGridNode(dataGridNode));
+        else
+            this._selectionController.deselectAll();
+    }
+
+    get selectedDataGridNodes()
+    {
+        if (this.allowsMultipleSelection) {
+            let selectedDataGridNodes = [];
+            for (let item of this._selectionController.selectedItems)
+                selectedDataGridNodes.push(this.dataGridNodeForSelectionItem(item));
+            return selectedDataGridNodes;
+        }
+
+        let selectedNode = this.selectedNode;
+        if (selectedNode)
+            return [selectedNode];
+
+        return [];
+    }
+
     get filterText() { return this._filterText; }
 
     set filterText(x)
@@ -1366,64 +1411,70 @@ WI.DataGrid = class DataGrid extends WI.View
 
     _keyDown(event)
     {
-        if (!this.selectedNode || event.shiftKey || event.metaKey || event.ctrlKey || this._editing)
+        if (this._editing)
             return;
 
-        let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
+        let isRTL = WI.resolveLayoutDirectionForElement(this.element) === WI.LayoutDirection.RTL;
+        let expandKeyIdentifier = isRTL ? "Left" : "Right";
+        let collapseKeyIdentifier = isRTL ? "Right" : "Left";
 
         var handled = false;
         var nextSelectedNode;
-        if (event.keyIdentifier === "Up" && !event.altKey) {
-            nextSelectedNode = this.selectedNode.traversePreviousNode(true);
-            while (nextSelectedNode && !nextSelectedNode.selectable)
-                nextSelectedNode = nextSelectedNode.traversePreviousNode(true);
-            handled = nextSelectedNode ? true : false;
-        } else if (event.keyIdentifier === "Down" && !event.altKey) {
-            nextSelectedNode = this.selectedNode.traverseNextNode(true);
-            while (nextSelectedNode && !nextSelectedNode.selectable)
-                nextSelectedNode = nextSelectedNode.traverseNextNode(true);
-            handled = nextSelectedNode ? true : false;
-        } else if ((!isRTL && event.keyIdentifier === "Left") || (isRTL && event.keyIdentifier === "Right")) {
-            if (this.selectedNode.expanded) {
-                if (event.altKey)
-                    this.selectedNode.collapseRecursively();
-                else
-                    this.selectedNode.collapse();
-                handled = true;
-            } else if (this.selectedNode.parent && !this.selectedNode.parent.root) {
-                handled = true;
-                if (this.selectedNode.parent.selectable) {
-                    nextSelectedNode = this.selectedNode.parent;
-                    handled = nextSelectedNode ? true : false;
-                } else if (this.selectedNode.parent)
-                    this.selectedNode.parent.collapse();
-            }
-        } else if ((!isRTL && event.keyIdentifier === "Right") || (isRTL && event.keyIdentifier === "Left")) {
-            if (!this.selectedNode.revealed) {
-                this.selectedNode.reveal();
-                handled = true;
-            } else if (this.selectedNode.hasChildren) {
-                handled = true;
+
+        if (this.selectedNode) {
+            if (event.keyIdentifier === collapseKeyIdentifier) {
                 if (this.selectedNode.expanded) {
-                    nextSelectedNode = this.selectedNode.children[0];
-                    handled = nextSelectedNode ? true : false;
-                } else {
                     if (event.altKey)
-                        this.selectedNode.expandRecursively();
+                        this.selectedNode.collapseRecursively();
                     else
-                        this.selectedNode.expand();
+                        this.selectedNode.collapse();
+                    handled = true;
+                } else if (this.selectedNode.parent && !this.selectedNode.parent.root) {
+                    handled = true;
+                    if (this.selectedNode.parent.selectable) {
+                        nextSelectedNode = this.selectedNode.parent;
+                        while (nextSelectedNode && !nextSelectedNode.selectable)
+                            nextSelectedNode = nextSelectedNode.parent;
+                        handled = !!nextSelectedNode;
+                    } else if (this.selectedNode.parent)
+                        this.selectedNode.parent.collapse();
+                }
+            } else if (event.keyIdentifier === expandKeyIdentifier) {
+                if (!this.selectedNode.revealed) {
+                    this.selectedNode.reveal();
+                    handled = true;
+                } else if (this.selectedNode.hasChildren) {
+                    handled = true;
+                    if (this.selectedNode.expanded) {
+                        nextSelectedNode = this.selectedNode.children[0];
+                        while (nextSelectedNode && !nextSelectedNode.selectable)
+                            nextSelectedNode = nextSelectedNode.nextSibling;
+                        handled = !!nextSelectedNode;
+                    } else {
+                        if (event.altKey)
+                            this.selectedNode.expandRecursively();
+                        else
+                            this.selectedNode.expand();
+                    }
+                }
+            } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */) {
+                if (this._deleteCallback) {
+                    handled = true;
+                    this._deleteCallback(this.selectedNode);
+                }
+            } else if (isEnterKey(event)) {
+                if (this._editCallback) {
+                    handled = true;
+                    this._startEditing(this.selectedNode.element.children[0]);
                 }
             }
-        } else if (event.keyCode === 8 || event.keyCode === 46) {
-            if (this._deleteCallback) {
-                handled = true;
-                this._deleteCallback(this.selectedNode);
-            }
-        } else if (isEnterKey(event)) {
-            if (this._editCallback) {
-                handled = true;
-                this._startEditing(this.selectedNode.element.children[0]);
-            }
+        }
+
+        if (!handled) {
+            handled = this._selectionController.handleKeyDown(event);
+
+            if (handled)
+                nextSelectedNode = this.selectedNode;
         }
 
         if (nextSelectedNode) {
@@ -1462,6 +1513,24 @@ WI.DataGrid = class DataGrid extends WI.View
         // This is the root, do nothing.
     }
 
+    selectNodes(nodes)
+    {
+        if (!nodes.length)
+            return;
+
+        if (nodes.length === 1) {
+            this.selectedNode = nodes[0];
+            return;
+        }
+
+        console.assert(this.allowsMultipleSelection, "Cannot select multiple DataGridNode with multiple selection disabled.");
+        if (!this.allowsMultipleSelection)
+            return;
+
+        let selectableObjects = nodes.map((node) => this.selectionItemForDataGridNode(node));
+        this._selectionController.selectItems(new Set(selectableObjects));
+    }
+
     dataGridNodeFromNode(target)
     {
         var rowElement = target.closest("tr");
@@ -1578,23 +1647,16 @@ WI.DataGrid = class DataGrid extends WI.View
 
     _mouseDownInDataTable(event)
     {
-        var gridNode = this.dataGridNodeFromNode(event.target);
-        if (!gridNode) {
-            if (this.selectedNode)
-                this.selectedNode.deselect();
+        let dataGridNode = this.dataGridNodeFromNode(event.target);
+        if (!dataGridNode) {
+            this._selectionController.deselectAll();
             return;
         }
 
-        if (!gridNode.selectable || gridNode.isEventWithinDisclosureTriangle(event))
+        if (!dataGridNode.selectable || dataGridNode.isEventWithinDisclosureTriangle(event))
             return;
 
-        if (event.metaKey) {
-            if (gridNode.selected)
-                gridNode.deselect();
-            else
-                gridNode.select();
-        } else
-            gridNode.select();
+        this._selectionController.handleItemMouseDown(this.selectionItemForDataGridNode(dataGridNode), event);
     }
 
     _contextMenuInHeader(event)
@@ -1666,7 +1728,7 @@ WI.DataGrid = class DataGrid extends WI.View
 
         if (gridNode) {
             if (gridNode.selectable && gridNode.copyable && !gridNode.isEventWithinDisclosureTriangle(event)) {
-                contextMenu.appendItem(WI.UIString("Copy Row"), this._copyRow.bind(this, event.target));
+                contextMenu.appendItem(WI.UIString("Copy Row"), this._copyRow.bind(this, gridNode));
                 contextMenu.appendItem(WI.UIString("Copy Table"), this._copyTable.bind(this));
 
                 if (this.dataGrid._editCallback) {
@@ -1746,22 +1808,30 @@ WI.DataGrid = class DataGrid extends WI.View
 
     handleCopyEvent(event)
     {
-        if (!this.selectedNode || !window.getSelection().isCollapsed)
+        if (!window.getSelection().isCollapsed)
+            return;
+
+        let copyData = [];
+        for (let dataGridNode of this.selectedDataGridNodes) {
+            if (!dataGridNode.copyable || dataGridNode.isPlaceholderNode)
+                continue;
+            copyData.push(this._copyTextForDataGridNode(dataGridNode));
+        }
+
+        if (!copyData.length)
             return;
 
-        var copyText = this._copyTextForDataGridNode(this.selectedNode);
-        event.clipboardData.setData("text/plain", copyText);
+        event.clipboardData.setData("text/plain", copyData.join("\n"));
         event.stopPropagation();
         event.preventDefault();
     }
 
-    _copyRow(target)
+    _copyRow(dataGridNode)
     {
-        var gridNode = this.dataGridNodeFromNode(target);
-        if (!gridNode)
+        if (!dataGridNode.copyable || dataGridNode.isPlaceholderNode)
             return;
 
-        var copyText = this._copyTextForDataGridNode(gridNode);
+        let copyText = this._copyTextForDataGridNode(dataGridNode);
         InspectorFrontendHost.copyText(copyText);
     }
 
@@ -1769,19 +1839,28 @@ WI.DataGrid = class DataGrid extends WI.View
     {
         let copyData = [];
         copyData.push(this._copyTextForDataGridHeaders());
-        for (let gridNode of this.children) {
-            if (!gridNode.copyable)
+        for (let dataGridNode of this._rows) {
+            if (!dataGridNode.copyable || dataGridNode.isPlaceholderNode)
                 continue;
-            copyData.push(this._copyTextForDataGridNode(gridNode));
+            copyData.push(this._copyTextForDataGridNode(dataGridNode));
         }
 
+        if (!copyData.length)
+            return;
+
         InspectorFrontendHost.copyText(copyData.join("\n"));
     }
 
     _hasCopyableData()
     {
-        let gridNode = this.children[0];
-        return gridNode && gridNode.selectable && gridNode.copyable;
+        const skipHidden = true;
+        const stayWithin = null;
+        const dontPopulate = true;
+
+        let dataGridNode = this._rows[0];
+        while (dataGridNode && (!dataGridNode.selectable || !dataGridNode.copyable || dataGridNode.isPlaceholderNode))
+            dataGridNode = dataGridNode.traverseNextNode(skipHidden, stayWithin, dontPopulate);
+        return !!dataGridNode;
     }
 
     resizerDragStarted(resizer)
@@ -1904,6 +1983,36 @@ WI.DataGrid = class DataGrid extends WI.View
         this._applyFilterToNodesTask.start();
     }
 
+    selectDataGridNodeInternal(dataGridNode, suppressSelectedEvent)
+    {
+        if (this._processingSelectionChange)
+            return;
+
+        this._suppressNextSelectionDidChangeEvent = suppressSelectedEvent;
+
+        this._selectionController.selectItem(this.selectionItemForDataGridNode(dataGridNode));
+    }
+
+    deselectDataGridNodeInternal(dataGridNode, suppressDeselectedEvent)
+    {
+        if (this._processingSelectionChange)
+            return;
+
+        this._suppressNextSelectionDidChangeEvent = suppressDeselectedEvent;
+
+        this._selectionController.deselectItem(this.selectionItemForDataGridNode(dataGridNode));
+    }
+
+    _dispatchSelectedNodeChangedEvent()
+    {
+        if (this._suppressNextSelectionDidChangeEvent) {
+            this._suppressNextSelectionDidChangeEvent = false;
+            return;
+        }
+
+        this.dispatchEventToListeners(WI.DataGrid.Event.SelectedNodeChanged);
+    }
+
     // YieldableTask delegate
 
     yieldableTaskWillProcessItem(task, node)
@@ -1927,6 +2036,95 @@ WI.DataGrid = class DataGrid extends WI.View
     {
         this._applyFilterToNodesTask = null;
     }
+
+    // SelectionController delegate
+
+    dataGridNodeForSelectionItem(item)
+    {
+        console.assert(!item || item instanceof WI.DataGridNode);
+        return item;
+    }
+
+    selectionItemForDataGridNode(dataGridNode)
+    {
+        console.assert(!dataGridNode || dataGridNode instanceof WI.DataGridNode);
+        return dataGridNode;
+    }
+
+    selectionControllerSelectionDidChange(selectionController, deselectedItems, selectedItems)
+    {
+        this._processingSelectionChange = true;
+
+        for (let item of deselectedItems) {
+            let dataGridNode = this.dataGridNodeForSelectionItem(item);
+            dataGridNode?.deselect();
+        }
+
+        for (let item of selectedItems) {
+            let dataGridNode = this.dataGridNodeForSelectionItem(item);
+            dataGridNode?.select();
+        }
+
+        this._dispatchSelectedNodeChangedEvent();
+
+        this._processingSelectionChange = false;
+    }
+
+    selectionControllerFirstSelectableItem(controller, operation)
+    {
+        let firstChild = this._rows[0];
+        let item = this.selectionItemForDataGridNode(firstChild);
+        if (firstChild.selectable && (!firstChild.isPlaceholderNode || operation === WI.SelectionController.Operation.Direct))
+            return item;
+        return this.selectionControllerNextSelectableItem(controller, item, operation);
+    }
+
+    selectionControllerLastSelectableItem(controller, operation)
+    {
+        let lastChild = this._rows.lastValue;
+        while (lastChild.expanded && lastChild.children.length)
+            lastChild = lastChild.children.lastValue;
+
+        let item = this.selectionItemForDataGridNode(lastChild);
+        if (lastChild.selectable && (!lastChild.isPlaceholderNode || operation === WI.SelectionController.Operation.Direct))
+            return item;
+        return this.selectionControllerPreviousSelectableItem(controller, item, operation);
+    }
+
+    selectionControllerPreviousSelectableItem(controller, item, operation)
+    {
+        let dataGridNode = this.dataGridNodeForSelectionItem(item);
+        console.assert(dataGridNode, "Missing DataGridNode for selection item.", item);
+        if (!dataGridNode)
+            return null;
+
+        const skipUnrevealed = true;
+        const dontPopulate = true;
+        while (dataGridNode = dataGridNode.traversePreviousNode(skipUnrevealed, dontPopulate)) {
+            if (dataGridNode.selectable && (!dataGridNode.isPlaceholderNode || operation === WI.SelectionController.Operation.Direct))
+                return this.selectionItemForDataGridNode(dataGridNode);
+        }
+
+        return null;
+    }
+
+    selectionControllerNextSelectableItem(controller, item, operation)
+    {
+        let dataGridNode = this.dataGridNodeForSelectionItem(item);
+        console.assert(dataGridNode, "Missing DataGridNode for selection item.", item);
+        if (!dataGridNode)
+            return null;
+
+        const skipUnrevealed = true;
+        const stayWithin = null;
+        const dontPopulate = true;
+        while (dataGridNode = dataGridNode.traverseNextNode(skipUnrevealed, stayWithin, dontPopulate)) {
+            if (dataGridNode.selectable && (!dataGridNode.isPlaceholderNode || operation === WI.SelectionController.Operation.Direct))
+                return this.selectionItemForDataGridNode(dataGridNode);
+        }
+
+        return null;
+    }
 };
 
 WI.DataGrid.Event = {
index 6406011..acf8de3 100644 (file)
@@ -566,18 +566,10 @@ WI.DataGridNode = class DataGridNode extends WI.Object
         if (!this.dataGrid || !this.selectable || this.selected)
             return;
 
-        let oldSelectedNode = this.dataGrid.selectedNode;
-        if (oldSelectedNode)
-            oldSelectedNode.deselect(true);
-
         this._selected = true;
-        this.dataGrid.selectedNode = this;
-
-        if (this._element)
-            this._element.classList.add("selected");
+        this._element?.classList.add("selected");
 
-        if (!suppressSelectedEvent)
-            this.dataGrid.dispatchEventToListeners(WI.DataGrid.Event.SelectedNodeChanged, {oldSelectedNode});
+        this.dataGrid.selectDataGridNodeInternal(this, suppressSelectedEvent);
     }
 
     revealAndSelect(suppressSelectedEvent)
@@ -588,17 +580,13 @@ WI.DataGridNode = class DataGridNode extends WI.Object
 
     deselect(suppressDeselectedEvent)
     {
-        if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
+        if (!this.dataGrid || !this.selectable || !this.selected)
             return;
 
         this._selected = false;
-        this.dataGrid.selectedNode = null;
-
-        if (this._element)
-            this._element.classList.remove("selected");
+        this._element?.classList.remove("selected");
 
-        if (!suppressDeselectedEvent)
-            this.dataGrid.dispatchEventToListeners(WI.DataGrid.Event.SelectedNodeChanged, {oldSelectedNode: this});
+        this.dataGrid.deselectDataGridNodeInternal(this, suppressDeselectedEvent);
     }
 
     traverseNextNode(skipHidden, stayWithin, dontPopulate, info)
index a23c515..224d051 100644 (file)
@@ -69,6 +69,8 @@ WI.ProfileView = class ProfileView extends WI.ContentView
         this._dataGrid.sortOrder = WI.DataGrid.SortOrder.Descending;
         this._dataGrid.createSettings("profile-view");
 
+        this._selectedDataGridNode = null;
+
         // Currently we create a new ProfileView for each CallingContextTree, so
         // to share state between them, use a common shared data object.
         this._sharedData = extraArguments;
@@ -211,18 +213,19 @@ WI.ProfileView = class ProfileView extends WI.ContentView
 
     _dataGridNodeSelected(event)
     {
-        let oldSelectedNode = event.data.oldSelectedNode;
-        if (oldSelectedNode) {
-            this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, oldSelectedNode);
-            oldSelectedNode.forEachChildInSubtree((node) => this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, node));
+        if (this._selectedDataGridNode) {
+            this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, this._selectedDataGridNode);
+            this._selectedDataGridNode.forEachChildInSubtree((node) => this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, node));
         }
 
-        let newSelectedNode = this._dataGrid.selectedNode;
-        if (newSelectedNode) {
-            this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, newSelectedNode);
-            newSelectedNode.forEachChildInSubtree((node) => this._appendGuidanceElement(WI.ProfileView.GuidanceType.Selected, node, newSelectedNode));
+        this._selectedDataGridNode = this._dataGrid.selectedNode;
+
+
+        if (this._selectedDataGridNode) {
+            this._removeGuidanceElement(WI.ProfileView.GuidanceType.Selected, this._selectedDataGridNode);
+            this._selectedDataGridNode.forEachChildInSubtree((node) => this._appendGuidanceElement(WI.ProfileView.GuidanceType.Selected, node, this._selectedDataGridNode));
 
-            this._sharedData.selectedNodeHash = newSelectedNode.callingContextTreeNode.hash;
+            this._sharedData.selectedNodeHash = this._selectedDataGridNode.callingContextTreeNode.hash;
         }
     }
 
index 8833df7..28f4326 100644 (file)
@@ -86,7 +86,8 @@ WI.Table = class Table extends WI.View
         this._columnWidths = null; // Calculated in _resizeColumnsAndFiller.
         this._fillerHeight = 0; // Calculated in _resizeColumnsAndFiller.
 
-        this._selectionController = new WI.SelectionController(this, (a, b) => this._indexForRepresentedObject(a) - this._indexForRepresentedObject(b));
+        let selectionComparator = WI.SelectionController.createListComparator(this._indexForRepresentedObject.bind(this));
+        this._selectionController = new WI.SelectionController(this, selectionComparator);
 
         this._resizers = [];
         this._currentResizer = null;
index 8120cd1..507a178 100644 (file)
@@ -57,53 +57,9 @@ WI.TreeOutline = class TreeOutline extends WI.Object
 
         this._cachedNumberOfDescendants = 0;
 
-        let comparator = (a, b) => {
-            function getLevel(treeElement) {
-                let level = 0;
-                while (treeElement = treeElement.parent)
-                    level++;
-                return level;
-            }
-
-            function compareSiblings(s, t) {
-                return s.parent.children.indexOf(s) - s.parent.children.indexOf(t);
-            }
-
-            // Translate represented objects to TreeElements, which have the
-            // hierarchical information needed to perform the comparison.
-            a = this.getCachedTreeElement(a);
-            b = this.getCachedTreeElement(b);
-            if (!a || !b)
-                return 0;
-
-            if (a.parent === b.parent)
-                return compareSiblings(a, b);
-
-            let aLevel = getLevel(a);
-            let bLevel = getLevel(b);
-            while (aLevel > bLevel) {
-                if (a.parent === b)
-                    return 1;
-                a = a.parent;
-                aLevel--;
-            }
-            while (bLevel > aLevel) {
-                if (b.parent === a)
-                    return -1;
-                b = b.parent;
-                bLevel--;
-            }
-
-            while (a.parent !== b.parent) {
-                a = a.parent;
-                b = b.parent;
-            }
-
-            console.assert(a.parent === b.parent, "Missing common ancestor for TreeElements.", a, b);
-            return compareSiblings(a, b);
-        };
-
-        this._selectionController = new WI.SelectionController(this, comparator);
+        let itemForRepresentedObject = this.getCachedTreeElement.bind(this);
+        let selectionComparator = WI.SelectionController.createTreeComparator(itemForRepresentedObject);
+        this._selectionController = new WI.SelectionController(this, selectionComparator);
 
         this._itemWasSelectedByUser = false;
         this._processingSelectionChange = false;
@@ -770,11 +726,6 @@ WI.TreeOutline = class TreeOutline extends WI.Object
 
     // SelectionController delegate
 
-    selectionControllerNumberOfItems(controller)
-    {
-        return this._cachedNumberOfDescendants;
-    }
-
     selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems)
     {
         this._processingSelectionChange = true;