Bug 1034036 - Part 3: start tracking windows activations to always be aware of their respective order. This allows consumers to iterate over a set of windows in order of appearance (e.g. z-order). r=dao
authorMike de Boer <mdeboer@mozilla.com>
Wed, 11 Apr 2018 12:06:03 +0200
changeset 467996 7b590a257f657e19501b70185f92174fef08851b
parent 467995 b86203bcf975e96ffc3d5d6a254af6bededd04f5
child 467997 3eb2e99b13c7b2eed1b86b8a73a85bf48bb7b07f
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdao
bugs1034036
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1034036 - Part 3: start tracking windows activations to always be aware of their respective order. This allows consumers to iterate over a set of windows in order of appearance (e.g. z-order). r=dao Tests are also added here for the legacy `getTopWindow` method to guard against basic regressions. We now start tracking browser windows right after the DOMContentLoaded event, which is earlier than before. We now also assume that any newly tracked window has the focus initially, which is closer to the nsIWindowMediator semantics. MozReview-Commit-ID: 6QYJqA1tMPC
browser/base/content/browser.js
browser/modules/BrowserWindowTracker.jsm
browser/modules/test/browser/browser.ini
browser/modules/test/browser/browser_BrowserWindowTracker.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1228,16 +1228,17 @@ var gBrowserInit = {
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIWebNavigation)
           .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
           .QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIXULWindow)
           .XULBrowserWindow = window.XULBrowserWindow;
     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
       new nsBrowserAccess();
+    BrowserWindowTracker.track(window);
 
     let initBrowser = gBrowser.initialBrowser;
 
     // remoteType and sameProcessAsFrameLoader are passed through to
     // updateBrowserRemoteness as part of an options object, which itself defaults
     // to an empty object. So defaulting them to undefined here will cause the
     // default behavior in updateBrowserRemoteness if they don't get set.
     let isRemote = gMultiProcessBrowser;
@@ -1484,18 +1485,16 @@ var gBrowserInit = {
       document.getElementById("textfieldDirection-swap").hidden = false;
     }
 
     // Setup click-and-hold gestures access to the session history
     // menus if global click-and-hold isn't turned on
     if (!getBoolPref("ui.click_hold_context_menus", false))
       SetClickAndHoldHandlers();
 
-    BrowserWindowTracker.track(window);
-
     PlacesToolbarHelper.init();
 
     ctrlTab.readPref();
     Services.prefs.addObserver(ctrlTab.prefName, ctrlTab);
 
     // The object handling the downloads indicator is initialized here in the
     // delayed startup function, but the actual indicator element is not loaded
     // unless there are downloads to be displayed.
--- a/browser/modules/BrowserWindowTracker.jsm
+++ b/browser/modules/BrowserWindowTracker.jsm
@@ -8,32 +8,29 @@
  */
 
 var EXPORTED_SYMBOLS = ["BrowserWindowTracker"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 // Lazy getters
-XPCOMUtils.defineLazyServiceGetter(this, "_focusManager",
-                                   "@mozilla.org/focus-manager;1",
-                                   "nsIFocusManager");
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm"
 });
 
 // Constants
 const TAB_EVENTS = ["TabBrowserInserted", "TabSelect"];
-const WINDOW_EVENTS = ["activate", "unload"];
+const WINDOW_EVENTS = ["activate", "sizemodechange", "unload"];
 const DEBUG = false;
 
 // Variables
-var _lastFocusedWindow = null;
 var _lastTopLevelWindowID = 0;
+var _trackedWindows = [];
 
 // Global methods
 function debug(s) {
   if (DEBUG) {
     dump("-*- UpdateTopLevelContentWindowIDHelper: " + s + "\n");
   }
 }
 
@@ -49,95 +46,109 @@ function _updateCurrentContentOuterWindo
   _lastTopLevelWindowID = browser.outerWindowID;
   let windowIDWrapper = Cc["@mozilla.org/supports-PRUint64;1"]
                           .createInstance(Ci.nsISupportsPRUint64);
   windowIDWrapper.data = _lastTopLevelWindowID;
   Services.obs.notifyObservers(windowIDWrapper,
                                "net:current-toplevel-outer-content-windowid");
 }
 
-function _handleEvent(aEvent) {
-  switch (aEvent.type) {
+function _handleEvent(event) {
+  switch (event.type) {
     case "TabBrowserInserted":
-      if (aEvent.target.ownerGlobal.gBrowser.selectedBrowser === aEvent.target.linkedBrowser) {
-        _updateCurrentContentOuterWindowID(aEvent.target.linkedBrowser);
+      if (event.target.ownerGlobal.gBrowser.selectedBrowser === event.target.linkedBrowser) {
+        _updateCurrentContentOuterWindowID(event.target.linkedBrowser);
       }
       break;
     case "TabSelect":
-      _updateCurrentContentOuterWindowID(aEvent.target.linkedBrowser);
+      _updateCurrentContentOuterWindowID(event.target.linkedBrowser);
       break;
     case "activate":
-      WindowHelper.onActivate(aEvent.target);
+      WindowHelper.onActivate(event.target);
+      break;
+    case "sizemodechange":
+      WindowHelper.onSizemodeChange(event.target);
       break;
     case "unload":
-      WindowHelper.removeWindow(aEvent.currentTarget);
+      WindowHelper.removeWindow(event.currentTarget);
       break;
   }
 }
 
 function _handleMessage(message) {
   let browser = message.target;
   if (message.name === "Browser:Init" &&
       browser === browser.ownerGlobal.gBrowser.selectedBrowser) {
     _updateCurrentContentOuterWindowID(browser);
   }
 }
 
+function _trackWindowOrder(window) {
+  _trackedWindows.splice(window.windowState == window.STATE_MINIMIZED ?
+    _trackedWindows.length - 1 : 0, 0, window);
+}
+
+function _untrackWindowOrder(window) {
+  let idx = _trackedWindows.indexOf(window);
+  if (idx >= 0)
+    _trackedWindows.splice(idx, 1);
+}
+
 // Methods that impact a window. Put into single object for organization.
 var WindowHelper = {
   addWindow(window) {
     // Add event listeners
     TAB_EVENTS.forEach(function(event) {
       window.gBrowser.tabContainer.addEventListener(event, _handleEvent);
     });
     WINDOW_EVENTS.forEach(function(event) {
       window.addEventListener(event, _handleEvent);
     });
 
     let messageManager = window.getGroupMessageManager("browsers");
     messageManager.addMessageListener("Browser:Init", _handleMessage);
 
-    // This gets called AFTER activate event, so if this is the focused window
-    // we want to activate it.
-    if (window == _focusManager.activeWindow)
-      this.handleFocusedWindow(window);
+    _trackWindowOrder(window);
 
     // Update the selected tab's content outer window ID.
     _updateCurrentContentOuterWindowID(window.gBrowser.selectedBrowser);
   },
 
   removeWindow(window) {
-    if (window == _lastFocusedWindow)
-      _lastFocusedWindow = null;
+    _untrackWindowOrder(window);
 
     // Remove the event listeners
     TAB_EVENTS.forEach(function(event) {
       window.gBrowser.tabContainer.removeEventListener(event, _handleEvent);
     });
     WINDOW_EVENTS.forEach(function(event) {
       window.removeEventListener(event, _handleEvent);
     });
 
     let messageManager = window.getGroupMessageManager("browsers");
     messageManager.removeMessageListener("Browser:Init", _handleMessage);
   },
 
   onActivate(window, hasFocus) {
     // If this window was the last focused window, we don't need to do anything
-    if (window == _lastFocusedWindow)
+    if (window == _trackedWindows[0])
       return;
 
-    this.handleFocusedWindow(window);
+    _untrackWindowOrder(window);
+    _trackWindowOrder(window);
 
     _updateCurrentContentOuterWindowID(window.gBrowser.selectedBrowser);
   },
 
-  handleFocusedWindow(window) {
-    // window is now focused
-    _lastFocusedWindow = window;
+  onSizemodeChange(window) {
+    if (window.windowState == window.STATE_MINIMIZED) {
+      // Make sure to have the minimized window at the end of the list.
+      _untrackWindowOrder(window);
+      _trackedWindows.push(window);
+    }
   },
 
   getTopWindow(options) {
     let checkPrivacy = typeof options == "object" &&
                        "private" in options;
 
     let allowPopups = typeof options == "object" && !!options.allowPopups;
 
@@ -187,12 +198,25 @@ this.BrowserWindowTracker = {
    *            only, false to restrict the search to non-private only.
    *            Omit the property to search in both groups.
    *        * allowPopups: true if popup windows are permissable.
    */
   getTopWindow(options) {
     return WindowHelper.getTopWindow(options);
   },
 
+  /**
+   * Iterator property that yields window objects by z-index, in reverse order.
+   * This means that the lastly focused window will the first item that is yielded.
+   * Note: we only know the order of windows we're actively tracking, which
+   * basically means _only_ browser windows.
+   */
+  orderedWindows: {
+    * [Symbol.iterator]() {
+      for (let window of _trackedWindows)
+        yield window;
+    }
+  },
+
   track(window) {
     return WindowHelper.addWindow(window);
   }
 };
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -9,16 +9,17 @@ support-files =
 skip-if = !e10s # Bug 1373549
 tags = openUILinkIn
 [browser_BrowserUITelemetry_defaults.js]
 skip-if = !e10s # Bug 1373549
 [browser_BrowserUITelemetry_sidebar.js]
 skip-if = !e10s # Bug 1373549
 [browser_BrowserUITelemetry_syncedtabs.js]
 skip-if = !e10s # Bug 1373549
+[browser_BrowserWindowTracker.js]
 [browser_ContentSearch.js]
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
   !/browser/components/search/test/head.js
   !/browser/components/search/test/testEngine.xml
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/browser_BrowserWindowTracker.js
@@ -0,0 +1,113 @@
+"use strict";
+
+ChromeUtils.import("resource:///modules/BrowserWindowTracker.jsm");
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+const TEST_WINDOW = window;
+
+async function withOpenWindows(amount, cont) {
+  let windows = [];
+  for (let i = 0; i < amount; ++i) {
+    windows.push(await BrowserTestUtils.openNewBrowserWindow());
+  }
+  await cont(windows);
+  await Promise.all(windows.map(window => BrowserTestUtils.closeWindow(window)));
+}
+
+add_task(async function test_getTopWindow() {
+  await withOpenWindows(5, async function(windows) {
+    // Without options passed in.
+    let window = BrowserWindowTracker.getTopWindow();
+    let expectedMostRecentIndex = windows.length - 1;
+    Assert.equal(window, windows[expectedMostRecentIndex],
+      "Last opened window should be the most recent one.");
+
+    // Mess with the focused window things a bit.
+    for (let idx of [3, 1]) {
+      let promise = BrowserTestUtils.waitForEvent(windows[idx], "activate");
+      Services.focus.focusedWindow = windows[idx];
+      await promise;
+      window = BrowserWindowTracker.getTopWindow();
+      Assert.equal(window, windows[idx], "Lastly focused window should be the most recent one.");
+      // For this test it's useful to keep the array of created windows in order.
+      windows.splice(idx, 1);
+      windows.push(window);
+    }
+    // Update the pointer to the most recent opened window.
+    expectedMostRecentIndex = windows.length - 1;
+
+    // With 'private' option.
+    window = BrowserWindowTracker.getTopWindow({ private: true });
+    Assert.equal(window, null, "No private windows opened yet.");
+    window = BrowserWindowTracker.getTopWindow({ private: 1 });
+    Assert.equal(window, null, "No private windows opened yet.");
+    windows.push(await BrowserTestUtils.openNewBrowserWindow({ private: true }));
+    ++expectedMostRecentIndex;
+    window = BrowserWindowTracker.getTopWindow({ private: true });
+    Assert.equal(window, windows[expectedMostRecentIndex], "Private window available.");
+    window = BrowserWindowTracker.getTopWindow({ private: 1 });
+    Assert.equal(window, windows[expectedMostRecentIndex], "Private window available.");
+    // Private window checks seems to mysteriously fail on Linux in this test.
+    if (AppConstants.platform != "linux") {
+      window = BrowserWindowTracker.getTopWindow({ private: false });
+      Assert.equal(window, windows[expectedMostRecentIndex - 1],
+        "Private window available, but should not be returned.");
+    }
+
+    // With 'allowPopups' option.
+    window = BrowserWindowTracker.getTopWindow({ allowPopups: true });
+    Assert.equal(window, windows[expectedMostRecentIndex],
+      "Window focused before the private window should be the most recent one.");
+    window = BrowserWindowTracker.getTopWindow({ allowPopups: false });
+    Assert.equal(window, windows[expectedMostRecentIndex],
+      "Window focused before the private window should be the most recent one.");
+    let popupWindowPromise = BrowserTestUtils.waitForNewWindow();
+    ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+      let features = "location=no, personalbar=no, toolbar=no, scrollbars=no, menubar=no, status=no";
+      content.window.open("about:blank", "_blank", features);
+    });
+    let popupWindow = await popupWindowPromise;
+    window = BrowserWindowTracker.getTopWindow({ allowPopups: true });
+    Assert.equal(window, popupWindow,
+      "The popup window should be the most recent one, when requested.");
+    window = BrowserWindowTracker.getTopWindow({ allowPopups: false });
+    Assert.equal(window, windows[expectedMostRecentIndex],
+      "Window focused before the popup window should be the most recent one.");
+    popupWindow.close();
+  });
+});
+
+add_task(async function test_orderedWindows() {
+  await withOpenWindows(10, async function(windows) {
+    let ordered = [...BrowserWindowTracker.orderedWindows].filter(w => w != TEST_WINDOW);
+    Assert.deepEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], ordered.map(w => windows.indexOf(w)),
+      "Order of opened windows should be as opened.");
+
+    // Mess with the focused window things a bit.
+    for (let idx of [4, 6, 1]) {
+      let promise = BrowserTestUtils.waitForEvent(windows[idx], "activate");
+      Services.focus.focusedWindow = windows[idx];
+      await promise;
+    }
+
+    let ordered2 = [...BrowserWindowTracker.orderedWindows].filter(w => w != TEST_WINDOW);
+    // After the shuffle, we expect window '1' to be the top-most window, because
+    // it was the last one we called focus on. Then '6', the window we focused
+    // before-last, followed by '4'. The order of the other windows remains
+    // unchanged.
+    let expected = [1, 6, 4, 9, 8, 7, 5, 3, 2, 0];
+    Assert.deepEqual(expected, ordered2.map(w => windows.indexOf(w)),
+      "After shuffle of focused windows, the order should've changed.");
+
+    // Minimizing a window should put it at the end of the ordered list of windows.
+    let promise = BrowserTestUtils.waitForEvent(windows[9], "sizemodechange");
+    windows[9].minimize();
+    await promise;
+
+    let ordered3 = [...BrowserWindowTracker.orderedWindows].filter(w => w != TEST_WINDOW);
+    // Test the end of the array of window indices, because Windows Debug builds
+    // mysteriously swap the order of the first two windows.
+    Assert.deepEqual([8, 7, 5, 3, 2, 0, 9], ordered3.map(w => windows.indexOf(w)).slice(3),
+      "When a window is minimized, the order should've changed.");
+  });
+});