--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -205,16 +205,25 @@ function debug(aMsg) {
/**
* A global value to tell that fingerprinting resistance is enabled or not.
* If it's enabled, the session restore won't restore the window's size and
* size mode.
* This value is controlled by preference privacy.resistFingerprinting.
*/
var gResistFingerprintingEnabled = false;
+/**
+ * Return a promise that will be resolved once it receives event
+ * |SSBrowserWindowShowing| which is dispatched in onBrowserWindowShown.
+ */
+function promiseWindowShowing(window) {
+ return new Promise(resolve => window.addEventListener("SSBrowserWindowShowing",
+ () => resolve(window), {once: true}));
+}
+
this.SessionStore = {
get promiseInitialized() {
return SessionStoreInternal.promiseInitialized;
},
get promiseAllWindowsRestored() {
return SessionStoreInternal.promiseAllWindowsRestored;
},
@@ -689,18 +698,19 @@ var SessionStoreInternal = {
// Update the session start time using the restored session state.
this._updateSessionStartTime(state);
// make sure that at least the first window doesn't have anything hidden
delete state.windows[0].hidden;
// Since nothing is hidden in the first window, it cannot be a popup
delete state.windows[0].isPopup;
- // We don't want to minimize and then open a window at startup.
- if (state.windows[0].sizemode == "minimized")
+ // We don't want to minimize and then open a window at startup. However,
+ // when we're restoring, we should preserve previous windows sizemode.
+ if (state.windows[0].sizemode == "minimized" && !ss.doRestore())
state.windows[0].sizemode = "normal";
// clear any lastSessionWindowID attributes since those don't matter
// during normal restore
state.windows.forEach(function(aWindow) {
delete aWindow.__lastSessionWindowID;
});
}
} catch (ex) { debug("The session file is invalid: " + ex); }
@@ -1186,19 +1196,21 @@ var SessionStoreInternal = {
} else {
// Nothing to restore, notify observers things are complete.
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
Services.obs.notifyObservers(null, "sessionstore-one-or-no-tab-restored");
this._deferredAllWindowsRestored.resolve();
}
// this window was opened by _openWindowWithState
} else if (!this._isWindowLoaded(aWindow)) {
- let state = this._statesToRestore[aWindow.__SS_restoreID];
- let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1};
- this.restoreWindow(aWindow, state.windows[0], options);
+ // We used to restore window when it is opened. However, we
+ // want to restore windows after all windows are opened
+ // (bug 1034036). So the restoration process has been moved to
+ // function restoreWindowsFeaturesAndTabs.
+
// The user opened another, non-private window after starting up with
// a single private one. Let's restore the session we actually wanted to
// restore at startup.
} else if (this._deferredInitialState && !isPrivateWindow &&
aWindow.toolbar.visible) {
// global data must be restored before restoreWindow is called so that
// it happens before observers are notified
@@ -1280,16 +1292,20 @@ var SessionStoreInternal = {
* Called right before a new browser window is shown.
* @param aWindow
* Window reference
*/
onBeforeBrowserWindowShown(aWindow) {
// Register the window.
this.onLoad(aWindow);
+ // Dispatch a custom event to tell that a new window is about to be shown.
+ let evt = new aWindow.CustomEvent("SSBrowserWindowShowing");
+ aWindow.dispatchEvent(evt);
+
// Just call initializeWindow() directly if we're initialized already.
if (this._sessionInitialized) {
this.initializeWindow(aWindow);
return;
}
// The very first window that is opened creates a promise that is then
// re-used by all subsequent windows. The promise will be used to tell
@@ -1354,17 +1370,17 @@ var SessionStoreInternal = {
let completionPromise = Promise.resolve();
// this window was about to be restored - conserve its original data, if any
let isFullyLoaded = this._isWindowLoaded(aWindow);
if (!isFullyLoaded) {
if (!aWindow.__SSi) {
aWindow.__SSi = this._generateWindowID();
}
- this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
+ this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID].windows[0];
delete this._statesToRestore[aWindow.__SS_restoreID];
delete aWindow.__SS_restoreID;
}
// ignore windows not tracked by SessionStore
if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
return completionPromise;
}
@@ -2528,16 +2544,17 @@ var SessionStoreInternal = {
}
// reopen the window
let state = { windows: this._removeClosedWindow(aIndex) };
delete state.windows[0].closedAt; // Window is now open.
let window = this._openWindowWithState(state);
this.windowToFocus = window;
+ promiseWindowShowing(window).then(win => this.restoreWindows(win, state, {overwriteTabs: true}));
// Notify of changes to closed objects.
this._notifyOfClosedObjectsChange();
return window;
},
forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
@@ -2808,16 +2825,19 @@ var SessionStoreInternal = {
// global data must be restored before restoreWindow is called so that
// it happens before observers are notified
this._globalState.setFromState(lastSessionState);
// Restore session cookies.
SessionCookies.restore(lastSessionState.cookies || []);
+ let openedWindows = [];
+ let remainingWindows = [];
+
// Restore into windows or open new ones as needed.
for (let i = 0; i < lastSessionState.windows.length; i++) {
let winState = lastSessionState.windows[i];
let lastSessionWindowID = winState.__lastSessionWindowID;
// delete lastSessionWindowID so we don't add that to the window again
delete winState.__lastSessionWindowID;
// See if we can use an open window. First try one that is associated with
@@ -2836,27 +2856,38 @@ var SessionStoreInternal = {
// Since we're not overwriting existing tabs, we want to merge _closedTabs,
// putting existing ones first. Then make sure we're respecting the max pref.
if (winState._closedTabs && winState._closedTabs.length) {
let curWinState = this._windows[windowToUse.__SSi];
curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length);
}
- // Restore into that window - pretend it's a followup since we'll already
- // have a focused window.
// XXXzpao This is going to merge extData together (taking what was in
// winState over what is in the window already.
- let options = {overwriteTabs: canOverwriteTabs, isFollowUp: true};
- this.restoreWindow(windowToUse, winState, options);
+ // We don't restore window right away, just store its data.
+ // Later, these windows will be restored with newly opened windows.
+ if ("zIndex" in winState) {
+ windowToUse.__SS_zIndex = winState.zIndex;
+ }
+
+ this._updateWindowRestoreState(windowToUse, {windows: [winState]});
+ windowToUse.__SS_restoreOptions = {overwriteTabs: canOverwriteTabs};
+ openedWindows.push(windowToUse);
} else {
- this._openWindowWithState({ windows: [winState] });
+ remainingWindows.push(winState);
}
}
+ // Actually restore windows in reversed z-order.
+ this.openWindows({windows: remainingWindows}).then(wins => {
+ wins = openedWindows.concat(wins);
+ this.restoreWindowsInReversedZOrder(wins, this._statesToRestore, false);
+ });
+
// Merge closed windows from this session with ones from last session
if (lastSessionState._closedWindows) {
this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows);
this._capClosedWindows();
this._closedObjectsChanged = true;
}
DevToolsShim.restoreDevToolsSession(lastSessionState);
@@ -3098,27 +3129,49 @@ var SessionStoreInternal = {
}
return [true, canOverwriteTabs];
},
/* ........ Saving Functionality .............. */
/**
+ * Return z-index of a window.
+ * If a window is minimized, its z-index is -1.
+ *
+ * @param aWindow
+ * Window reference
+ * @return z-index of that window
+ */
+ _getZIndex(window) {
+ let isMinimized = this._getWindowDimension(window, "sizemode") == "minimized";
+ return isMinimized ? -1 : window.__SS_zIndex;
+ },
+
+ /**
* Store window dimensions, visibility, sidebar
* @param aWindow
* Window reference
*/
_updateWindowFeatures: function ssi_updateWindowFeatures(aWindow) {
var winData = this._windows[aWindow.__SSi];
WINDOW_ATTRIBUTES.forEach(function(aAttr) {
winData[aAttr] = this._getWindowDimension(aWindow, aAttr);
}, this);
+ // We only update zIndex when a valid zIndex is found in a window.
+ // There will be a case (flushAllWindowsAsync) where this function
+ // is called outside |forEachBrowserWindow|, therefore, no zIndex
+ // is found.
+ let zIndex = this._getZIndex(aWindow);
+ if (zIndex) {
+ winData.zIndex = this._getZIndex(aWindow);
+ }
+
var hidden = WINDOW_HIDEABLE_FEATURES.filter(function(aItem) {
return aWindow[aItem] && !aWindow[aItem].visible;
});
if (hidden.length != 0)
winData.hidden = hidden.join(",");
else if (winData.hidden)
delete winData.hidden;
@@ -3304,37 +3357,49 @@ var SessionStoreInternal = {
DirtyWindows.remove(aWindow);
return tabMap;
},
/* ........ Restoring Functionality .............. */
/**
+ * Open windows with data
+ *
+ * @param root
+ * Windows data
+ * @returns a promise resolved when all windows have been opened
+ */
+ openWindows(root) {
+ let openedWindowPromises = [];
+ for (let winData of root.windows) {
+ if (winData && winData.tabs && winData.tabs[0]) {
+ let window = this._openWindowWithState({ windows: [winData] });
+ openedWindowPromises.push(promiseWindowShowing(window));
+ }
+ }
+ return Promise.all(openedWindowPromises);
+ },
+
+ /**
* restore features to a single window
* @param aWindow
* Window reference to the window to use for restoration
* @param winData
* JS object
* @param aOptions
* {overwriteTabs: true} to overwrite existing tabs w/ new ones
- * {isFollowUp: true} if this is not the restoration of the 1st window
* {firstWindow: true} if this is the first non-private window we're
* restoring in this session, that might open an
* external link as well
*/
restoreWindow: function ssi_restoreWindow(aWindow, winData, aOptions = {}) {
let overwriteTabs = aOptions && aOptions.overwriteTabs;
- let isFollowUp = aOptions && aOptions.isFollowUp;
let firstWindow = aOptions && aOptions.firstWindow;
- if (isFollowUp) {
- this.windowToFocus = aWindow;
- }
-
// initialize window if necessary
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
this.onLoad(aWindow);
TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
// We're not returning from this before we end up calling restoreTabs
// for this window, so make sure we send the SSWindowStateBusy event.
@@ -3461,17 +3526,16 @@ var SessionStoreInternal = {
// We want to correlate the window with data from the last session, so
// assign another id if we have one. Otherwise clear so we don't do
// anything with it.
delete aWindow.__SS_lastSessionWindowID;
if (winData.__lastSessionWindowID)
aWindow.__SS_lastSessionWindowID = winData.__lastSessionWindowID;
if (overwriteTabs) {
- this.restoreWindowFeatures(aWindow, winData);
delete this._windows[aWindow.__SSi].extData;
}
// Restore cookies from legacy sessions, i.e. before bug 912717.
SessionCookies.restore(winData.cookies || []);
if (winData.extData) {
if (!this._windows[aWindow.__SSi].extData) {
@@ -3560,36 +3624,78 @@ var SessionStoreInternal = {
}
// A flag indicate that we've prepared a connection for this tab and
// if is called again, we shouldn't prepare another connection.
tab.__SS_connectionPrepared = true;
}
},
/**
+ * This function will restore window features and then retore window data.
+ *
+ * @param windows
+ * array of windows to be restored into
+ * @param statesToRestore
+ * states of windows to be restored
+ */
+ restoreWindowsFeaturesAndTabs(windows, statesToRestore) {
+ // First, we restore window features, so that when users
+ // interacting with a window, we don't steal the window focus.
+ windows.forEach((window) => {
+ let state = statesToRestore[window.__SS_restoreID];
+ this.restoreWindowFeatures(window, state.windows[0]);
+ });
+
+ // Then we restore data into windows.
+ for (let i = 0; i < windows.length; ++i) {
+ let state = statesToRestore[windows[i].__SS_restoreID];
+ let option = windows[i].__SS_restoreOptions || {overwriteTabs: true};
+ this.restoreWindow(windows[i], state.windows[0], option);
+ delete windows[i].__SS_restoreOptions;
+ delete windows[i].__SS_zIndex;
+ }
+ },
+
+ /**
+ * This function will restore window in reversed z-index, so that users
+ * will be presented with most recently used window first.
+ *
+ * @param windows
+ * array of windows to be restored into
+ * @param statesToRestore
+ * states of windows to be restored
+ * @param areFollowUps
+ * a flag indicate these windows are follow-up windows
+ */
+ restoreWindowsInReversedZOrder(windows, statesToRestore, areFollowUps) {
+ if (windows.some(window => !!window.__SS_zIndex)) {
+ windows.sort((a, b) => b.__SS_zIndex - a.__SS_zIndex);
+ }
+
+ if (!areFollowUps) {
+ this.windowToFocus = windows[0];
+ }
+
+ this.restoreWindowsFeaturesAndTabs(windows, statesToRestore);
+ },
+
+ /**
* Restore multiple windows using the provided state.
* @param aWindow
* Window reference to the first window to use for restoration.
* Additionally required windows will be opened.
* @param aState
* JS object or JSON string
* @param aOptions
* {overwriteTabs: true} to overwrite existing tabs w/ new ones
- * {isFollowUp: true} if this is not the restoration of the 1st window
* {firstWindow: true} if this is the first non-private window we're
* restoring in this session, that might open an
* external link as well
*/
restoreWindows: function ssi_restoreWindows(aWindow, aState, aOptions = {}) {
- let isFollowUp = aOptions && aOptions.isFollowUp;
-
- if (isFollowUp) {
- this.windowToFocus = aWindow;
- }
-
// initialize window if necessary
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
this.onLoad(aWindow);
let root;
try {
root = (typeof aState == "string") ? JSON.parse(aState) : aState;
} catch (ex) { // invalid state object - don't restore anything
@@ -3605,34 +3711,38 @@ var SessionStoreInternal = {
}
// We're done here if there are no windows.
if (!root.windows || !root.windows.length) {
this._sendRestoreCompletedNotifications();
return;
}
- if (!root.selectedWindow || root.selectedWindow > root.windows.length) {
- root.selectedWindow = 0;
- }
-
- // open new windows for all further window entries of a multi-window session
- // (unless they don't contain any tab data)
- let winData;
- for (var w = 1; w < root.windows.length; w++) {
- winData = root.windows[w];
- if (winData && winData.tabs && winData.tabs[0]) {
- var window = this._openWindowWithState({ windows: [winData] });
- if (w == root.selectedWindow - 1) {
- this.windowToFocus = window;
- }
- }
- }
-
- this.restoreWindow(aWindow, root.windows[0], aOptions);
+ // Store z-index to current window so that it can be restored in reversed z-order.
+ let firstWindowData = root.windows.splice(0, 1);
+ if ("zIndex" in firstWindowData[0]) {
+ aWindow.__SS_zIndex = firstWindowData[0].zIndex;
+ }
+
+ // Store the restore state and restore option of the current window,
+ // so that the window can be restored in reversed z-order.
+ this._updateWindowRestoreState(aWindow, {windows: firstWindowData});
+ aWindow.__SS_restoreOptions = aOptions;
+
+ // Begin the restoration: First open all windows in creation order.
+ // After all windows are opened, we restore states to windows in
+ // reversed z-order.
+ this.openWindows(root).then(windows => {
+ // We want to add current window to opened window, so that this window will be
+ // restored in reversed z-order. (We add the window to first position, in case
+ // no z-indices are found, that window will be restored first.)
+ windows.unshift(aWindow);
+
+ this.restoreWindowsInReversedZOrder(windows, this._statesToRestore, false);
+ });
DevToolsShim.restoreDevToolsSession(aState);
},
/**
* Manage history restoration for a window
* @param aWindow
* Window to restore the tabs into
@@ -4205,28 +4315,52 @@ var SessionStoreInternal = {
_updateSessionStartTime: function ssi_updateSessionStartTime(state) {
// Attempt to load the session start time from the session state
if (state.session && state.session.startTime) {
this._sessionStartTime = state.session.startTime;
}
},
/**
- * call a callback for all currently opened browser windows
+ * A boolean flag indicates whether we can iterate over all windows
+ * in their z order.
+ */
+ get isWMZOrderBroken() {
+ let broken_wm_z_order = AppConstants.platform != "macosx" && AppConstants.platform != "win";
+ delete this.isWMZOrderBroken;
+ return this.isWMZOrderBroken = broken_wm_z_order;
+ },
+
+ /**
+ * Call a callback for all currently opened browser windows.
+ * This will iterate the windows in z-index from front to back,
+ * and assign z-index to the window.
* (might miss the most recent one)
* @param aFunc
* Callback each window is passed to
*/
_forEachBrowserWindow: function ssi_forEachBrowserWindow(aFunc) {
- var windowsEnum = Services.wm.getEnumerator("navigator:browser");
-
+ let windowsEnum = this.isWMZOrderBroken ?
+ Services.wm.getEnumerator("navigator:browser") :
+ Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false);
+ let mostRecentWindow = this.isWMZOrderBroken ? this._getMostRecentBrowserWindow() : null;
+
+ // We want to start zIndex at 1, so that, in _updateWindowFeatures, if no z-index is found
+ // in a window, we can just check with a simple condition if: `if (zIndex)`.
+ let zIndex = 1;
while (windowsEnum.hasMoreElements()) {
- var window = windowsEnum.getNext();
+ let window = windowsEnum.getNext();
if (window.__SSi && !window.closed) {
+ if (this.isWMZOrderBroken) {
+ window.__SS_zIndex = mostRecentWindow.__SSi === window.__SSi ? 2 : 1;
+ } else {
+ window.__SS_zIndex = zIndex++;
+ }
aFunc.call(this, window);
+ delete window.__SS_zIndex;
}
}
},
/**
* Returns most recent window
* @returns Window reference
*/
@@ -4248,16 +4382,33 @@ var SessionStoreInternal = {
if (window.closed) {
promises.push(this.onClose(window));
}
}
return Promise.all(promises);
},
/**
+ * Store a restore state of a window to this._statesToRestore. The window
+ * will be given an id that can be used to get the restore state from
+ * this._statesToRestore.
+ *
+ * @param window
+ * a reference to a window that has a state to restore
+ * @param state
+ * an object containing session data
+ */
+ _updateWindowRestoreState(window, state) {
+ do {
+ var ID = "window" + Math.random();
+ } while (ID in this._statesToRestore);
+ this._statesToRestore[(window.__SS_restoreID = ID)] = state;
+ },
+
+ /**
* open a new browser window for a given session state
* called when restoring a multi-window session
* @param aState
* Object containing session data
*/
_openWindowWithState: function ssi_openWindowWithState(aState) {
var argString = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
@@ -4275,20 +4426,22 @@ var SessionStoreInternal = {
if (winState.isPrivate) {
features += ",private";
}
var window =
Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"),
"_blank", features, argString);
- do {
- var ID = "window" + Math.random();
- } while (ID in this._statesToRestore);
- this._statesToRestore[(window.__SS_restoreID = ID)] = aState;
+ // Store z-index, so that windows can be restored in reversed z-order.
+ if ("zIndex" in aState.windows[0]) {
+ window.__SS_zIndex = aState.windows[0].zIndex;
+ }
+
+ this._updateWindowRestoreState(window, aState);
return window;
},
/**
* Whether or not to resume session, if not recovering from a crash.
* @returns bool
*/