Bug 1034036 - Part 1: Separate windows opening and windows restoration process. Make windows restored in reversed z-order. r=mikedeboer
authorbeekill <nnn_bikiu0707@yahoo.com>
Fri, 21 Jul 2017 10:00:32 +0700
changeset 379783 1576132e98a33b5034f1cca833c8fa4a8ab5e4d2
parent 379782 6af4edb9979ef0c3f2d6ae9fe0afc9249d34c32c
child 379784 b9fdf5d1b40222f6a6dcf41334d643a43d6b40ed
push id94742
push userkwierso@gmail.com
push dateFri, 08 Sep 2017 20:41:39 +0000
treeherdermozilla-inbound@e78342f09ee6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1034036
milestone57.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 1: Separate windows opening and windows restoration process. Make windows restored in reversed z-order. r=mikedeboer MozReview-Commit-ID: Faa8fnHRVvw
browser/components/sessionstore/SessionStore.jsm
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -203,16 +203,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;
   },
@@ -687,18 +696,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); }
@@ -1174,19 +1184,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
@@ -1268,16 +1280,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
@@ -1342,17 +1358,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;
     }
@@ -2508,16 +2524,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) {
@@ -2761,16 +2778,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
@@ -2789,27 +2809,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);
@@ -3051,27 +3082,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;
 
@@ -3256,37 +3309,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.
@@ -3413,17 +3478,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) {
@@ -3512,36 +3576,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
@@ -3557,34 +3663,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
@@ -4162,28 +4272,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
    */
@@ -4205,16 +4339,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);
@@ -4232,20 +4383,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
    */