Bug 1217129 - Toggle WebExtensions panels on click instead of re-opening them. WIP! draft
authorBlake Winton <bwinton@latte.ca>
Thu, 22 Oct 2015 10:07:02 -0400
changeset 309026 e283c71597b6e268997a2fa6f5d21bf8feecb899
parent 309025 37ee16bb31cf4b78973b71af1362542dfc6b64bc
child 511249 41ff0213ceb5945f8a802208365778cf5c7cef54
push id7563
push userbwinton@latte.ca
push dateMon, 16 Nov 2015 19:34:59 +0000
bugs1217129
milestone45.0a1
Bug 1217129 - Toggle WebExtensions panels on click instead of re-opening them. WIP!
browser/components/customizableui/content/panelUI.inc.xul
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/test/browser/browser_ext_browserAction_simple.js
browser/components/extensions/test/browser/browser_ext_currentWindow.js
browser/components/extensions/test/browser/browser_ext_getViews.js
browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -157,16 +157,20 @@
                      command="Browser:ShowAllBookmarks"
                      onclick="PanelUI.hide();"/>
     </panelview>
 
     <panelview id="PanelUI-socialapi" flex="1"/>
 
     <panelview id="PanelUI-loopapi" flex="1"/>
 
+    <panelview id="PanelUI-webExtensions-browserAction" flex="1">
+      <browser type="content" disableglobalhistory="true"/>
+    </panelview>
+
     <panelview id="PanelUI-feeds" flex="1" oncommand="FeedHandler.subscribeToFeed(null, event);">
       <label value="&feedsMenu.label;" class="panel-subview-header"/>
     </panelview>
 
     <panelview id="PanelUI-helpView" flex="1" class="PanelUI-subView">
       <label value="&helpMenu.label;" class="panel-subview-header"/>
       <vbox id="PanelUI-helpItems" class="panel-subview-body"/>
     </panelview>
@@ -321,8 +325,16 @@
       <description>&panicButton.thankyou.msg1;</description>
       <description>&panicButton.thankyou.msg2;</description>
     </vbox>
   </hbox>
   <button label="&panicButton.thankyou.buttonlabel;"
           id="panic-button-success-closebutton"
           oncommand="PanicButtonNotifier.close()"/>
 </panel>
+
+<panel id="panel-webExtensions-pageAction"
+       class="browser-extension-panel"
+       type="arrow"
+       orient="vertical"
+       flex="1">
+  <browser type="content" disableglobalhistory="true"/>
+</panel>
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -52,53 +52,49 @@ function BrowserAction(options, extensio
 
   EventEmitter.decorate(this);
 }
 
 BrowserAction.prototype = {
   build() {
     let widget = CustomizableUI.createWidget({
       id: this.id,
-      type: "custom",
+      type: "view",
+      viewId: "PanelUI-webExtensions-browserAction",
       removable: true,
       defaultArea: CustomizableUI.AREA_NAVBAR,
-      onBuild: document => {
-        let node = document.createElement("toolbarbutton");
-        node.id = this.id;
-        node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional badged-button");
-        node.setAttribute("constrain-size", "true");
-
+      onCreated: node => {
+        this.node = node;
+        node.classList.add("badged-button");
         this.updateButton(node, this.defaults);
-
+      },
+      onClick: event => {
+        let document = this.node.ownerDocument;
         let tabbrowser = document.defaultView.gBrowser;
-
-        node.addEventListener("command", event => {
-          let tab = tabbrowser.selectedTab;
-          let popup = this.getProperty(tab, "popup");
-          if (popup) {
-            this.togglePopup(node, popup);
-          } else {
-            this.emit("click");
-          }
-        });
-
-        return node;
-      },
+        let tab = tabbrowser.selectedTab;
+        let popup = this.getProperty(tab, "popup");
+        if (popup) {
+          let panel = document.getElementById(this.widget.viewId);
+          loadPanel(panel, popup, this.extension, () => {
+            let browser = panel.querySelector("browser");
+            panel.setAttribute("width", browser.getAttribute("width"));
+            panel.setAttribute("height", browser.getAttribute("height"));
+          });
+          return;
+        }
+        this.emit("click", tab);
+      }
     });
 
     this.tabContext.on("tab-select",
                        (evt, tab) => { this.updateWindow(tab.ownerDocument.defaultView); })
 
     this.widget = widget;
   },
 
-  togglePopup(node, popupResource) {
-    openPanel(node, popupResource, this.extension);
-  },
-
   // Update the toolbar button |node| with the tab context data
   // in |tabData|.
   updateButton(node, tabData) {
     if (tabData.title) {
       node.setAttribute("tooltiptext", tabData.title);
       node.setAttribute("label", tabData.title);
       node.setAttribute("aria-label", tabData.title);
     } else {
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -135,17 +135,22 @@ PageAction.prototype = {
   // If the page action has a |popup| property, a panel is opened to
   // that URL. Otherwise, a "click" event is emitted, and dispatched to
   // the any click listeners in the add-on.
   handleClick(window) {
     let tab = window.gBrowser.selectedTab;
     let popup = this.tabContext.get(tab).popup;
 
     if (popup) {
-      openPanel(this.getButton(window), popup, this.extension);
+      let node = this.getButton(window);
+      let document = node.ownerDocument;
+      let panel = document.getElementById("panel-webExtensions-pageAction");
+      loadPanel(panel, popup, this.extension, () => {
+        panel.openPopup(node, "bottomcenter topright", 0, 0, false, false);
+      });
     } else {
       this.emit("click", tab);
     }
   },
 
   handleLocationChange(eventType, tab, fromBrowse) {
     if (fromBrowse) {
       this.tabContext.clear(tab);
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -445,21 +445,23 @@ extensions.registerAPI((extension, conte
 
           return true;
         }
 
         let result = [];
         let e = Services.wm.getEnumerator("navigator:browser");
         while (e.hasMoreElements()) {
           let window = e.getNext();
+          dump("\n\nwindow = " + window.document.documentURI + "\n");
           if (window.document.readyState != "complete") {
             continue;
           }
           let tabs = TabManager.getTabs(extension, window);
           for (let tab of tabs) {
+            dump("  tab = " + tab.id + ":" + tab.windowId + " - " + tab.url + "\n");
             if (matches(window, tab)) {
               result.push(tab);
             }
           }
         }
         runSafe(context, callback, result);
       },
 
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -106,108 +106,73 @@ global.IconDetails = {
 };
 
 global.makeWidgetId = id => {
   id = id.toLowerCase();
   // FIXME: This allows for collisions.
   return id.replace(/[^a-z0-9_-]/g, "_");
 }
 
-// Open a panel anchored to the given node, containing a browser opened
-// to the given URL, owned by the given extension. If |popupURL| is not
-// an absolute URL, it is resolved relative to the given extension's
-// base URL.
-global.openPanel = (node, popupURL, extension) => {
-  let document = node.ownerDocument;
-
+// Toggle the panel anchored to the given node. The panel will contain a
+// browser opened to the given URL, owned by the given extension. If
+// |popupURL| is not an absolute URL, it is resolved relative to the
+// given extension's base URL.
+global.loadPanel = (panel, popupURL, extension, cb) => {
   let popupURI = Services.io.newURI(popupURL, null, extension.baseURI);
 
   Services.scriptSecurityManager.checkLoadURIWithPrincipal(
     extension.principal, popupURI,
     Services.scriptSecurityManager.DISALLOW_SCRIPT);
 
-  let panel = document.createElement("panel");
-  panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
-  panel.setAttribute("class", "browser-extension-panel");
-  panel.setAttribute("type", "arrow");
-  panel.setAttribute("role", "group");
-
-  let anchor;
-  if (node.localName == "toolbarbutton") {
-    // Toolbar buttons are a special case. The panel becomes a child of
-    // the button, and is anchored to the button's icon.
-    node.appendChild(panel);
-    anchor = document.getAnonymousElementByAttribute(node, "class", "toolbarbutton-icon");
-  } else {
-    // In all other cases, the panel is anchored to the target node
-    // itself, and is a child of a popupset node.
-    document.getElementById("mainPopupSet").appendChild(panel);
-    anchor = node;
-  }
-
-  const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-  let browser = document.createElementNS(XUL_NS, "browser");
-  browser.setAttribute("type", "content");
-  browser.setAttribute("disableglobalhistory", "true");
-  panel.appendChild(browser);
+  let browser = panel.querySelector("browser");
+  browser.setAttribute("src", "about:blank");
 
   let titleChangedListener = () => {
     panel.setAttribute("aria-label", browser.contentTitle);
   }
 
-  let context;
-  panel.addEventListener("popuphidden", () => {
-    browser.removeEventListener("DOMTitleChanged", titleChangedListener, true);
-    context.unload();
-    panel.remove();
+  let context = new ExtensionPage(extension, {
+    type: "popup",
+    contentWindow: browser.contentWindow,
+    uri: popupURI,
+    docShell: browser.docShell,
   });
 
-  let loadListener = () => {
-    panel.removeEventListener("load", loadListener);
-
-    context = new ExtensionPage(extension, {
-      type: "popup",
-      contentWindow: browser.contentWindow,
-      uri: popupURI,
-      docShell: browser.docShell,
-    });
-    GlobalManager.injectInDocShell(browser.docShell, extension, context);
-    browser.setAttribute("src", context.uri.spec);
+  let contentLoadListener = () => {
+    browser.removeEventListener("load", contentLoadListener, true);
 
-    let contentLoadListener = () => {
-      browser.removeEventListener("load", contentLoadListener, true);
-
-      let contentViewer = browser.docShell.contentViewer;
-      let width = {}, height = {};
-      try {
-        contentViewer.getContentSize(width, height);
-        [width, height] = [width.value, height.value];
-      } catch (e) {
-        // getContentSize can throw
-        [width, height] = [400, 400];
-      }
-
+    let contentViewer = browser.docShell.contentViewer;
+    let width = {}, height = {};
+    try {
+      contentViewer.getContentSize(width, height);
+      [width, height] = [width.value, height.value];
+      let document = panel.ownerDocument;
       let window = document.defaultView;
       width /= window.devicePixelRatio;
       height /= window.devicePixelRatio;
-      width = Math.min(width, 800);
-      height = Math.min(height, 800);
+    } catch (e) {
+      // getContentSize can throw
+      [width, height] = [400, 400];
+    }
 
-      browser.setAttribute("width", width);
-      browser.setAttribute("height", height);
+    width = Math.min(width, 800);
+    height = Math.min(height, 800);
 
-      panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
-    };
-    browser.addEventListener("load", contentLoadListener, true);
+    browser.setAttribute("width", width);
+    browser.setAttribute("height", height);
+    if (cb) {
+      cb();
+    }
+  };
+  browser.addEventListener("load", contentLoadListener, true);
 
-    browser.addEventListener("DOMTitleChanged", titleChangedListener, true);
-  };
-  panel.addEventListener("load", loadListener);
+  browser.addEventListener("DOMTitleChanged", titleChangedListener, true);
 
-  return panel;
+  GlobalManager.injectInDocShell(browser.docShell, extension, context);
+  browser.setAttribute("src", context.uri.spec);
 }
 
 // Manages tab-specific context data, and dispatching tab select events
 // across all windows.
 global.TabContext = function TabContext(getDefaults, extension) {
   this.extension = extension;
   this.getDefaults = getDefaults;
 
@@ -419,25 +384,27 @@ global.WindowListManager = {
     while (e.hasMoreElements()) {
       let window = e.getNext();
       if (includeIncomplete || window.document.readyState == "complete") {
         yield window;
       }
     }
   },
 
-  addOpenListener(listener) {
+  addOpenListener(listener, fireOnExisting = true) {
     if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
       Services.ww.registerNotification(this);
     }
     this._openListeners.add(listener);
 
     for (let window of this.browserWindows(true)) {
       if (window.document.readyState != "complete") {
         window.addEventListener("load", this);
+      } else if (fireOnExisting) {
+        listener(window);
       }
     }
   },
 
   removeOpenListener(listener) {
     this._openListeners.delete(listener);
     if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
       Services.ww.unregisterNotification(this);
@@ -455,17 +422,17 @@ global.WindowListManager = {
     this._closeListeners.delete(listener);
     if (this._openListeners.length == 0 && this._closeListeners.length == 0) {
       Services.ww.unregisterNotification(this);
     }
   },
 
   handleEvent(event) {
     let window = event.target.defaultView;
-    window.removeEventListener("load", this);
+    window.removeEventListener("load", this.loadListener);
     if (window.document.documentElement.getAttribute("windowtype") != "navigator:browser") {
       return;
     }
 
     for (let listener of this._openListeners) {
       listener(window);
     }
   },
@@ -497,27 +464,28 @@ global.AllWindowEvents = {
   // a web progress listener that covers all open windows.
   addListener(type, listener) {
     if (type == "domwindowopened") {
       return WindowListManager.addOpenListener(listener);
     } else if (type == "domwindowclosed") {
       return WindowListManager.addCloseListener(listener);
     }
 
-    if (this._listeners.size == 0) {
-      WindowListManager.addOpenListener(this.openListener);
-    }
+    let needOpenListener = this._listeners.size == 0;
 
     if (!this._listeners.has(type)) {
       this._listeners.set(type, new Set());
     }
     let list = this._listeners.get(type);
     list.add(listener);
 
-    // Register listener on all existing windows.
+    if (needOpenListener) {
+      WindowListManager.addOpenListener(this.openListener, false);
+    }
+
     for (let window of WindowListManager.browserWindows()) {
       this.addWindowListener(window, type, listener);
     }
   },
 
   removeListener(type, listener) {
     if (type == "domwindowopened") {
       return WindowListManager.removeOpenListener(listener);
@@ -529,17 +497,16 @@ global.AllWindowEvents = {
     listeners.delete(listener);
     if (listeners.size == 0) {
       this._listeners.delete(type);
       if (this._listeners.size == 0) {
         WindowListManager.removeOpenListener(this.openListener);
       }
     }
 
-    // Unregister listener from all existing windows.
     for (let window of WindowListManager.browserWindows()) {
       if (type == "progress") {
         window.gBrowser.removeTabsProgressListener(listener);
       } else {
         window.removeEventListener(type, listener);
       }
     }
   },
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_simple.js
@@ -29,24 +29,18 @@ add_task(function* () {
 
   yield extension.startup();
 
   let widgetId = makeWidgetId(extension.id) + "-browser-action";
   let node = CustomizableUI.getWidget(widgetId).forWindow(window).node;
 
   // Do this a few times to make sure the pop-up is reloaded each time.
   for (let i = 0; i < 3; i++) {
-    let evt = new CustomEvent("command", {
-      bubbles: true,
-      cancelable: true
-    });
+    let evt = new CustomEvent("click", {});
     node.dispatchEvent(evt);
 
     yield extension.awaitMessage("popup");
 
-    let panel = node.querySelector("panel");
-    if (panel) {
-      panel.hidePopup();
-    }
+    node.dispatchEvent(evt);
   }
 
   yield extension.unload();
 });
--- a/browser/components/extensions/test/browser/browser_ext_currentWindow.js
+++ b/browser/components/extensions/test/browser/browser_ext_currentWindow.js
@@ -4,20 +4,25 @@ function genericChecker()
   var path = window.location.pathname;
   if (path.indexOf("popup") != -1) {
     kind = "popup";
   } else if (path.indexOf("page") != -1) {
     kind = "page";
   }
 
   browser.test.onMessage.addListener((msg, ...args) => {
+    dump("\n\nBW2 - " + msg + "\n");
     if (msg == kind + "-check-current1") {
+      // if (kind === 'popup') {
+      //   debugger;
+      // }
       browser.tabs.query({
         currentWindow: true
       }, function(tabs) {
+        dump("\n\nBW3 - " + kind + ", " + tabs[0].windowId + "\n");
         browser.test.sendMessage("result", tabs[0].windowId);
       });
     } else if (msg == kind + "-check-current2") {
       browser.tabs.query({
         windowId: browser.windows.WINDOW_ID_CURRENT
       }, function(tabs) {
         browser.test.sendMessage("result", tabs[0].windowId);
       });
@@ -88,16 +93,18 @@ add_task(function* () {
   yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
 
   let {TabManager, WindowManager} = Cu.import("resource://gre/modules/Extension.jsm", {});
 
   let winId1 = WindowManager.getId(win1);
   let winId2 = WindowManager.getId(win2);
 
   function* checkWindow(kind, winId, name) {
+    dump("\n\nBW1 - " + kind + ", " + winId + ", " + name + "\n");
+    // debugger;
     extension.sendMessage(kind + "-check-current1");
     is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 1) [${kind}]`);
     extension.sendMessage(kind + "-check-current2");
     is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 2) [${kind}]`);
     extension.sendMessage(kind + "-check-current3");
     is((yield extension.awaitMessage("result")), winId, `${name} is on top (check 3) [${kind}]`);
   }
 
@@ -105,30 +112,24 @@ add_task(function* () {
   yield checkWindow("background", winId1, "win1");
   yield focusWindow(win2);
   yield checkWindow("background", winId2, "win2");
 
   function* triggerPopup(win, callback) {
     let widgetId = makeWidgetId(extension.id) + "-browser-action";
     let node = CustomizableUI.getWidget(widgetId).forWindow(win).node;
 
-    let evt = new CustomEvent("command", {
-      bubbles: true,
-      cancelable: true
-    });
+    let evt = new CustomEvent("click", {});
     node.dispatchEvent(evt);
 
     yield extension.awaitMessage("popup-ready");
 
     yield callback();
 
-    let panel = node.querySelector("panel");
-    if (panel) {
-      panel.hidePopup();
-    }
+    node.dispatchEvent(evt);
   }
 
   // Set focus to some other window.
   yield focusWindow(window);
 
   yield triggerPopup(win1, function*() {
     yield checkWindow("popup", winId1, "win1");
   });
--- a/browser/components/extensions/test/browser/browser_ext_getViews.js
+++ b/browser/components/extensions/test/browser/browser_ext_getViews.js
@@ -111,30 +111,24 @@ add_task(function* () {
   yield openTab(winId2);
 
   yield checkViews("background", 2, 0);
 
   function* triggerPopup(win, callback) {
     let widgetId = makeWidgetId(extension.id) + "-browser-action";
     let node = CustomizableUI.getWidget(widgetId).forWindow(win).node;
 
-    let evt = new CustomEvent("command", {
-      bubbles: true,
-      cancelable: true
-    });
+    let evt = new CustomEvent("click", {});
     node.dispatchEvent(evt);
 
     yield extension.awaitMessage("popup-ready");
 
     yield callback();
 
-    let panel = node.querySelector("panel");
-    if (panel) {
-      panel.hidePopup();
-    }
+    node.dispatchEvent(evt);
   }
 
   yield triggerPopup(win1, function*() {
     yield checkViews("background", 2, 1);
     yield checkViews("popup", 2, 1);
   });
 
   yield triggerPopup(win2, function*() {
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -131,17 +131,18 @@ add_task(function* testPageActionPopup()
     let evt = new MouseEvent("click", {});
     image.dispatchEvent(evt);
   });
 
   extension.onMessage("next-test", Task.async(function* () {
     let panel = document.getElementById(panelId);
     if (panel) {
       yield promisePopupShown(panel);
-      panel.hidePopup();
+      let evt = new MouseEvent("click", {});
+      document.getElementById(pageActionId).dispatchEvent(evt);
 
       panel = document.getElementById(panelId);
       is(panel, undefined, "panel successfully removed from document after hiding");
     }
 
     extension.sendMessage("next-test");
   }));
 
@@ -193,17 +194,17 @@ add_task(function* testPageActionSecurit
   });
 
   yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
   let browserActionId = makeWidgetId(extension.id) + "-browser-action";
   let pageActionId = makeWidgetId(extension.id) + "-page-action";
 
   let browserAction = document.getElementById(browserActionId);
-  let evt = new CustomEvent("command", {});
+  let evt = new CustomEvent("click", {});
   browserAction.dispatchEvent(evt);
 
   let pageAction = document.getElementById(pageActionId);
   evt = new MouseEvent("click", {});
   pageAction.dispatchEvent(evt);
 
   yield extension.unload();