Bug 970321 - Australis' UITour: make menu panel not break if tour tab is opened in new window, r=Unfocused
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Thu, 20 Feb 2014 22:42:33 +0000
changeset 170192 74f8a4d343a054fab5f166b2b731a07ef2d0e615
parent 170191 58ba975978540fdc4e2ce990836777a3117ff05f
child 170193 dbc693bc48c5795f03ebbd57887da0516f0ea255
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersUnfocused
bugs970321
milestone30.0a1
Bug 970321 - Australis' UITour: make menu panel not break if tour tab is opened in new window, r=Unfocused
browser/base/content/tabbrowser.xml
browser/components/customizableui/content/panelUI.js
browser/modules/UITour.jsm
browser/modules/test/browser.ini
browser/modules/test/browser_UITour_detach_tab.js
browser/modules/test/head.js
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2487,16 +2487,25 @@
       <method name="replaceTabWithWindow">
         <parameter name="aTab"/>
         <parameter name="aOptions"/>
         <body>
           <![CDATA[
             if (this.tabs.length == 1)
               return null;
 
+            let event = new CustomEvent("TabBecomingWindow", {
+              bubbles: true,
+              cancelable: true
+            });
+            aTab.dispatchEvent(event);
+            if (event.defaultPrevented) {
+              return null;
+            }
+
             var options = "chrome,dialog=no,all";
             for (var name in aOptions)
               options += "," + name + "=" + aOptions[name];
 
             // tell a new window to take the "dropped" tab
             return window.openDialog(getBrowserURL(), "_blank", options, aTab);
           ]]>
         </body>
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -28,29 +28,31 @@ const PanelUI = {
       multiView: "PanelUI-multiView",
       helpView: "PanelUI-helpView",
       menuButton: "PanelUI-menu-button",
       panel: "PanelUI-popup",
       scroller: "PanelUI-contents-scroller"
     };
   },
 
+  _initialized: false,
   init: function() {
     for (let [k, v] of Iterator(this.kElements)) {
       // Need to do fresh let-bindings per iteration
       let getKey = k;
       let id = v;
       this.__defineGetter__(getKey, function() {
         delete this[getKey];
         return this[getKey] = document.getElementById(id);
       });
     }
 
     this.menuButton.addEventListener("mousedown", this);
     this.menuButton.addEventListener("keypress", this);
+    this._initialized = true;
   },
 
   _eventListenersAdded: false,
   _ensureEventListenersAdded: function() {
     if (this._eventListenersAdded)
       return;
     this._addEventListeners();
   },
@@ -196,16 +198,28 @@ const PanelUI = {
    *
    * @return a Promise that resolves once the panel is ready to roll.
    */
   ensureReady: function(aCustomizing=false) {
     if (this._readyPromise) {
       return this._readyPromise;
     }
     this._readyPromise = Task.spawn(function() {
+      if (!this._initialized) {
+        let delayedStartupDeferred = Promise.defer();
+        let delayedStartupObserver = (aSubject, aTopic, aData) => {
+          if (aSubject == window) {
+            Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
+            delayedStartupDeferred.resolve();
+          }
+        };
+        Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
+        yield delayedStartupDeferred.promise;
+      }
+
       this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang",
                                    getLocale());
       if (!this._scrollWidth) {
         // In order to properly center the contents of the panel, while ensuring
         // that we have enough space on either side to show a scrollbar, we have to
         // do a bit of hackery. In particular, we calculate a new width for the
         // scroller, based on the system scrollbar width.
         this._scrollWidth =
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -37,21 +37,27 @@ const BUCKET_TIMESTEPS    = [
 ];
 
 
 
 this.UITour = {
   seenPageIDs: new Set(),
   pageIDSourceTabs: new WeakMap(),
   pageIDSourceWindows: new WeakMap(),
+  /* Map from browser windows to a set of tabs in which a tour is open */
   originTabs: new WeakMap(),
+  /* Map from browser windows to a set of pinned tabs opened by (a) tour(s) */
   pinnedTabs: new WeakMap(),
   urlbarCapture: new WeakMap(),
   appMenuOpenForAnnotation: new Set(),
 
+  _detachingTab: false,
+  _queuedEvents: [],
+  _pendingDoc: null,
+
   highlightEffects: ["random", "wobble", "zoom", "color"],
   targets: new Map([
     ["accountStatus", {
       query: (aDocument) => {
         let statusButton = aDocument.getElementById("PanelUI-fxa-status");
         return aDocument.getAnonymousElementByAttribute(statusButton,
                                                         "class",
                                                         "toolbarbutton-icon");
@@ -133,17 +139,30 @@ this.UITour = {
     if (typeof action != "string" || !action)
       return false;
 
     let data = aEvent.detail.data;
     if (typeof data != "object")
       return false;
 
     let window = this.getChromeWindow(contentDocument);
+    // Do this before bailing if there's no tab, so later we can pick up the pieces:
+    window.gBrowser.tabContainer.addEventListener("TabSelect", this);
     let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView);
+    if (!tab) {
+      // This should only happen while detaching a tab:
+      if (this._detachingTab) {
+        this._queuedEvents.push(aEvent);
+        this._pendingDoc = Cu.getWeakReference(contentDocument);
+        return;
+      }
+      Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
+                     "This shouldn't happen!");
+      return;
+    }
 
     switch (action) {
       case "registerPageID": {
         // We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the
         // pageID, as it could make parsing the telemetry bucket name difficult.
         if (typeof data.pageID == "string" &&
             !data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
           this.seenPageIDs.add(data.pageID);
@@ -294,33 +313,36 @@ this.UITour = {
 
         this.getConfiguration(contentDocument, data.configuration, data.callbackID);
         break;
       }
     }
 
     if (!this.originTabs.has(window))
       this.originTabs.set(window, new Set());
+
     this.originTabs.get(window).add(tab);
-
     tab.addEventListener("TabClose", this);
-    window.gBrowser.tabContainer.addEventListener("TabSelect", this);
+    tab.addEventListener("TabBecomingWindow", this);
     window.addEventListener("SSWindowClosing", this);
 
     return true;
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "pagehide": {
         let window = this.getChromeWindow(aEvent.target);
         this.teardownTour(window);
         break;
       }
 
+      case "TabBecomingWindow":
+        this._detachingTab = true;
+        // Fall through
       case "TabClose": {
         let tab = aEvent.target;
         if (this.pageIDSourceTabs.has(tab)) {
           let pageID = this.pageIDSourceTabs.get(tab);
 
           // Delete this from the window cache, so if the window is closed we
           // don't expire this page ID twice.
           let window = tab.ownerDocument.defaultView;
@@ -341,23 +363,44 @@ this.UITour = {
 
           if (this.pageIDSourceTabs.has(previousTab)) {
             let pageID = this.pageIDSourceTabs.get(previousTab);
             this.setExpiringTelemetryBucket(pageID, "inactive");
           }
         }
 
         let window = aEvent.target.ownerDocument.defaultView;
+        let selectedTab = window.gBrowser.selectedTab;
         let pinnedTab = this.pinnedTabs.get(window);
-        if (pinnedTab && pinnedTab.tab == window.gBrowser.selectedTab)
+        if (pinnedTab && pinnedTab.tab == selectedTab)
           break;
         let originTabs = this.originTabs.get(window);
-        if (originTabs && originTabs.has(window.gBrowser.selectedTab))
+        if (originTabs && originTabs.has(selectedTab))
           break;
 
+        let pendingDoc;
+        if (this._detachingTab && this._pendingDoc && (pendingDoc = this._pendingDoc.get())) {
+          if (selectedTab.linkedBrowser.contentDocument == pendingDoc) {
+            if (!this.originTabs.get(window)) {
+              this.originTabs.set(window, new Set());
+            }
+            this.originTabs.get(window).add(selectedTab);
+            this.pendingDoc = null;
+            this._detachingTab = false;
+            while (this._queuedEvents.length) {
+              try {
+                this.onPageEvent(this._queuedEvents.shift());
+              } catch (ex) {
+                Cu.reportError(ex);
+              }
+            }
+            break;
+          }
+        }
+
         this.teardownTour(window);
         break;
       }
 
       case "SSWindowClosing": {
         let window = aEvent.target;
         if (this.pageIDSourceWindows.has(window)) {
           let pageID = this.pageIDSourceWindows.get(window);
@@ -408,18 +451,20 @@ this.UITour = {
   teardownTour: function(aWindow, aWindowClosing = false) {
     aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
     aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations);
     aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations);
     aWindow.removeEventListener("SSWindowClosing", this);
 
     let originTabs = this.originTabs.get(aWindow);
     if (originTabs) {
-      for (let tab of originTabs)
+      for (let tab of originTabs) {
         tab.removeEventListener("TabClose", this);
+        tab.removeEventListener("TabBecomingWindow", this);
+      }
     }
     this.originTabs.delete(aWindow);
 
     if (!aWindowClosing) {
       this.hideHighlight(aWindow);
       this.hideInfo(aWindow);
       aWindow.PanelUI.panel.removeAttribute("noautohide");
       this.recreatePopup(aWindow.PanelUI.panel);
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -7,12 +7,13 @@ support-files =
 [browser_BrowserUITelemetry_buckets.js]
 [browser_NetworkPrioritizer.js]
 [browser_SignInToWebsite.js]
 [browser_UITour.js]
 skip-if = os == "linux" # Intermittent failures, bug 951965
 [browser_UITour2.js]
 [browser_UITour3.js]
 [browser_UITour_panel_close_annotation.js]
+[browser_UITour_detach_tab.js]
 [browser_UITour_registerPageID.js]
 [browser_UITour_sync.js]
 [browser_taskbar_preview.js]
 run-if = os == "win"
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UITour_detach_tab.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that annotations disappear when their target is hidden.
+ */
+
+"use strict";
+
+let gTestTab;
+let gContentAPI;
+let gContentWindow;
+let gContentDoc;
+let highlight = document.getElementById("UITourHighlight");
+let tooltip = document.getElementById("UITourTooltip");
+
+Components.utils.import("resource:///modules/UITour.jsm");
+
+function test() {
+  registerCleanupFunction(function() {
+    gContentDoc = null;
+  });
+  UITourTest();
+}
+
+let tests = [
+  function test_move_tab_to_new_window(done) {
+    let gOpenedWindow;
+    let onVisibilityChange = (aEvent) => {
+      if (!document.hidden && window != UITour.getChromeWindow(aEvent.target)) {
+        gContentAPI.showHighlight("appMenu");
+      }
+    };
+    let onDOMWindowDestroyed = (aWindow, aTopic, aData) => {
+      if (gOpenedWindow && aWindow == gOpenedWindow) {
+        Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
+        done();
+      }
+    };
+    let onBrowserDelayedStartup = (aWindow, aTopic, aData) => {
+      gOpenedWindow = aWindow;
+      Services.obs.removeObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished");
+      try {
+        let newWindowHighlight = gOpenedWindow.document.getElementById("UITourHighlight");
+        let selectedTab = aWindow.gBrowser.selectedTab;
+        is(selectedTab.linkedBrowser && selectedTab.linkedBrowser.contentDocument, gContentDoc, "Document should be selected in new window");
+        ok(UITour.originTabs && UITour.originTabs.has(aWindow), "Window should be known");
+        ok(UITour.originTabs.get(aWindow).has(selectedTab), "Tab should be known");
+        waitForElementToBeVisible(newWindowHighlight, function checkHighlightIsThere() {
+          gContentAPI.showMenu("appMenu");
+          isnot(aWindow.PanelUI.panel.state, "closed", "Panel should be open");
+          ok(aWindow.PanelUI.contents.children.length > 0, "Panel contents should have children");
+          gContentAPI.hideHighlight();
+          gContentAPI.hideMenu("appMenu");
+          gTestTab = null;
+          aWindow.close();
+        }, "Highlight should be shown in new window.");
+      } catch (ex) {
+        Cu.reportError(ex);
+        ok(false, "An error occurred running UITour tab detach test.");
+      } finally {
+        gContentDoc.removeEventListener("visibilitychange", onVisibilityChange, false);
+        Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
+      }
+    };
+
+    Services.obs.addObserver(onBrowserDelayedStartup, "browser-delayed-startup-finished", false);
+    // NB: we're using this rather than gContentWindow.document because the latter wouldn't
+    // have an XRayWrapper, and we need to compare this to the doc we get using this method
+    // later on...
+    gContentDoc = gBrowser.selectedTab.linkedBrowser.contentDocument;
+    gContentDoc.addEventListener("visibilitychange", onVisibilityChange, false);
+    gContentAPI.showHighlight("appMenu");
+    waitForElementToBeVisible(highlight, function checkForInitialHighlight() {
+      gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
+    });
+
+  },
+];
+
--- a/browser/modules/test/head.js
+++ b/browser/modules/test/head.js
@@ -73,17 +73,17 @@ function waitForPopupAtAnchor(popup, anc
 }
 
 function is_element_hidden(element, msg) {
   isnot(element, null, "Element should not be null, when checking visibility");
   ok(is_hidden(element), msg);
 }
 
 function loadUITourTestPage(callback, host = "https://example.com/") {
-   if (gTestTab)
+  if (gTestTab)
     gBrowser.removeTab(gTestTab);
 
   let url = getRootDirectory(gTestPath) + "uitour.html";
   url = url.replace("chrome://mochitests/content/", host);
 
   gTestTab = gBrowser.addTab(url);
   gBrowser.selectedTab = gTestTab;