Bug 1374590 - Fix changing devtools toolbox dock location while using WebExtension devtools panel. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 11 Sep 2017 17:32:05 +0200
changeset 664018 2b4692446d788cf888b62be7f4124ab39e4f7304
parent 664017 a7f61d74a4221f168fae94e7582e0928c06d7baf
child 664019 accda12ccc3d206670127f23c7ab2290ec4eeeb8
push id79593
push userluca.greco@alcacoop.it
push dateWed, 13 Sep 2017 16:19:52 +0000
bugs1374590
milestone57.0a1
Bug 1374590 - Fix changing devtools toolbox dock location while using WebExtension devtools panel. MozReview-Commit-ID: 2O1MoNZXZm0
browser/components/extensions/ext-c-devtools-panels.js
browser/components/extensions/ext-devtools-panels.js
--- a/browser/components/extensions/ext-c-devtools-panels.js
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -55,30 +55,29 @@ class ChildDevToolsPanel extends Extensi
         return view;
       }
     }
 
     return null;
   }
 
   receiveMessage({name, data}) {
-    // Filter out any message received while the panel context do not yet
-    // exist.
-    if (!this.panelContext || !this.panelContext.contentWindow) {
-      return;
-    }
-
     // Filter out any message that is not related to the id of this
     // toolbox panel.
     if (!data || data.toolboxPanelId !== this.id) {
       return;
     }
 
     switch (name) {
       case "Extension:DevToolsPanelShown":
+        // Filter out *Shown message received while the panel context do not yet
+        // exist.
+        if (!this.panelContext || !this.panelContext.contentWindow) {
+          return;
+        }
         this.onParentPanelShown();
         break;
       case "Extension:DevToolsPanelHidden":
         this.onParentPanelHidden();
         break;
     }
   }
 
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -56,22 +56,29 @@ class ParentDevToolsPanel {
 
     this.panelOptions = panelOptions;
 
     this.context.callOnClose(this);
 
     this.id = this.panelOptions.id;
 
     this.onToolboxPanelSelect = this.onToolboxPanelSelect.bind(this);
+    this.onToolboxHostWillChange = this.onToolboxHostWillChange.bind(this);
+    this.onToolboxHostChanged = this.onToolboxHostChanged.bind(this);
 
     this.unwatchExtensionProxyContextLoad = null;
     this.waitTopLevelContext = new Promise(resolve => {
       this._resolveTopLevelContext = resolve;
     });
 
+    // References to the panel browser XUL element and the toolbox window global which
+    // contains the devtools panel UI.
+    this.browser = null;
+    this.browserContainerWindow = null;
+
     this.panelAdded = false;
     this.addPanel();
   }
 
   addPanel() {
     const {icon, title} = this.panelOptions;
     const extensionName = this.context.extension.name;
 
@@ -98,30 +105,76 @@ class ParentDevToolsPanel {
     this.panelAdded = true;
   }
 
   buildPanel(window) {
     const {toolbox} = this;
 
     this.createBrowserElement(window);
 
+    // Store the last panel's container element (used to restore it when the toolbox
+    // host is switched between docked and undocked).
+    this.browserContainerWindow = window;
+
     toolbox.on("select", this.onToolboxPanelSelect);
+    toolbox.on("host-will-change", this.onToolboxHostWillChange);
+    toolbox.on("host-changed", this.onToolboxHostChanged);
 
     // Return a cleanup method that is when the panel is destroyed, e.g.
     // - when addon devtool panel has been disabled by the user from the toolbox preferences,
     //   its ParentDevToolsPanel instance is still valid, but the built devtools panel is removed from
     //   the toolbox (and re-built again if the user re-enables it from the toolbox preferences panel)
     // - when the creator context has been destroyed, the ParentDevToolsPanel close method is called,
     //   it removes the tool definition from the toolbox, which will call this destroy method.
     return () => {
       this.destroyBrowserElement();
+      this.browserContainerWindow = null;
       toolbox.off("select", this.onToolboxPanelSelect);
+      toolbox.off("host-will-change", this.onToolboxHostWillChange);
+      toolbox.off("host-changed", this.onToolboxHostChanged);
     };
   }
 
+  onToolboxHostWillChange() {
+    // NOTE: Using a content iframe here breaks the devtools panel
+    // switching between docked and undocked mode,
+    // because of a swapFrameLoader exception (see bug 1075490),
+    // destroy the browser and recreate it after the toolbox host has been
+    // switched is a reasonable workaround to fix the issue on release and beta
+    // Firefox versions (at least until the underlying bug can be fixed).
+    if (this.browser) {
+      // Fires a panel.onHidden event before destroying the browser element because
+      // the toolbox hosts is changing.
+      if (this.visible) {
+        this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelHidden", {
+          toolboxPanelId: this.id,
+        });
+      }
+
+      this.destroyBrowserElement();
+    }
+  }
+
+  async onToolboxHostChanged() {
+    if (this.browserContainerWindow) {
+      this.createBrowserElement(this.browserContainerWindow);
+
+      // Fires a panel.onShown event once the browser element has been recreated
+      // after the toolbox hosts has been changed (needed to provide the new window
+      // object to the extension page that has created the devtools panel).
+      if (this.visible) {
+        await this.waitTopLevelContext;
+
+        this.context.parentMessageManager.sendAsyncMessage("Extension:DevToolsPanelShown", {
+          toolboxPanelId: this.id,
+        });
+      }
+    }
+  }
+
   async onToolboxPanelSelect(what, id) {
     if (!this.waitTopLevelContext || !this.panelAdded) {
       return;
     }
 
     // Wait that the panel is fully loaded and emit show.
     await this.waitTopLevelContext;
 
@@ -145,20 +198,22 @@ class ParentDevToolsPanel {
     }
 
     // Explicitly remove the panel if it is registered and the toolbox is not
     // closing itself.
     if (this.panelAdded && toolbox.isToolRegistered(this.id)) {
       toolbox.removeAdditionalTool(this.id);
     }
 
+    this.waitTopLevelContext = null;
+    this._resolveTopLevelContext = null;
     this.context = null;
     this.toolbox = null;
-    this.waitTopLevelContext = null;
-    this._resolveTopLevelContext = null;
+    this.browser = null;
+    this.browserContainerWindow = null;
   }
 
   createBrowserElement(window) {
     const {toolbox} = this;
     const {url} = this.panelOptions;
     const {document} = window;
 
     const browser = document.createElementNS(XUL_NS, "browser");
@@ -174,22 +229,16 @@ class ParentDevToolsPanel {
 
     const {extension} = this.context;
 
     let awaitFrameLoader = Promise.resolve();
     if (extension.remote) {
       browser.setAttribute("remote", "true");
       browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
       awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
-    } else if (!AppConstants.RELEASE_OR_BETA) {
-      // NOTE: Using a content iframe here breaks the devtools panel
-      // switching between docked and undocked mode,
-      // because of a swapFrameLoader exception (see bug 1075490).
-      browser.setAttribute("type", "chrome");
-      browser.setAttribute("forcemessagemanager", true);
     }
 
     let hasTopLevelContext = false;
 
     // Listening to new proxy contexts.
     this.unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(this, context => {
       // Keep track of the toolbox and target associated to the context, which is
       // needed by the API methods implementation.