Title: [286887] trunk/Source/WebInspectorUI
Revision
286887
Author
bb...@apple.com
Date
2021-12-10 16:47:02 -0800 (Fri, 10 Dec 2021)

Log Message

Web Inspector: save and restore extension tab positions
https://bugs.webkit.org/show_bug.cgi?id=234115
<rdar://85560636>

Reviewed by Devin Rousso and Patrick Angle.

The existing tab state restoration system works by saving or loading tab positions
from persistent storage and saving or restoring each tab's state using one cookie per tab type.

With extension tabs, it is now possible to have more than one tab per tab type.
Additionally, extension tabs can be added at any time via InspectorFrontendAPI.
Given these challenges, we need a different system for saving and restoring extension tabs.

Extension tab restoration is now handled by WI.WebInspectorExtensionController.
We consider a tab to be an 'anchor' tab if it is saveable, visible, and not pinnable.
In other words, an anchor tab is one of the built-in singleton tabs like Console, Elements, etc.

When the tab bar item list is modified, for each extension tab, we save the observed
'anchor' tab's type and a distance from that anchor tab's insertion index.
Updates to extension tab positions are saved to persistent storage at most every 5 seconds.

When it is time to place an extension tab with createTabForExtension() or showExtensionTab(),
perform the reverse operation of computing an insertion index from a anchorTabType and distanceFromAnchorTab.

This patch was tested with one extension, multiple extensions, showing/hiding extension tabs,
remote inspecting a JSContext, and remote inspecting a WKWebView.

* UserInterface/Views/TabBar.js:
(WI.TabBar.prototype.get visibleTabBarItemsFromLeftToRight): Added.

* UserInterface/Controllers/WebInspectorExtensionController.js:
(WI.WebInspectorExtensionController):
(WI.WebInspectorExtensionController.get extensionTabPositionsObjectStoreKey): Added.

(WI.WebInspectorExtensionController.prototype.registerExtension):
(WI.WebInspectorExtensionController.prototype.unregisterExtension):
Add and remove WI.TabBar event listeners that notify us of changes to the tab bar.

(WI.WebInspectorExtensionController.prototype.createTabForExtension): Deleted.
(WI.WebInspectorExtensionController.prototype.async createTabForExtension): Renamed.
Load saved tab positions from persistent storage if needed. Compute the insertion index for the new tab.
This method is already expected to return a promise, so make it `async` to allow using `await`.

(WI.WebInspectorExtensionController.prototype.showExtensionTab):
Compute the insertion index for the new tab.

(WI.WebInspectorExtensionController.prototype.async _loadExtensionTabPositions):
Load saved tab positions from persistent storage, allowing for the case where nothing has been saved yet.

(WI.WebInspectorExtensionController.prototype.async _saveExtensionTabPositions):
Recompute and save tab positions for all extension tabs. Then write to persistent storage
at most every 5 seconds using a WI.Debouncer.

(WI.WebInspectorExtensionController.prototype._insertionIndexForExtensionTab): Added, wrapper method.
(WI.WebInspectorExtensionController.prototype._computeIndicesForExtensionTab):
Compute the anchorTabType, distanceFromAnchorTab, and insertionIndex for the extension tab.
If saving tab positions, pass `options.recomputePositions` to ignore saved positions
and recompute these fields based on what is currently visible in the tab bar.

* UserInterface/Views/WebInspectorExtensionTabContentView.js:
(WI.WebInspectorExtensionTabContentView.prototype.get savedTabPositionKey): Added.

Modified Paths

Diff

Modified: trunk/Source/WebInspectorUI/ChangeLog (286886 => 286887)


--- trunk/Source/WebInspectorUI/ChangeLog	2021-12-11 00:46:51 UTC (rev 286886)
+++ trunk/Source/WebInspectorUI/ChangeLog	2021-12-11 00:47:02 UTC (rev 286887)
@@ -1,3 +1,67 @@
+2021-12-10  BJ Burg  <bb...@apple.com>
+
+        Web Inspector: save and restore extension tab positions
+        https://bugs.webkit.org/show_bug.cgi?id=234115
+        <rdar://85560636>
+
+        Reviewed by Devin Rousso and Patrick Angle.
+
+        The existing tab state restoration system works by saving or loading tab positions
+        from persistent storage and saving or restoring each tab's state using one cookie per tab type.
+
+        With extension tabs, it is now possible to have more than one tab per tab type.
+        Additionally, extension tabs can be added at any time via InspectorFrontendAPI.
+        Given these challenges, we need a different system for saving and restoring extension tabs.
+
+        Extension tab restoration is now handled by WI.WebInspectorExtensionController.
+        We consider a tab to be an 'anchor' tab if it is saveable, visible, and not pinnable.
+        In other words, an anchor tab is one of the built-in singleton tabs like Console, Elements, etc.
+
+        When the tab bar item list is modified, for each extension tab, we save the observed
+        'anchor' tab's type and a distance from that anchor tab's insertion index.
+        Updates to extension tab positions are saved to persistent storage at most every 5 seconds.
+
+        When it is time to place an extension tab with createTabForExtension() or showExtensionTab(),
+        perform the reverse operation of computing an insertion index from a anchorTabType and distanceFromAnchorTab.
+
+        This patch was tested with one extension, multiple extensions, showing/hiding extension tabs,
+        remote inspecting a JSContext, and remote inspecting a WKWebView.
+
+        * UserInterface/Views/TabBar.js:
+        (WI.TabBar.prototype.get visibleTabBarItemsFromLeftToRight): Added.
+
+        * UserInterface/Controllers/WebInspectorExtensionController.js:
+        (WI.WebInspectorExtensionController):
+        (WI.WebInspectorExtensionController.get extensionTabPositionsObjectStoreKey): Added.
+
+        (WI.WebInspectorExtensionController.prototype.registerExtension):
+        (WI.WebInspectorExtensionController.prototype.unregisterExtension):
+        Add and remove WI.TabBar event listeners that notify us of changes to the tab bar.
+
+        (WI.WebInspectorExtensionController.prototype.createTabForExtension): Deleted.
+        (WI.WebInspectorExtensionController.prototype.async createTabForExtension): Renamed.
+        Load saved tab positions from persistent storage if needed. Compute the insertion index for the new tab.
+        This method is already expected to return a promise, so make it `async` to allow using `await`.
+
+        (WI.WebInspectorExtensionController.prototype.showExtensionTab):
+        Compute the insertion index for the new tab.
+
+        (WI.WebInspectorExtensionController.prototype.async _loadExtensionTabPositions):
+        Load saved tab positions from persistent storage, allowing for the case where nothing has been saved yet.
+
+        (WI.WebInspectorExtensionController.prototype.async _saveExtensionTabPositions):
+        Recompute and save tab positions for all extension tabs. Then write to persistent storage
+        at most every 5 seconds using a WI.Debouncer.
+
+        (WI.WebInspectorExtensionController.prototype._insertionIndexForExtensionTab): Added, wrapper method.
+        (WI.WebInspectorExtensionController.prototype._computeIndicesForExtensionTab):
+        Compute the anchorTabType, distanceFromAnchorTab, and insertionIndex for the extension tab.
+        If saving tab positions, pass `options.recomputePositions` to ignore saved positions
+        and recompute these fields based on what is currently visible in the tab bar.
+
+        * UserInterface/Views/WebInspectorExtensionTabContentView.js:
+        (WI.WebInspectorExtensionTabContentView.prototype.get savedTabPositionKey): Added.
+
 2021-12-10  Nikita Vasilyev  <nvasil...@apple.com>
 
         Web Inspector: Add a swatch for justify-content, justify-items, and justify-self

Modified: trunk/Source/WebInspectorUI/UserInterface/Controllers/WebInspectorExtensionController.js (286886 => 286887)


--- trunk/Source/WebInspectorUI/UserInterface/Controllers/WebInspectorExtensionController.js	2021-12-11 00:46:51 UTC (rev 286886)
+++ trunk/Source/WebInspectorUI/UserInterface/Controllers/WebInspectorExtensionController.js	2021-12-11 00:47:02 UTC (rev 286887)
@@ -34,9 +34,19 @@
         this._tabIDsForExtensionIDMap = new Multimap;
         this._nextExtensionTabID = 1;
 
+        this._extensionTabPositions = null;
+        this._saveTabPositionsDebouncer = null;
+
         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleMainResourceDidChange, this);
     }
 
+    // Static
+
+    static get extensionTabPositionsObjectStoreKey()
+    {
+        return "extension-tab-positions";
+    }
+
     // Public
 
     get registeredExtensionIDs()
@@ -51,6 +61,12 @@
             return WI.WebInspectorExtension.ErrorCode.RegistrationFailed;
         }
 
+        if (!this._extensionForExtensionIDMap.size) {
+            WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemAdded, this._saveExtensionTabPositions, this);
+            WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemRemoved, this._saveExtensionTabPositions, this);
+            WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemsReordered, this._saveExtensionTabPositions, this);
+        }
+
         let extension = new WI.WebInspectorExtension(extensionID, extensionBundleIdentifier, displayName);
         this._extensionForExtensionIDMap.set(extensionID, extension);
 
@@ -65,6 +81,12 @@
             return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
         }
 
+        if (!this._extensionForExtensionIDMap.size) {
+            WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemAdded, this._saveExtensionTabPositions, this);
+            WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemRemoved, this._saveExtensionTabPositions, this);
+            WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemsReordered, this._saveExtensionTabPositions, this);
+        }
+
         let extensionTabIDsToRemove = this._tabIDsForExtensionIDMap.take(extensionID) || [];
         for (let extensionTabID of extensionTabIDsToRemove) {
             let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.take(extensionTabID);
@@ -77,7 +99,7 @@
         this.dispatchEventToListeners(WI.WebInspectorExtensionController.Event.ExtensionRemoved, {extension});
     }
 
-    createTabForExtension(extensionID, tabName, tabIconURL, sourceURL)
+    async createTabForExtension(extensionID, tabName, tabIconURL, sourceURL)
     {
         let extension = this._extensionForExtensionIDMap.get(extensionID);
         if (!extension) {
@@ -90,8 +112,15 @@
 
         this._tabIDsForExtensionIDMap.add(extensionID, extensionTabID);
         this._extensionTabContentViewForExtensionTabIDMap.set(extensionTabID, tabContentView);
-        WI.tabBrowser.addTabForContentView(tabContentView, {suppressAnimations: true});
 
+        if (!this._extensionTabPositions)
+            await this._loadExtensionTabPositions();
+
+        WI.tabBrowser.addTabForContentView(tabContentView, {
+            suppressAnimations: true,
+            insertionIndex: this._insertionIndexForExtensionTab(tabContentView),
+        });
+
         // The calling convention is to return an error string or a result object.
         return {"result": extensionTabID};
     }
@@ -174,6 +203,7 @@
         tabContentView.visible = true;
         let success = WI.tabBrowser.showTabForContentView(tabContentView, {
             ...options,
+            insertionIndex: this._insertionIndexForExtensionTab(tabContentView),
             initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI,
         });
 
@@ -284,6 +314,85 @@
 
     // Private
 
+    async _loadExtensionTabPositions()
+    {
+        let savedTabPositions = await WI.objectStores.general.get(WebInspectorExtensionController.extensionTabPositionsObjectStoreKey);
+        this._extensionTabPositions = savedTabPositions || {};
+    }
+
+    _saveExtensionTabPositions()
+    {
+        if (!this._extensionTabPositions)
+            return;
+
+        this._saveTabPositionsDebouncer ||= new Debouncer(() => {
+            for (let tabBarItem of WI.tabBrowser.tabBar.visibleTabBarItemsFromLeftToRight) {
+                if (!(tabBarItem.representedObject instanceof WI.WebInspectorExtensionTabContentView))
+                    continue;
+
+                let {anchorTabType, anchorTabIndex, distanceFromAnchorTab} = this._computeIndicesForExtensionTab(tabBarItem.representedObject, {recomputePositions: true});
+                this._extensionTabPositions[tabBarItem.representedObject.savedTabPositionKey] = {anchorTabType, distanceFromAnchorTab};
+            }
+
+            WI.objectStores.general.put(this._extensionTabPositions, WebInspectorExtensionController.extensionTabPositionsObjectStoreKey);
+        });
+        this._saveTabPositionsDebouncer.delayForTime(5000);
+    }
+
+    _insertionIndexForExtensionTab(tabContentView, options = {})
+    {
+        let {anchorTabType, anchorTabIndex, distanceFromAnchorTab} = this._computeIndicesForExtensionTab(tabContentView, options);
+        return anchorTabIndex + distanceFromAnchorTab + 1;
+    }
+
+    _computeIndicesForExtensionTab(tabContentView, {recomputePositions} = {})
+    {
+        // Note: pinned tabs always appear on the trailing edge, so we can ignore them
+        // for the purposes of computing an `insertionIndex`` for `tabContentView`.
+        let anchorTabIndex = 0;
+        let savedPositions = this._extensionTabPositions[tabContentView.savedTabPositionKey] || {};
+        let anchorTabType = (recomputePositions && savedPositions.anchorTabType) || null;
+        let distanceFromAnchorTab = (recomputePositions && savedPositions.distanceFromAnchorTab) || 0;
+
+        let visibleTabBarItems = WI.tabBrowser.tabBar.visibleTabBarItemsFromLeftToRight;
+        for (let i = 0; i < visibleTabBarItems.length; ++i) {
+            let visibleTab = visibleTabBarItems[i].representedObject;
+            if (!visibleTab)
+                continue;
+
+            if (visibleTab === tabContentView)
+                break;
+
+            if (visibleTab instanceof WI.WebInspectorExtensionTabContentView)
+                continue;
+
+            if (recomputePositions) {
+                anchorTabType = visibleTab.type || null;
+                continue;
+            }
+
+            if (visibleTab.type !== anchorTabType)
+                continue;
+
+            anchorTabIndex = i;
+            break;
+        }
+
+        // Find the count of extension tabs after the anchor tab to compute the real distanceFromAnchorTab.
+        // Adding `distanceFromAnchorTab` to `anchorTabIndex` should not insert the tab after a different anchor tab.
+        for (let i = 1; i < visibleTabBarItems.length - anchorTabIndex; ++i) {
+            if (visibleTabBarItems[anchorTabIndex + i].representedObject?.constructor?.shouldSaveTab?.()) {
+                distanceFromAnchorTab = Number.constrain(distanceFromAnchorTab, 0, Math.max(0, i - 1));
+                return {anchorTabType, anchorTabIndex, distanceFromAnchorTab};
+            }
+        }
+
+        // If the anchor tab is now hidden upon restoring, place the extension at the end.
+        // This could happen if a smaller set of tabs are enabled for the inspection target.
+        anchorTabIndex = visibleTabBarItems.length - 1;
+        return {anchorTabType, anchorTabIndex, distanceFromAnchorTab};
+    }
+
     _handleMainResourceDidChange(event)
     {
         if (!event.target.isMainFrame())

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/TabBar.js (286886 => 286887)


--- trunk/Source/WebInspectorUI/UserInterface/Views/TabBar.js	2021-12-11 00:46:51 UTC (rev 286886)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/TabBar.js	2021-12-11 00:47:02 UTC (rev 286887)
@@ -407,6 +407,11 @@
         return this._tabBarItems;
     }
 
+    get visibleTabBarItemsFromLeftToRight()
+    {
+         return this._tabBarItemsFromLeftToRight().filter((item) => !item.hidden);
+    }
+
     get tabCount()
     {
         return this._tabBarItems.filter((item) => item.representedObject instanceof WI.TabContentView).length;

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/WebInspectorExtensionTabContentView.js (286886 => 286887)


--- trunk/Source/WebInspectorUI/UserInterface/Views/WebInspectorExtensionTabContentView.js	2021-12-11 00:46:51 UTC (rev 286886)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/WebInspectorExtensionTabContentView.js	2021-12-11 00:47:02 UTC (rev 286887)
@@ -73,6 +73,11 @@
         return true;
     }
 
+    get savedTabPositionKey()
+    {
+        return `ExtensionTab-${this._extension.extensionBundleIdentifier}-${this._tabInfo.displayName}`;
+    }
+
     whenPageAvailable()
     {
         return this._whenPageAvailablePromise.promise;
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to