Bug 1034036 - Part 2: 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
MozReview-Commit-ID: 53BZifLLRX2
--- a/browser/modules/WindowTracker.jsm
+++ b/browser/modules/WindowTracker.jsm
@@ -20,20 +20,23 @@ 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 DEBUG = false;
+const DEFAULT_ZINDEX = 0;
// Variables
var _lastFocusedWindow = null;
var _lastTopLevelWindowID = 0;
+var _zIndexIncrement = 0;
+var _trackedWindows = new WeakMap();
// Global methods
function debug(s) {
if (DEBUG) {
dump("-*- WindowTracker: " + s + "\n");
}
}
@@ -76,26 +79,65 @@ function _handleEvent(aEvent) {
function _handleMessage(aMessage) {
let browser = aMessage.target;
if (aMessage.name === "Browser:Init" &&
browser === browser.ownerGlobal.gBrowser.selectedBrowser) {
_updateCurrentContentOuterWindowID(browser);
}
}
+function _updateZIndex(window) {
+ if (!_trackedWindows.has(window))
+ return;
+ // It's not like we'll ever hit this, but ok. Pathological case for infinitely
+ // long sessions.
+ if (++_zIndexIncrement == Number.MAX_SAFE_INTEGER) {
+ // Restart from `0`.
+ _resetZIndexTracking();
+ ++_zIndexIncrement;
+ }
+ let props = _trackedWindows.get(window);
+ props.zIndex = _zIndexIncrement;
+ _trackedWindows.set(window, props);
+}
+
+function _resetZIndexTracking() {
+ // Restart the increment from '0'.
+ _zIndexIncrement = 0;
+ let windows = [];
+ // Iterate over the list of currently opened windows to take a snapshot of the
+ // current state and rebuild the z-index tracking map with fresh indices.
+ let windowList = Services.wm.getEnumerator("navigator:browser");
+ while (windowList.hasMoreElements()) {
+ let window = windowList.getNext();
+ if (!_trackedWindows.has(window)) {
+ continue;
+ }
+ windows.push([window, _trackedWindows.get(window).zIndex || DEFAULT_ZINDEX]);
+ }
+ // We're not going to take an amount of windows > MAX_SAFE_INTEGER, because
+ // at that point the whole browser process will be frozen anyway.
+ // Now we know all the tracked windows and their respective order, so it's time
+ // to rebuild the map with lower numbers as before.
+ for (let [window, ] of windows.sort((a, b) => a[1] - b[1])) {
+ _trackedWindows.set(window, { zIndex: ++_zIndexIncrement });
+ }
+}
+
// Methods that impact a window. Put into single object for organization.
var WindowHelper = {
addWindow(aWindow) {
// Add event listeners
TAB_EVENTS.forEach(function(event) {
aWindow.gBrowser.tabContainer.addEventListener(event, _handleEvent);
});
WINDOW_EVENTS.forEach(function(event) {
aWindow.addEventListener(event, _handleEvent);
});
+ _trackedWindows.set(aWindow, {});
let messageManager = aWindow.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 (aWindow == _focusManager.activeWindow)
this.handleFocusedWindow(aWindow);
@@ -128,16 +170,17 @@ var WindowHelper = {
this.handleFocusedWindow(aWindow);
_updateCurrentContentOuterWindowID(aWindow.gBrowser.selectedBrowser);
},
handleFocusedWindow(aWindow) {
// aWindow is now focused
_lastFocusedWindow = aWindow;
+ _updateZIndex(aWindow);
},
getMostRecentBrowserWindow(aOptions) {
let checkPrivacy = typeof aOptions == "object" &&
"private" in aOptions;
let allowPopups = typeof aOptions == "object" && !!aOptions.allowPopups;
@@ -170,16 +213,28 @@ var WindowHelper = {
}
let windowList = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true);
while (windowList.hasMoreElements()) {
let win = windowList.getNext();
if (isSuitableBrowserWindow(win))
return win;
}
return null;
+ },
+
+ orderWindows(windows) {
+ let orderedSet = [];
+ for (let window of windows) {
+ let incr = _trackedWindows.has(window) ?
+ (_trackedWindows.get(window).zIndex || DEFAULT_ZINDEX) : DEFAULT_ZINDEX;
+ orderedSet.push([window, incr]);
+ }
+ // Sort the set in reverse increment, so that the last focused window is the
+ // first item in the array.
+ return orderedSet.sort((a, b) => b[1] - a[1]).map(w => w[0]);
}
};
this.WindowTracker = {
/**
* Get the most recent browser window.
*
* @param aOptions an object accepting the arguments for the search.
@@ -187,12 +242,39 @@ this.WindowTracker = {
* 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.
*/
getMostRecentBrowserWindow(options) {
return WindowHelper.getMostRecentBrowserWindow(options);
},
+ /**
+ * Order an iterable yielding window objects by z-index, in reverse order. This
+ * means that the lastly focused window will the first item in the array that
+ * is returned.
+ * Note: we only know the order of windows we're actively tracking, which
+ * basically means _only_ browser windows. This means that other window types
+ * will be appended to the list.
+ *
+ * @param {Iterable} windows Set of windows to order by z-index.
+ * @return {Array} A list of window objects, ordered by z-index, in reverse.
+ * The lastly focused window will thusly be located at index 0.
+ */
+ orderWindows(windows) {
+ return WindowHelper.orderWindows(windows);
+ },
+
track(window) {
return WindowHelper.addWindow(window);
+ },
+
+ /**
+ * Helper method to mock a specific environment for unit tests.
+ *
+ * @param {Object} env Bag of properties that change the internal behavior and/ or state.
+ */
+ setEnv(env) {
+ if ("zIndexIncrement" in env) {
+ _zIndexIncrement = env.zIndexIncrement;
+ }
}
};
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -43,8 +43,9 @@ support-files =
usageTelemetrySearchSuggestions.sjs
usageTelemetrySearchSuggestions.xml
[browser_UsageTelemetry_searchbar.js]
support-files =
usageTelemetrySearchSuggestions.sjs
usageTelemetrySearchSuggestions.xml
[browser_UsageTelemetry_content.js]
[browser_UsageTelemetry_content_aboutHome.js]
+[browser_WindowTracker.js]
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/browser_WindowTracker.js
@@ -0,0 +1,52 @@
+"use strict";
+
+ChromeUtils.import("resource:///modules/WindowTracker.jsm");
+
+const TEST_WINDOWID_PROP = "__TEST_WINDOWID";
+
+async function withOpenWindows(amount, cont) {
+ let windows = [];
+ for (let i = 0; i < amount; ++i) {
+ let window = await BrowserTestUtils.openNewBrowserWindow();
+ window[TEST_WINDOWID_PROP] = i;
+ windows.push(window);
+ }
+ await cont(windows);
+ for (let window of windows) {
+ await BrowserTestUtils.closeWindow(window);
+ }
+}
+
+add_task(async function test_orderWindows() {
+ await withOpenWindows(10, async function(windows) {
+ let ordered = WindowTracker.orderWindows(windows);
+ Assert.deepEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0], ordered.map(w => w[TEST_WINDOWID_PROP]),
+ "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");
+ windows[idx].focus();
+ await promise;
+ }
+
+ let ordered2 = WindowTracker.orderWindows(windows);
+ // 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.
+ Assert.deepEqual([1, 6, 4, 9, 8, 7, 5, 3, 2, 0], ordered2.map(w => w[TEST_WINDOWID_PROP]),
+ "After shuffle of focused windows, the order should've changed.");
+
+ // Test that a huge amount of 'activate' events (switching windows) doesn't
+ // cause unexpected behavior.
+ WindowTracker.setEnv({ zIndexIncrement: Number.MAX_SAFE_INTEGER - 1 });
+ let promise = BrowserTestUtils.waitForEvent(windows[9], "activate");
+ windows[9].focus();
+ await promise;
+
+ let ordered3 = WindowTracker.orderWindows(windows);
+ Assert.deepEqual([9, 1, 6, 4, 8, 7, 5, 3, 2, 0], ordered3.map(w => w[TEST_WINDOWID_PROP]),
+ "An insane amount of 'activate' events shouldn't confuse the tracker.");
+ });
+});