--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -11,16 +11,18 @@ const FORMAT_VERSION = 1;
const TAB_CUSTOM_VALUES = new WeakMap();
const TAB_LAZY_STATES = new WeakMap();
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
const TAB_STATE_WILL_RESTORE = 3;
const TAB_STATE_FOR_BROWSER = new WeakMap();
const WINDOW_RESTORE_IDS = new WeakMap();
+const WINDOW_RESTORE_ZINDICES = new WeakMap();
+const WINDOW_SHOWING_PROMISES = new Map();
// A new window has just been restored. At this stage, tabs are generally
// not restored.
const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
@@ -686,23 +688,16 @@ var SessionStoreInternal = {
state.windows[0].tabs[0].entries[0].url = "about:sessionrestore";
state.windows[0].tabs[0].entries[0].triggeringPrincipal_base64 = Utils.SERIALIZED_SYSTEMPRINCIPAL;
}
}
// 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")
- 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); }
}
@@ -1187,36 +1182,33 @@ 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[WINDOW_RESTORE_IDS.get(aWindow)];
- let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1};
- this.restoreWindow(aWindow, state.windows[0], options);
+ // We want to restore windows after all windows have opened (since bug
+ // 1034036), so bail out here.
+ return;
// 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) {
-
+ } 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
this._globalState.setFromState(this._deferredInitialState);
this._restoreCount = this._deferredInitialState.windows ?
this._deferredInitialState.windows.length : 0;
this.restoreWindows(aWindow, this._deferredInitialState, {firstWindow: true});
this._deferredInitialState = null;
} else if (this._restoreLastWindow && aWindow.toolbar.visible &&
- this._closedWindows.length && !isPrivateWindow) {
-
+ this._closedWindows.length && !isPrivateWindow) {
// default to the most-recently closed window
// don't use popup windows
let closedWindowState = null;
let closedWindowIndex;
for (let i = 0; i < this._closedWindows.length; i++) {
// Take the first non-popup, point our object at it, and break out.
if (!this._closedWindows[i].isPopup) {
closedWindowState = this._closedWindows[i];
@@ -1281,16 +1273,24 @@ var SessionStoreInternal = {
* Called right before a new browser window is shown.
* @param aWindow
* Window reference
*/
onBeforeBrowserWindowShown(aWindow) {
// Register the window.
this.onLoad(aWindow);
+ // Some are waiting for this window to be shown, which is now, so let's resolve
+ // the deferred operation.
+ let deferred = WINDOW_SHOWING_PROMISES.get(aWindow);
+ if (deferred) {
+ deferred.resolve(aWindow);
+ WINDOW_SHOWING_PROMISES.delete(aWindow);
+ }
+
// 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
@@ -1356,17 +1356,17 @@ var SessionStoreInternal = {
// 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();
}
let restoreID = WINDOW_RESTORE_IDS.get(aWindow);
- this._windows[aWindow.__SSi] = this._statesToRestore[restoreID];
+ this._windows[aWindow.__SSi] = this._statesToRestore[restoreID].windows[0];
delete this._statesToRestore[restoreID];
WINDOW_RESTORE_IDS.delete(aWindow);
}
// ignore windows not tracked by SessionStore
if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
return completionPromise;
}
@@ -1582,20 +1582,22 @@ var SessionStoreInternal = {
}
}
},
/**
* On quit application granted
*/
onQuitApplicationGranted: function ssi_onQuitApplicationGranted(syncShutdown = false) {
- // Collect an initial snapshot of window data before we do the flush
- this._forEachBrowserWindow((win) => {
- this._collectWindowData(win);
- });
+ // Collect an initial snapshot of window data before we do the flush.
+ let index = 0;
+ for (let window of this._browserWindows) {
+ this._collectWindowData(window);
+ this._windows[window.__SSi].zIndex = ++index;
+ }
// Now add an AsyncShutdown blocker that'll spin the event loop
// until the windows have all been flushed.
// This progress object will track the state of async window flushing
// and will help us debug things that go wrong with our AsyncShutdown
// blocker.
let progress = { total: -1, current: -1 };
@@ -1670,29 +1672,29 @@ var SessionStoreInternal = {
*
* @return Promise
*/
async flushAllWindowsAsync(progress = {}) {
let windowPromises = new Map();
// We collect flush promises and close each window immediately so that
// the user can't start changing any window state while we're waiting
// for the flushes to finish.
- this._forEachBrowserWindow((win) => {
- windowPromises.set(win, TabStateFlusher.flushWindow(win));
+ for (let window of this._browserWindows) {
+ windowPromises.set(window, TabStateFlusher.flushWindow(window));
// We have to wait for these messages to come up from
// each window and each browser. In the meantime, hide
// the windows to improve perceived shutdown speed.
- let baseWin = win.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .QueryInterface(Ci.nsIDocShellTreeItem)
- .treeOwner
- .QueryInterface(Ci.nsIBaseWindow);
+ let baseWin = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .treeOwner
+ .QueryInterface(Ci.nsIBaseWindow);
baseWin.visibility = false;
- });
+ }
progress.total = windowPromises.size;
progress.current = 0;
// We'll iterate through the Promise array, yielding each one, so as to
// provide useful progress information to AsyncShutdown.
for (let [win, promise] of windowPromises) {
await promise;
@@ -1756,17 +1758,19 @@ var SessionStoreInternal = {
// session data on disk as this notification fires after the
// quit-application notification so the browser is about to exit.
if (RunState.isQuitting)
return;
LastSession.clear();
let openWindows = {};
// Collect open windows.
- this._forEachBrowserWindow(({__SSi: id}) => openWindows[id] = true);
+ for (let window of this._browserWindows) {
+ openWindows[window.__SSi] = true;
+ }
// also clear all data about closed tabs and windows
for (let ix in this._windows) {
if (ix in openWindows) {
if (this._windows[ix]._closedTabs.length) {
this._windows[ix]._closedTabs = [];
this._closedObjectsChanged = true;
}
@@ -2280,22 +2284,22 @@ var SessionStoreInternal = {
var window = this._getTopWindow();
if (!window) {
this._restoreCount = 1;
this._openWindowWithState(state);
return;
}
// close all other browser windows
- this._forEachBrowserWindow(function(aWindow) {
- if (aWindow != window) {
- aWindow.close();
- this.onClose(aWindow);
+ for (let otherWin of this._browserWindows) {
+ if (otherWin != window) {
+ otherWin.close();
+ this.onClose(otherWin);
}
- });
+ }
// make sure closed window data isn't kept
if (this._closedWindows.length) {
this._closedWindows = [];
this._closedObjectsChanged = true;
}
// determine how many windows are meant to be restored
@@ -2544,16 +2548,18 @@ 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;
+ WINDOW_SHOWING_PROMISES.get(window).promise.then(win =>
+ this.restoreWindows(win, state, {overwriteTabs: true}));
// Notify of changes to closed objects.
this._notifyOfClosedObjectsChange();
return window;
},
forgetClosedWindow: function ssi_forgetClosedWindow(aIndex) {
@@ -2793,20 +2799,20 @@ var SessionStoreInternal = {
if (!this.canRestoreLastSession) {
throw Components.Exception("Last session can not be restored");
}
Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE);
// First collect each window with its id...
let windows = {};
- this._forEachBrowserWindow(function(aWindow) {
- if (aWindow.__SS_lastSessionWindowID)
- windows[aWindow.__SS_lastSessionWindowID] = aWindow;
- });
+ for (let window of this._browserWindows) {
+ if (window.__SS_lastSessionWindowID)
+ windows[window.__SS_lastSessionWindowID] = window;
+ }
let lastSessionState = LastSession.getState();
// This shouldn't ever be the case...
if (!lastSessionState.windows.length) {
throw Components.Exception("lastSessionState has no windows", Cr.NS_ERROR_UNEXPECTED);
}
@@ -2822,16 +2828,19 @@ var SessionStoreInternal = {
let lastWindow = this._getTopWindow();
let canUseLastWindow = lastWindow &&
!lastWindow.__SS_lastSessionWindowID;
// global data must be restored before restoreWindow is called so that
// it happens before observers are notified
this._globalState.setFromState(lastSessionState);
+ let openWindows = [];
+ let windowsToOpen = [];
+
// Restore session cookies.
SessionCookies.restore(lastSessionState.cookies || []);
// 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
@@ -2853,27 +2862,34 @@ 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.
+ this._updateWindowRestoreState(windowToUse, {
+ windows: [winState],
+ options: {overwriteTabs: canOverwriteTabs}
+ });
+ openWindows.push(windowToUse);
} else {
- this._openWindowWithState({ windows: [winState] });
+ windowsToOpen.push(winState);
}
}
+ // Actually restore windows in reversed z-order.
+ this._openWindows({windows: windowsToOpen}).then(openedWindows =>
+ this._restoreWindowsInReversedZOrder(openWindows.concat(openedWindows)));
+
// 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);
@@ -3157,26 +3173,28 @@ var SessionStoreInternal = {
this._handleClosedWindows().then(() => {
this._notifyOfClosedObjectsChange();
});
var activeWindow = this._getTopWindow();
TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
if (RunState.isRunning) {
- // update the data for all windows with activities since the last save operation
- this._forEachBrowserWindow(function(aWindow) {
- if (!this._isWindowLoaded(aWindow)) // window data is still in _statesToRestore
- return;
- if (aUpdateAll || DirtyWindows.has(aWindow) || aWindow == activeWindow) {
- this._collectWindowData(aWindow);
+ // update the data for all windows with activities since the last save operation.
+ let index = 0;
+ for (let window of this._browserWindows) {
+ if (!this._isWindowLoaded(window)) // window data is still in _statesToRestore
+ continue;
+ if (aUpdateAll || DirtyWindows.has(window) || window == activeWindow) {
+ this._collectWindowData(window);
} else { // always update the window features (whose change alone never triggers a save operation)
- this._updateWindowFeatures(aWindow);
+ this._updateWindowFeatures(window);
}
- });
+ this._windows[window.__SSi].zIndex = ++index;
+ }
DirtyWindows.clear();
}
TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS");
// An array that at the end will hold all current window data.
var total = [];
// The ids of all windows contained in 'total' in the same order.
var ids = [];
@@ -3321,37 +3339,47 @@ 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) {
+ for (let winData of root.windows) {
+ if (!winData || !winData.tabs || !winData.tabs[0])
+ continue;
+ this._openWindowWithState({ windows: [winData] });
+ }
+ return Promise.all([...WINDOW_SHOWING_PROMISES.values()].map(deferred => deferred.promise));
+ },
+
+ /**
* 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.
@@ -3479,17 +3507,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) {
@@ -3575,36 +3602,66 @@ 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.
tabState.connectionPrepared = true;
}
},
/**
+ * This function will restore window features and then retore window data.
+ *
+ * @param windows
+ * ordered array of windows to restore
+ */
+ _restoreWindowsFeaturesAndTabs(windows) {
+ // First, we restore window features, so that when users start interacting
+ // with a window, we don't steal the window focus.
+ for (let window of windows) {
+ let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)];
+ this.restoreWindowFeatures(window, state.windows[0]);
+ }
+
+ // Then we restore data into windows.
+ for (let window of windows) {
+ let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(window)];
+ this.restoreWindow(window, state.windows[0], state.options || {overwriteTabs: true});
+ WINDOW_RESTORE_ZINDICES.delete(window);
+ }
+ },
+
+ /**
+ * This function will restore window in reversed z-index, so that users will
+ * be presented with most recently used window first.
+ *
+ * @param windows
+ * unordered array of windows to restore
+ */
+ _restoreWindowsInReversedZOrder(windows) {
+ windows.sort((a, b) =>
+ (WINDOW_RESTORE_ZINDICES.get(a) || 0) - (WINDOW_RESTORE_ZINDICES.get(b) || 0));
+
+ this.windowToFocus = windows[0];
+ this._restoreWindowsFeaturesAndTabs(windows);
+ },
+
+ /**
* 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
@@ -3620,34 +3677,31 @@ 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);
+ let firstWindowData = root.windows.splice(0, 1);
+ // 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, options: aOptions});
+
+ // Begin the restoration: First open all windows in creation order. After all
+ // windows have 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);
+ });
DevToolsShim.restoreDevToolsSession(aState);
},
/**
* Manage history restoration for a window
* @param aWindow
* Window to restore the tabs into
@@ -4220,28 +4274,24 @@ 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
- * (might miss the most recent one)
- * @param aFunc
- * Callback each window is passed to
+ * Iterator that yields all currently opened browser windows, in order.
+ * (Might miss the most recent one.)
*/
- _forEachBrowserWindow: function ssi_forEachBrowserWindow(aFunc) {
- var windowsEnum = Services.wm.getEnumerator("navigator:browser");
-
- while (windowsEnum.hasMoreElements()) {
- var window = windowsEnum.getNext();
- if (window.__SSi && !window.closed) {
- aFunc.call(this, window);
+ _browserWindows: {
+ * [Symbol.iterator]() {
+ for (let window of BrowserWindowTracker.orderedWindows) {
+ if (window.__SSi && !window.closed)
+ yield window;
}
}
},
/**
* Returns most recent window
* @returns Window reference
*/
@@ -4263,16 +4313,38 @@ 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) {
+ // Store z-index, so that windows can be restored in reversed z-order.
+ if ("zIndex" in state.windows[0]) {
+ WINDOW_RESTORE_ZINDICES.set(window, state.windows[0].zIndex);
+ }
+ do {
+ var ID = "window" + Math.random();
+ } while (ID in this._statesToRestore);
+ WINDOW_RESTORE_IDS.set(window, ID);
+ this._statesToRestore[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);
@@ -4290,21 +4362,18 @@ 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);
- WINDOW_RESTORE_IDS.set(window, ID);
- this._statesToRestore[ID] = aState;
+ this._updateWindowRestoreState(window, aState);
+ WINDOW_SHOWING_PROMISES.set(window, PromiseUtils.defer());
return window;
},
/**
* Whether or not to resume session, if not recovering from a crash.
* @returns bool
*/