Title: [236853] trunk
Revision
236853
Author
[email protected]
Date
2018-10-04 14:54:14 -0700 (Thu, 04 Oct 2018)

Log Message

Web Inspector: Table should support multiple selection and Cmd-click behavior
https://bugs.webkit.org/show_bug.cgi?id=189705
<rdar://problem/44571170>

Reviewed by Devin Rousso.

Source/WebInspectorUI:

Add multiple row selection to Table, with new methods for programmatic
selection (deselectRow, deselectAll), and Command-click support for
selecting/deselecting Table rows.

* UserInterface/Base/IndexSet.js: Added.
(WI.IndexSet):
(WI.IndexSet.prototype.get size):
(WI.IndexSet.prototype.get firstIndex):
(WI.IndexSet.prototype.get lastIndex):
(WI.IndexSet.prototype.add):
(WI.IndexSet.prototype.delete):
(WI.IndexSet.prototype.has):
(WI.IndexSet.prototype.clear):
(WI.IndexSet.prototype.indexGreaterThan):
(WI.IndexSet.prototype.indexLessThan):
(WI.IndexSet.prototype.Symbol.iterator):
(WI.IndexSet.prototype._indexClosestTo):
(WI.IndexSet.prototype._validateIndex):
Helper container for managing an ordered sequence of unique positive
integers, with set semantics, backed by a sorted array. Used by Table,
and eventually by TreeOutline.

* UserInterface/Main.html:
* UserInterface/Test.html:
* UserInterface/Test/Test.js:
New files and stubs to make Table layout tests possible.

* UserInterface/Views/NetworkTableContentView.js:
(WI.NetworkTableContentView.prototype.reset):
(WI.NetworkTableContentView.prototype.showRepresentedObject):
(WI.NetworkTableContentView.prototype.networkResourceDetailViewClose):
(WI.NetworkTableContentView.prototype.tableSelectionDidChange):
(WI.NetworkTableContentView.prototype._restoreSelectedRow):
(WI.NetworkTableContentView.prototype.tableSelectedRowChanged): Deleted.
Replace uses of `clearSelectedRow` with `deselectAll`, and updated
selection changed delegate.

* UserInterface/Views/Table.css:
(.table > .data-container > .data-list > li):
(.table > .data-container > .data-list > li.selected):
(@media (prefers-dark-interface)):
(.table,): Deleted.
Removed styles that are no longer needed after https://webkit.org/b/189766,
and provide a visual separation between adjacent selected rows.

* UserInterface/Views/Table.js:
(WI.Table):
(WI.Table.prototype.get selectedRows):
(WI.Table.prototype.get allowsMultipleSelection):
(WI.Table.prototype.set allowsMultipleSelection):
(WI.Table.prototype.reloadData):
(WI.Table.prototype.selectRow):
(WI.Table.prototype.deselectRow):
(WI.Table.prototype.deselectAll):
(WI.Table.prototype._getOrCreateRow):
(WI.Table.prototype._handleMouseDown):
(WI.Table.prototype._deselectAllAndSelect):
(WI.Table.prototype._isRowSelected):
(WI.Table.prototype._notifySelectionDidChange):
(WI.Table.prototype.clearSelectedRow): Deleted.
Table now tracks selected rows using an IndexSet. selectRow accepts an
optional parameter, `extendSelection`, for adding rows to the selection.
_selectedRowIndex is now used to track the most recently selected row.
This will be the only selected row unless multiple selection is enabled,
in which case it is the row that has the "focus", for purposes of selecting
a new row using the up or down arrow keys.

LayoutTests:

* inspector/table/resources/table-utilities.js: Added.
(TestPage.registerInitializer.InspectorTest.TableDataSource):
(TestPage.registerInitializer.InspectorTest.TableDataSource.prototype.get items):
(TestPage.registerInitializer.InspectorTest.TableDataSource.prototype.tableNumberOfRows):
(TestPage.registerInitializer.InspectorTest.TableDelegate):
(TestPage.registerInitializer.InspectorTest.TableDelegate.prototype.tableSelectionDidChange):
(TestPage.registerInitializer.InspectorTest.TableDelegate.prototype.tablePopulateCell):
(TestPage.registerInitializer.InspectorTest.createTable):
(TestPage.registerInitializer):

* inspector/table/table-selection-expected.txt: Added.
* inspector/table/table-selection.html: Added.
* inspector/unit-tests/index-set-expected.txt: Added.
* inspector/unit-tests/index-set.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (236852 => 236853)


--- trunk/LayoutTests/ChangeLog	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/LayoutTests/ChangeLog	2018-10-04 21:54:14 UTC (rev 236853)
@@ -1,3 +1,26 @@
+2018-10-04  Matt Baker  <[email protected]>
+
+        Web Inspector: Table should support multiple selection and Cmd-click behavior
+        https://bugs.webkit.org/show_bug.cgi?id=189705
+        <rdar://problem/44571170>
+
+        Reviewed by Devin Rousso.
+
+        * inspector/table/resources/table-utilities.js: Added.
+        (TestPage.registerInitializer.InspectorTest.TableDataSource):
+        (TestPage.registerInitializer.InspectorTest.TableDataSource.prototype.get items):
+        (TestPage.registerInitializer.InspectorTest.TableDataSource.prototype.tableNumberOfRows):
+        (TestPage.registerInitializer.InspectorTest.TableDelegate):
+        (TestPage.registerInitializer.InspectorTest.TableDelegate.prototype.tableSelectionDidChange):
+        (TestPage.registerInitializer.InspectorTest.TableDelegate.prototype.tablePopulateCell):
+        (TestPage.registerInitializer.InspectorTest.createTable):
+        (TestPage.registerInitializer):
+
+        * inspector/table/table-selection-expected.txt: Added.
+        * inspector/table/table-selection.html: Added.
+        * inspector/unit-tests/index-set-expected.txt: Added.
+        * inspector/unit-tests/index-set.html: Added.
+
 2018-10-03  Ryosuke Niwa  <[email protected]>
 
         MutationRecord doesn't keep JS wrappers of target, addedNodes, and removedNodes alive

Added: trunk/LayoutTests/inspector/table/resources/table-utilities.js (0 => 236853)


--- trunk/LayoutTests/inspector/table/resources/table-utilities.js	                        (rev 0)
+++ trunk/LayoutTests/inspector/table/resources/table-utilities.js	2018-10-04 21:54:14 UTC (rev 236853)
@@ -0,0 +1,64 @@
+TestPage.registerInitializer(() => {
+    InspectorTest.TableDataSource = class TableDataSource
+    {
+        constructor(items)
+        {
+            this._items = items;
+        }
+
+        get items() { return this._items; }
+
+        // Table DataSource
+
+        tableNumberOfRows(table)
+        {
+            return this._items.length;
+        }
+    }
+
+    InspectorTest.TableDelegate = class TableDelegate
+    {
+        constructor(items)
+        {
+            this._items = items;
+        }
+
+        tableSelectionDidChange(table)
+        {
+            InspectorTest.pass("Table selection changed.");
+        }
+
+        tablePopulateCell(table, cell, column, rowIndex)
+        {
+            let item = this._items[rowIndex];
+            InspectorTest.assert(item, "Should have an item for row " + rowIndex);
+            InspectorTest.assert(item[column.identifier], "Should have data for column " + column.identifier);
+            cell.textContent = item[column.identifier];
+            return cell;
+        }
+    }
+
+    InspectorTest.createTable = function(delegate, dataSource) {
+        if (!dataSource) {
+            let items = [];
+            for (let i = 0; i < 10; ++i) {
+                items.push({
+                    index: i,
+                    name: "Row " + i,
+                });
+            }
+            dataSource = new InspectorTest.TableDataSource(items);
+        }
+
+        delegate = delegate || new InspectorTest.TableDelegate(dataSource.items);
+
+        const rowHeight = 20;
+        let table = new WI.Table("test", dataSource, delegate, rowHeight);
+        table.addColumn(new WI.TableColumn("index", WI.UIString("Index")));
+        table.addColumn(new WI.TableColumn("name", WI.UIString("Name")));
+
+        table.updateLayout();
+
+        return table;
+    }
+});

Added: trunk/LayoutTests/inspector/table/table-selection-expected.txt (0 => 236853)


--- trunk/LayoutTests/inspector/table/table-selection-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/inspector/table/table-selection-expected.txt	2018-10-04 21:54:14 UTC (rev 236853)
@@ -0,0 +1,68 @@
+Tests for WI.Table.
+
+
+== Running test suite: Table
+-- Running test case: Table.constructor
+PASS: selectedRow should be NaN.
+PASS: Should have no selected rows.
+PASS: allowsMultipleSelection should initially be false.
+
+-- Running test case: Table.SelectRow
+Selecting row 0.
+PASS: Table selection changed.
+PASS: selectedRow should be 0.
+PASS: selectedRows should be [0].
+Selecting row 1.
+PASS: Table selection changed.
+PASS: selectedRow should be 1.
+PASS: selectedRows should be [1].
+
+-- Running test case: Table.DeselectRow
+Selecting row 0.
+PASS: Table selection changed.
+PASS: selectedRow should be 0.
+Deselecting row 0.
+PASS: Table selection changed.
+PASS: selectedRows should not include 0.
+PASS: Should have no selected rows.
+
+-- Running test case: Table.AllowsMultipleSelection
+PASS: allowsMultipleSelection enabled.
+PASS: allowsMultipleSelection disabled.
+
+-- Running test case: Table.SelectMultipleRows.ExtendSelection
+PASS: allowsMultipleSelection enabled.
+Selecting row 0.
+PASS: Table selection changed.
+PASS: selectedRow should be 0.
+Selecting row 1.
+PASS: Table selection changed.
+PASS: selectedRow should be 1.
+Selecting row 9.
+PASS: Table selection changed.
+PASS: selectedRow should be 9.
+PASS: selectedRows should be [0, 1, 9].
+
+-- Running test case: Table.SelectMultipleRows.SelectTheSameRowTwice.ExtendSelection
+Selecting row 0.
+PASS: Table selection changed.
+PASS: selectedRow should be 0.
+Selecting row 1.
+PASS: Table selection changed.
+PASS: selectedRow should be 1.
+Selecting row 1.
+PASS: selectedRow should be 1.
+PASS: selectedRows should be [0, 1].
+
+-- Running test case: Table.SelectMultipleRows.SelectTheSameRowTwice.NoExtendSelection
+Selecting row 0.
+PASS: Table selection changed.
+PASS: selectedRow should be 0.
+Selecting row 1.
+PASS: Table selection changed.
+PASS: selectedRow should be 1.
+Selecting row 1.
+PASS: Table selection changed.
+PASS: selectedRow should be 1.
+PASS: selectedRows should be [1].
+

Added: trunk/LayoutTests/inspector/table/table-selection.html (0 => 236853)


--- trunk/LayoutTests/inspector/table/table-selection.html	                        (rev 0)
+++ trunk/LayoutTests/inspector/table/table-selection.html	2018-10-04 21:54:14 UTC (rev 236853)
@@ -0,0 +1,146 @@
+<!doctype html>
+<html>
+<head>
+<script src=""
+<script src=""
+<script>
+function test()
+{
+    InspectorTest.redirectRequestAnimationFrame();
+
+    let suite = InspectorTest.createSyncSuite("Table");
+
+    // Import names.
+    let {createTable, cleanupTable} = InspectorTest;
+
+    suite.addTestCase({
+        name: "Table.constructor",
+        test() {
+            let table = createTable();
+
+            InspectorTest.expectThat(isNaN(table.selectedRow), "selectedRow should be NaN.");
+            InspectorTest.expectEqual(table.selectedRows.length, 0, "Should have no selected rows.");
+            InspectorTest.expectFalse(table.allowsMultipleSelection, "allowsMultipleSelection should initially be false.");
+
+            return true;
+        }
+    });
+
+    function triggerSelectRow(table, rowIndex, extendSelection) {
+        InspectorTest.log(`Selecting row ${rowIndex}.`);
+        table.selectRow(rowIndex, extendSelection);
+        InspectorTest.expectEqual(table.selectedRow, rowIndex, `selectedRow should be ${rowIndex}.`);
+    }
+
+    function triggerDeselectRow(table, rowIndex) {
+        InspectorTest.log(`Deselecting row ${rowIndex}.`);
+        table.deselectRow(rowIndex);
+        InspectorTest.expectFalse(table.selectedRows.includes(rowIndex), `selectedRows should not include ${rowIndex}.`);
+    }
+
+    suite.addTestCase({
+        name: "Table.SelectRow",
+        description: "Select a row, then select another row causing the frist to become deselected.",
+        test() {
+            let table = createTable();
+
+            triggerSelectRow(table, 0);
+            InspectorTest.expectShallowEqual(table.selectedRows, [0], "selectedRows should be [0].");
+            triggerSelectRow(table, 1);
+            InspectorTest.expectShallowEqual(table.selectedRows, [1], "selectedRows should be [1].");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "Table.DeselectRow",
+        description: "Deselect the selected row.",
+        test() {
+            let table = createTable();
+
+            triggerSelectRow(table, 0);
+            triggerDeselectRow(table, 0);
+            InspectorTest.expectEqual(table.selectedRows.length, 0, "Should have no selected rows.");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "Table.AllowsMultipleSelection",
+        description: "Should be able to enable multiple selection.",
+        test() {
+            let table = createTable();
+
+            table.allowsMultipleSelection = true;
+            InspectorTest.expectThat(table.allowsMultipleSelection, "allowsMultipleSelection enabled.");
+            table.allowsMultipleSelection = false;
+            InspectorTest.expectFalse(table.allowsMultipleSelection, "allowsMultipleSelection disabled.");
+
+            return true;
+        }
+    });
+    suite.addTestCase({
+        name: "Table.SelectMultipleRows.ExtendSelection",
+        description: "Select multiple rows, extending the selection.",
+        test() {
+            let table = createTable();
+            table.allowsMultipleSelection = true;
+            InspectorTest.expectThat(table.allowsMultipleSelection, "allowsMultipleSelection enabled.");
+
+            const extendSelection = true;
+
+            triggerSelectRow(table, 0, extendSelection);
+            triggerSelectRow(table, 1, extendSelection);
+            triggerSelectRow(table, 9, extendSelection);
+            InspectorTest.expectShallowEqual(table.selectedRows, [0, 1, 9], "selectedRows should be [0, 1, 9].");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "Table.SelectMultipleRows.SelectTheSameRowTwice.ExtendSelection",
+        description: "Select an already selected row, and extend the selection.",
+        test() {
+            let table = createTable();
+            table.allowsMultipleSelection = true;
+
+            const extendSelection = true;
+
+            triggerSelectRow(table, 0, extendSelection);
+            triggerSelectRow(table, 1, extendSelection);
+            triggerSelectRow(table, 1, extendSelection);
+            InspectorTest.expectShallowEqual(table.selectedRows, [0, 1], "selectedRows should be [0, 1].");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "Table.SelectMultipleRows.SelectTheSameRowTwice.NoExtendSelection",
+        description: "Select an already selected row, and do not extend the selection.",
+        test() {
+            let table = createTable();
+            table.allowsMultipleSelection = true;
+
+            const extendSelection = true;
+
+            triggerSelectRow(table, 0, extendSelection);
+            triggerSelectRow(table, 1, extendSelection);
+            triggerSelectRow(table, 1);
+            InspectorTest.expectShallowEqual(table.selectedRows, [1], "selectedRows should be [1].");
+
+            return true;
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body _onLoad_="runTest()">
+    <p>Tests for WI.Table.</p>
+</body>
+</html>

Added: trunk/LayoutTests/inspector/unit-tests/index-set-expected.txt (0 => 236853)


--- trunk/LayoutTests/inspector/unit-tests/index-set-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/inspector/unit-tests/index-set-expected.txt	2018-10-04 21:54:14 UTC (rev 236853)
@@ -0,0 +1,70 @@
+Tests for WI.IndexSet.
+
+
+== Running test suite: IndexSet
+-- Running test case: IndexSet.constructor
+PASS: size should be zero.
+PASS: firstIndex should be NaN.
+PASS: lastIndex should be NaN.
+PASS: Should be [].
+
+-- Running test case: IndexSet.constructor array
+Initialize IndexSet with an array.
+PASS: size should be 5.
+PASS: firstIndex should be 0.
+PASS: lastIndex should be 100.
+PASS: Should be [0,1,5,50,100].
+Initialize IndexSet with an array containing duplicate indexes.
+PASS: size should be 5.
+PASS: Should be [0,1,5,50,100].
+
+-- Running test case: IndexSet.constructor invalid
+PASS: size should be zero.
+
+-- Running test case: IndexSet.prototype.clear
+PASS: size should be zero.
+PASS: firstIndex should be NaN.
+PASS: lastIndex should be NaN.
+PASS: Should be [].
+
+-- Running test case: IndexSet.prototype.add
+PASS: size should be 1.
+PASS: has should return true.
+
+-- Running test case: IndexSet.prototype.add duplicate
+PASS: size should be 1.
+
+-- Running test case: IndexSet.prototype.add invalid
+PASS: size should be zero.
+
+-- Running test case: IndexSet.prototype.delete
+Given an IndexSet with values [1,2,3]:
+PASS: delete 3 should succeed.
+PASS: has 3 should return false.
+
+-- Running test case: IndexSet.prototype.delete nonexistent
+Given an IndexSet with values [1,2,3]:
+PASS: delete 4 should fail.
+
+-- Running test case: IndexSet.prototype.delete invalid
+-- Running test case: IndexSet.prototype[Symbol.iterator]
+Given an IndexSet with values [20,1,10,2]:
+1
+2
+10
+20
+
+-- Running test case: IndexSet.prototype.indexGreaterThan
+Given an IndexSet with values [1,2]:
+PASS: Index greater than 0 should be 1.
+PASS: Index greater than 1 should be 2.
+PASS: Index greater than 2 should be NaN.
+PASS: Index greater than 3 should be NaN.
+
+-- Running test case: IndexSet.prototype.indexLessThan
+Given an IndexSet with values [1,2]:
+PASS: Index less than 0 should be NaN.
+PASS: Index less than 1 should be NaN.
+PASS: Index less than 2 should be 1.
+PASS: Index less than 3 should be 2.
+

Added: trunk/LayoutTests/inspector/unit-tests/index-set.html (0 => 236853)


--- trunk/LayoutTests/inspector/unit-tests/index-set.html	                        (rev 0)
+++ trunk/LayoutTests/inspector/unit-tests/index-set.html	2018-10-04 21:54:14 UTC (rev 236853)
@@ -0,0 +1,204 @@
+<!doctype html>
+<html>
+<head>
+<script src=""
+<script>
+function test()
+{
+    let suite = InspectorTest.createSyncSuite("IndexSet");
+
+    function createIndexSet(values = []) {
+        if (!Array.isArray(values))
+            values = [values];
+        InspectorTest.log(`Given an IndexSet with values [${values}]:`);
+
+        return new WI.IndexSet(values);
+    }
+
+    suite.addTestCase({
+        name: "IndexSet.constructor",
+        test() {
+            let indexSet = new WI.IndexSet;
+            InspectorTest.expectEqual(indexSet.size, 0, "size should be zero.");
+            InspectorTest.expectThat(isNaN(indexSet.firstIndex), "firstIndex should be NaN.");
+            InspectorTest.expectThat(isNaN(indexSet.lastIndex), "lastIndex should be NaN.");
+            InspectorTest.expectShallowEqual(Array.from(indexSet), [], "Should be [].");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.constructor array",
+        test() {
+            const values = [5, 1, 0, 100, 50];
+            const sortedValues = values.slice().sort((a, b) => a - b);
+
+            InspectorTest.log("Initialize IndexSet with an array.");
+            {
+                let indexSet = new WI.IndexSet(values);
+                InspectorTest.expectEqual(indexSet.size, values.length, `size should be ${values.length}.`);
+                InspectorTest.expectEqual(indexSet.firstIndex, sortedValues[0], `firstIndex should be ${sortedValues[0]}.`);
+                InspectorTest.expectEqual(indexSet.lastIndex, sortedValues.lastValue, `lastIndex should be ${sortedValues.lastValue}.`);
+                InspectorTest.expectShallowEqual(Array.from(indexSet), sortedValues, `Should be [${sortedValues}].`);
+            }
+
+            InspectorTest.log("Initialize IndexSet with an array containing duplicate indexes.");
+            {
+                let indexSet = new WI.IndexSet(values.concat(values));
+                InspectorTest.expectEqual(indexSet.size, values.length, `size should be ${values.length}.`);
+                InspectorTest.expectShallowEqual(Array.from(indexSet), sortedValues, `Should be [${sortedValues}].`);
+            }
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.constructor invalid",
+        test() {
+            let indexSet = new WI.IndexSet([-1, 1.5, "abc"]);
+            InspectorTest.expectEqual(indexSet.size, 0, "size should be zero.");
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.clear",
+        test() {
+            let indexSet = new WI.IndexSet([1, 2, 3]);
+            indexSet.add(42);
+            indexSet.clear();
+            InspectorTest.expectEqual(indexSet.size, 0, "size should be zero.");
+            InspectorTest.expectThat(isNaN(indexSet.firstIndex), "firstIndex should be NaN.");
+            InspectorTest.expectThat(isNaN(indexSet.lastIndex), "lastIndex should be NaN.");
+            InspectorTest.expectShallowEqual(Array.from(indexSet), [], "Should be [].");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.add",
+        test() {
+            let indexSet = new WI.IndexSet;
+            indexSet.add(42);
+            InspectorTest.expectEqual(indexSet.size, 1, "size should be 1.");
+            InspectorTest.expectThat(indexSet.has(42), "has should return true.");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.add duplicate",
+        test() {
+            let indexSet = new WI.IndexSet;
+            indexSet.add(42);
+            indexSet.add(42);
+            InspectorTest.expectEqual(indexSet.size, 1, "size should be 1.");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.add invalid",
+        test() {
+            let indexSet = new WI.IndexSet;
+            indexSet.add(-1);
+            indexSet.add(1.5);
+            indexSet.add("abc");
+            InspectorTest.expectEqual(indexSet.size, 0, "size should be zero.");
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.delete",
+        test() {
+            let indexSet = createIndexSet([1, 2, 3]);
+            const indexToDelete = indexSet.lastIndex;
+            let wasDeleted = indexSet.delete(indexToDelete);
+            InspectorTest.expectThat(wasDeleted, `delete ${indexToDelete} should succeed.`);
+            InspectorTest.expectFalse(indexSet.has(indexToDelete), `has ${indexToDelete} should return false.`);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.delete nonexistent",
+        test() {
+            let indexSet = createIndexSet([1, 2, 3]);
+            const indexToDelete = indexSet.lastIndex + 1;
+            let wasDeleted = indexSet.delete(indexToDelete);
+            InspectorTest.expectFalse(wasDeleted, `delete ${indexToDelete} should fail.`);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.delete invalid",
+        test() {
+            let indexSet = new WI.IndexSet;
+            indexSet.delete(-1);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype[Symbol.iterator]",
+        test() {
+            let indexSet = createIndexSet([20, 1, 10, 2]);
+            for (let index of indexSet)
+                InspectorTest.log(index);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.indexGreaterThan",
+        test() {
+            let indexSet = createIndexSet([1, 2]);
+            const {firstIndex, lastIndex} = indexSet;
+            const indexBefore = firstIndex - 1;
+            const indexAfter = lastIndex + 1;
+            InspectorTest.expectEqual(indexSet.indexGreaterThan(indexBefore), firstIndex, `Index greater than ${indexBefore} should be ${firstIndex}.`);
+            InspectorTest.expectEqual(indexSet.indexGreaterThan(firstIndex), lastIndex, `Index greater than ${firstIndex} should be ${lastIndex}.`);
+            InspectorTest.expectThat(isNaN(indexSet.indexGreaterThan(lastIndex)), `Index greater than ${lastIndex} should be NaN.`);
+            InspectorTest.expectThat(isNaN(indexSet.indexGreaterThan(indexAfter)), `Index greater than ${indexAfter} should be NaN.`);
+
+            return true;
+        }
+    });
+
+    suite.addTestCase({
+        name: "IndexSet.prototype.indexLessThan",
+        test() {
+            let indexSet = createIndexSet([1, 2]);
+            const {firstIndex, lastIndex} = indexSet;
+            const indexBefore = firstIndex - 1;
+            const indexAfter = lastIndex + 1;
+
+            InspectorTest.expectThat(isNaN(indexSet.indexLessThan(indexBefore)), `Index less than ${indexBefore} should be NaN.`);
+            InspectorTest.expectThat(isNaN(indexSet.indexLessThan(firstIndex)), `Index less than ${firstIndex} should be NaN.`);
+            InspectorTest.expectEqual(indexSet.indexLessThan(lastIndex), firstIndex, `Index less than ${lastIndex} should be ${firstIndex}.`);
+            InspectorTest.expectEqual(indexSet.indexLessThan(indexAfter), lastIndex, `Index less than ${indexAfter} should be ${lastIndex}.`);
+
+            return true;
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body _onLoad_="runTest()">
+    <p>Tests for WI.IndexSet.</p>
+</body>
+</html>

Modified: trunk/Source/WebInspectorUI/ChangeLog (236852 => 236853)


--- trunk/Source/WebInspectorUI/ChangeLog	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/Source/WebInspectorUI/ChangeLog	2018-10-04 21:54:14 UTC (rev 236853)
@@ -1,3 +1,78 @@
+2018-10-04  Matt Baker  <[email protected]>
+
+        Web Inspector: Table should support multiple selection and Cmd-click behavior
+        https://bugs.webkit.org/show_bug.cgi?id=189705
+        <rdar://problem/44571170>
+
+        Reviewed by Devin Rousso.
+
+        Add multiple row selection to Table, with new methods for programmatic
+        selection (deselectRow, deselectAll), and Command-click support for
+        selecting/deselecting Table rows.
+
+        * UserInterface/Base/IndexSet.js: Added.
+        (WI.IndexSet):
+        (WI.IndexSet.prototype.get size):
+        (WI.IndexSet.prototype.get firstIndex):
+        (WI.IndexSet.prototype.get lastIndex):
+        (WI.IndexSet.prototype.add):
+        (WI.IndexSet.prototype.delete):
+        (WI.IndexSet.prototype.has):
+        (WI.IndexSet.prototype.clear):
+        (WI.IndexSet.prototype.indexGreaterThan):
+        (WI.IndexSet.prototype.indexLessThan):
+        (WI.IndexSet.prototype.Symbol.iterator):
+        (WI.IndexSet.prototype._indexClosestTo):
+        (WI.IndexSet.prototype._validateIndex):
+        Helper container for managing an ordered sequence of unique positive
+        integers, with set semantics, backed by a sorted array. Used by Table,
+        and eventually by TreeOutline.
+
+        * UserInterface/Main.html:
+        * UserInterface/Test.html:
+        * UserInterface/Test/Test.js:
+        New files and stubs to make Table layout tests possible.
+
+        * UserInterface/Views/NetworkTableContentView.js:
+        (WI.NetworkTableContentView.prototype.reset):
+        (WI.NetworkTableContentView.prototype.showRepresentedObject):
+        (WI.NetworkTableContentView.prototype.networkResourceDetailViewClose):
+        (WI.NetworkTableContentView.prototype.tableSelectionDidChange):
+        (WI.NetworkTableContentView.prototype._restoreSelectedRow):
+        (WI.NetworkTableContentView.prototype.tableSelectedRowChanged): Deleted.
+        Replace uses of `clearSelectedRow` with `deselectAll`, and updated
+        selection changed delegate.
+
+        * UserInterface/Views/Table.css:
+        (.table > .data-container > .data-list > li):
+        (.table > .data-container > .data-list > li.selected):
+        (@media (prefers-dark-interface)):
+        (.table,): Deleted.
+        Removed styles that are no longer needed after https://webkit.org/b/189766,
+        and provide a visual separation between adjacent selected rows.
+
+        * UserInterface/Views/Table.js:
+        (WI.Table):
+        (WI.Table.prototype.get selectedRows):
+        (WI.Table.prototype.get allowsMultipleSelection):
+        (WI.Table.prototype.set allowsMultipleSelection):
+        (WI.Table.prototype.reloadData):
+        (WI.Table.prototype.selectRow):
+        (WI.Table.prototype.deselectRow):
+        (WI.Table.prototype.deselectAll):
+        (WI.Table.prototype._getOrCreateRow):
+        (WI.Table.prototype._handleMouseDown):
+        (WI.Table.prototype._deselectAllAndSelect):
+        (WI.Table.prototype._isRowSelected):
+        (WI.Table.prototype._notifySelectionDidChange):
+        (WI.Table.prototype.clearSelectedRow): Deleted.
+        Table now tracks selected rows using an IndexSet. selectRow accepts an
+        optional parameter, `extendSelection`, for adding rows to the selection.
+        _selectedRowIndex is now used to track the most recently selected row.
+        This will be the only selected row unless multiple selection is enabled,
+        in which case it is the row that has the "focus", for purposes of selecting
+        a new row using the up or down arrow keys.
+
 2018-10-04  Devin Rousso  <[email protected]>
 
         Web Inspector: REGRESSION(r236540): Uncaught Exception: TypeError: pauseReasonBreakpointTreeElement.removeStatusImage is not a function.

Added: trunk/Source/WebInspectorUI/UserInterface/Base/IndexSet.js (0 => 236853)


--- trunk/Source/WebInspectorUI/UserInterface/Base/IndexSet.js	                        (rev 0)
+++ trunk/Source/WebInspectorUI/UserInterface/Base/IndexSet.js	2018-10-04 21:54:14 UTC (rev 236853)
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 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
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.IndexSet = class IndexSet
+{
+    constructor(values)
+    {
+        console.assert(!values || Array.isArray(values));
+
+        this._indexes = [];
+
+        if (values) {
+            for (let value of values.slice().sort((a, b) => a - b)) {
+                if (value === this._indexes.lastValue)
+                    continue;
+                if (this._validateIndex(value))
+                    this._indexes.push(value);
+            }
+        }
+    }
+
+    // Public
+
+    get size() { return this._indexes.length; }
+
+    get firstIndex()
+    {
+        return this._indexes.length ? this._indexes[0] : NaN;
+    }
+
+    get lastIndex()
+    {
+        return this._indexes.length ? this._indexes.lastValue : NaN;
+    }
+
+    add(value)
+    {
+        if (!this._validateIndex(value))
+            return;
+
+        let index = this._indexes.lowerBound(value);
+        if (this._indexes[index] === value)
+            return;
+
+        this._indexes.insertAtIndex(value, index);
+    }
+
+    delete(value)
+    {
+        if (!this._validateIndex(value))
+            return false;
+
+        if (!this.size)
+            return false;
+
+        let index = this._indexes.lowerBound(value);
+        if (index === this._indexes.length)
+            return false;
+        this._indexes.splice(index, 1);
+        return true;
+    }
+
+    has(value)
+    {
+        if (!this.size)
+            return false;
+
+        let index = this._indexes.lowerBound(value);
+        return this._indexes[index] === value;
+    }
+
+    clear()
+    {
+        this._indexes = [];
+    }
+
+    indexGreaterThan(value)
+    {
+        const following = true;
+        return this._indexClosestTo(value, following);
+    }
+
+    indexLessThan(value)
+    {
+        const following = false;
+        return this._indexClosestTo(value, following);
+    }
+
+    [Symbol.iterator]()
+    {
+        return this._indexes[Symbol.iterator]();
+    }
+
+    // Private
+
+    _indexClosestTo(value, following)
+    {
+        if (!this.size)
+            return NaN;
+
+        if (this.size === 1) {
+            if (following)
+                return this.firstIndex > value ? this.firstIndex : NaN;
+            return this.firstIndex < value ? this.firstIndex : NaN;
+        }
+
+        let offset = following ? 1 : -1;
+        let position = this._indexes.lowerBound(value + offset);
+
+        let closestIndex = this._indexes[position];
+        if (closestIndex === undefined)
+            return NaN;
+
+        if (value === closestIndex)
+            return NaN;
+
+        if (!following && closestIndex > value)
+            return NaN;
+        return closestIndex;
+    }
+
+    _validateIndex(value)
+    {
+        console.assert(Number.isInteger(value) && value >= 0, "Index must be a non-negative integer.");
+        return Number.isInteger(value) && value >= 0;
+    }
+};

Modified: trunk/Source/WebInspectorUI/UserInterface/Main.html (236852 => 236853)


--- trunk/Source/WebInspectorUI/UserInterface/Main.html	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/Source/WebInspectorUI/UserInterface/Main.html	2018-10-04 21:54:14 UTC (rev 236853)
@@ -259,6 +259,7 @@
 
     <script src=""
     <script src=""
+    <script src=""
     <script src=""
     <script src=""
     <script src=""

Modified: trunk/Source/WebInspectorUI/UserInterface/Test/Test.js (236852 => 236853)


--- trunk/Source/WebInspectorUI/UserInterface/Test/Test.js	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/Source/WebInspectorUI/UserInterface/Test/Test.js	2018-10-04 21:54:14 UTC (rev 236853)
@@ -101,6 +101,14 @@
 
 WI.indentString = () => "    ";
 
+WI.LayoutDirection = {
+    System: "system",
+    LTR: "ltr",
+    RTL: "rtl",
+};
+
+WI.resolvedLayoutDirection = () => { return InspectorFrontendHost.userInterfaceLayoutDirection(); }
+
 // Add stubs that are called by the frontend API.
 WI.updateDockedState = () => {};
 WI.updateDockingAvailability = () => {};

Modified: trunk/Source/WebInspectorUI/UserInterface/Test.html (236852 => 236853)


--- trunk/Source/WebInspectorUI/UserInterface/Test.html	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/Source/WebInspectorUI/UserInterface/Test.html	2018-10-04 21:54:14 UTC (rev 236853)
@@ -37,6 +37,7 @@
 
     <script src=""
     <script src=""
+    <script src=""
     <script src=""
     <script src=""
     <script src=""
@@ -235,6 +236,10 @@
     <script src=""
     <script src=""
 
+    <script src=""
+    <script src=""
+    <script src=""
+
     <script type="text/_javascript_">
         WI.sharedApp = new WI.TestAppController;
         WI.sharedApp.initialize();

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js (236852 => 236853)


--- trunk/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/NetworkTableContentView.js	2018-10-04 21:54:14 UTC (rev 236853)
@@ -271,7 +271,7 @@
 
         if (this._table) {
             this._selectedResource = null;
-            this._table.clearSelectedRow();
+            this._table.deselectAll();
             this._table.reloadData();
             this._hidePopover();
             this._hideResourceDetailView();
@@ -285,7 +285,7 @@
         let rowIndex = this._rowIndexForResource(representedObject);
         if (rowIndex === -1) {
             this._selectedResource = null;
-            this._table.clearSelectedRow();
+            this._table.deselectAll();
             this._hideResourceDetailView();
             return;
         }
@@ -300,7 +300,7 @@
     networkResourceDetailViewClose(resourceDetailView)
     {
         this._selectedResource = null;
-        this._table.clearSelectedRow();
+        this._table.deselectAll();
         this._hideResourceDetailView();
     }
 
@@ -347,8 +347,9 @@
         return column === this._nameColumn;
     }
 
-    tableSelectedRowChanged(table, rowIndex)
+    tableSelectionDidChange(table)
     {
+        let rowIndex = table.selectedRow;
         if (isNaN(rowIndex)) {
             this._selectedResource = null;
             this._hideResourceDetailView();
@@ -1403,7 +1404,7 @@
         let rowIndex = this._rowIndexForResource(this._selectedResource);
         if (rowIndex === -1) {
             this._selectedResource = null;
-            this._table.clearSelectedRow();
+            this._table.deselectAll();
             return;
         }
 

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/Table.css (236852 => 236853)


--- trunk/Source/WebInspectorUI/UserInterface/Views/Table.css	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/Table.css	2018-10-04 21:54:14 UTC (rev 236853)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2013-2017 Apple Inc. All rights reserved.
+ * Copyright (C) 2013-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
@@ -126,11 +126,13 @@
     line-height: 20px;
     vertical-align: middle;
     white-space: nowrap;
+    border-bottom: solid 1px transparent;
 }
 
 .table > .data-container > .data-list > li.selected {
     background-color: var(--selected-background-color-unfocused) !important;
     color: inherit !important;
+    background-clip: padding-box;
 }
 
 .table:focus > .data-container > .data-list li.selected {
@@ -176,11 +178,6 @@
 }
 
 @media (prefers-dark-interface) {
-    .table,
-    .table > .header {
-        background: var(--background-color);
-    }
-
     .table > .header > .sortable:active {
         background-color: hsla(0, 0%, var(--foreground-lightness), 0.2);
     }

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/Table.js (236852 => 236853)


--- trunk/Source/WebInspectorUI/UserInterface/Views/Table.js	2018-10-04 21:47:13 UTC (rev 236852)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/Table.js	2018-10-04 21:54:14 UTC (rev 236853)
@@ -87,6 +87,8 @@
         this._fillerHeight = 0; // Calculated in _resizeColumnsAndFiller.
 
         this._selectedRowIndex = NaN;
+        this._allowsMultipleSelection = false;
+        this._selectedRows = new WI.IndexSet;
 
         this._resizers = [];
         this._currentResizer = null;
@@ -122,6 +124,12 @@
     get delegate() { return this._delegate; }
     get rowHeight() { return this._rowHeight; }
     get selectedRow() { return this._selectedRowIndex; }
+
+    get selectedRows()
+    {
+        return Array.from(this._selectedRows);
+    }
+
     get scrollContainer() { return this._scrollContainerElement; }
 
     get sortOrder()
@@ -200,6 +208,24 @@
             this._dataSource.tableSortChanged(this);
     }
 
+    get allowsMultipleSelection()
+    {
+        return this._allowsMultipleSelection;
+    }
+
+    set allowsMultipleSelection(flag)
+    {
+        if (this._allowsMultipleSelection === flag)
+            return;
+
+        this._allowsMultipleSelection = flag;
+
+        let numberOfSelectedRows = this._selectedRows.size;
+        this._selectedRows.clear();
+        if (numberOfSelectedRows > 1)
+            this._notifySelectionDidChange();
+    }
+
     resize()
     {
         this._cachedWidth = NaN;
@@ -211,6 +237,7 @@
     reloadData()
     {
         this._cachedRows.clear();
+        this._selectedRows.clear();
 
         this._previousRevealedRowCount = NaN;
         this.needsLayout();
@@ -277,37 +304,73 @@
         this._cachedRows.delete(rowIndex);
     }
 
-    selectRow(rowIndex)
+    selectRow(rowIndex, extendSelection = false)
     {
-        if (this._selectedRowIndex === rowIndex)
+        console.assert(!extendSelection || this._allowsMultipleSelection, "Cannot extend selection with multiple selection disabled.");
+
+        if (this._isRowSelected(rowIndex)) {
+            if (!extendSelection)
+                this._deselectAllAndSelect(rowIndex);
             return;
+        }
 
-        let oldSelectedRow = this._cachedRows.get(this._selectedRowIndex);
-        if (oldSelectedRow)
-            oldSelectedRow.classList.remove("selected");
+        if (!extendSelection && this._selectedRows.size) {
+            this._suppressNextSelectionDidChange = true;
+            this.deselectAll();
+        }
 
         this._selectedRowIndex = rowIndex;
+        this._selectedRows.add(rowIndex);
 
         let newSelectedRow = this._cachedRows.get(this._selectedRowIndex);
         if (newSelectedRow)
             newSelectedRow.classList.add("selected");
 
-        if (this._delegate.tableSelectedRowChanged)
-            this._delegate.tableSelectedRowChanged(this, this._selectedRowIndex);
+        this._notifySelectionDidChange();
     }
 
-    clearSelectedRow()
+    deselectRow(rowIndex)
     {
-        if (isNaN(this._selectedRowIndex))
+        if (!this._isRowSelected(rowIndex))
             return;
 
-        let oldSelectedRow = this._cachedRows.get(this._selectedRowIndex);
-        if (oldSelectedRow)
-            oldSelectedRow.classList.remove("selected");
+        let oldSelectedRow = this._cachedRows.get(rowIndex);
+        if (!oldSelectedRow)
+            return;
 
-        this._selectedRowIndex = NaN;
+        oldSelectedRow.classList.remove("selected");
+
+        this._selectedRows.delete(rowIndex);
+
+        if (this._selectedRowIndex === rowIndex) {
+            this._selectedRowIndex = NaN;
+            if (this._selectedRows.size) {
+                // Find selected row closest to deselected row.
+                let preceding = this._selectedRows.indexLessThan(rowIndex);
+                let following = this._selectedRows.indexGreaterThan(rowIndex);
+
+                if (isNaN(preceding))
+                    this._selectedRowIndex = following;
+                else if (isNaN(following))
+                    this._selectedRowIndex = preceding;
+                else {
+                    if ((following - rowIndex) < (rowIndex - preceding))
+                        this._selectedRowIndex = following;
+                    else
+                        this._selectedRowIndex = preceding;
+                }
+            }
+        }
+
+        this._notifySelectionDidChange();
     }
 
+    deselectAll()
+    {
+        const rowIndex = NaN;
+        this._deselectAllAndSelect(rowIndex);
+    }
+
     columnWithIdentifier(identifier)
     {
         return this._columnSpecs.get(identifier);
@@ -678,7 +741,7 @@
         let row = document.createElement("li");
         row.__index = rowIndex;
         row.__widthGeneration = 0;
-        if (rowIndex === this._selectedRowIndex)
+        if (this._isRowSelected(rowIndex))
             row.classList.add("selected");
 
         this._cachedRows.set(rowIndex, row);
@@ -1201,10 +1264,19 @@
         let column = this._visibleColumns[columnIndex];
         let rowIndex = row.__index;
 
+        if (this._isRowSelected(rowIndex)) {
+            if (event.metaKey)
+                this.deselectRow(rowIndex)
+            else
+                this._deselectAllAndSelect(rowIndex);
+            return;
+        }
+
         if (this._delegate.tableShouldSelectRow && !this._delegate.tableShouldSelectRow(this, cell, column, rowIndex))
             return;
 
-        this.selectRow(rowIndex);
+        let extendSelection = this._allowsMultipleSelection && event.metaKey;
+        this.selectRow(rowIndex, extendSelection);
     }
 
     _handleContextMenu(event)
@@ -1282,6 +1354,47 @@
             }, checked);
         }
     }
+
+    _deselectAllAndSelect(rowIndex)
+    {
+        if (!this._selectedRows.size)
+            return;
+
+        if (this._selectedRows.size === 1 && this._selectedRows.firstIndex === rowIndex)
+            return;
+
+        for (let selectedRowIndex of this._selectedRows) {
+            if (selectedRowIndex === rowIndex)
+                continue;
+            let oldSelectedRow = this._cachedRows.get(selectedRowIndex);
+            if (oldSelectedRow)
+                oldSelectedRow.classList.remove("selected");
+        }
+
+        this._selectedRowIndex = rowIndex;
+        this._selectedRows.clear();
+
+        if (!isNaN(rowIndex))
+            this._selectedRows.add(rowIndex);
+
+        this._notifySelectionDidChange();
+    }
+
+    _isRowSelected(rowIndex)
+    {
+        return this._selectedRows.has(rowIndex);
+    }
+
+    _notifySelectionDidChange()
+    {
+        if (this._suppressNextSelectionDidChange) {
+            this._suppressNextSelectionDidChange = false;
+            return;
+        }
+
+        if (this._delegate.tableSelectionDidChange)
+            this._delegate.tableSelectionDidChange(this);
+    }
 };
 
 WI.Table.SortOrder = {
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to