Bug 810981 - Part 2 - Save the current zoom level when scrolling. r=kats r=margaret
authorJan Henning <jh+bugzilla@buttercookie.de>
Thu, 26 May 2016 23:31:15 +0200
changeset 340532 b92caaa61e809cc93e560c1fb9416e86446ed939
parent 340531 5e6a99ab5e9a9ff893f7e12e19639847cae068f0
child 340533 b37f2c10a77781e648367c21a66ff7e2e81cd848
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskats, margaret
bugs810981, 1270019
milestone49.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 810981 - Part 2 - Save the current zoom level when scrolling. r=kats r=margaret Since we're now recording the scroll position in the session data, it makes sense to store the zoom level as well. To do this, we make use of the new facility introduced in bug 1270019, which allows us to provide a desired initial resolution to the MobileViewportManager. For this to work, we need to send our desired zoom level before the MVM calculates the initial page resolution, i.e. before first paint/page load. Therefore, we now have browser.js notify us of location change events, so we can set the zoom level to restore early enough. This also means that we can no longer restore the scroll position on load, because the MobileViewportManager applies the resolution we provide it via utils.setRestoreResolution() only after the first paint or page load ( whichever happens earlier). In the latter case, our JS load event handler will run shortly before the MVM has applied the desired zoom level in its own event handling code, which means that any scroll attempt which depends on the page already being zoomed in will fail to apply. Therefore, the scroll position restoring needs to be moved to a later point in time, i.e. in this case the "pageshow" event. MozReview-Commit-ID: 6NtYqc8pm3N
mobile/android/chrome/content/browser.js
mobile/android/components/SessionStore.js
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4463,16 +4463,17 @@ Tab.prototype = {
 
     Messaging.sendRequest(message);
 
     if (!sameDocument) {
       // XXX This code assumes that this is the earliest hook we have at which
       // browser.contentDocument is changed to the new document we're loading
       this.contentDocumentIsDisplayed = false;
       this.hasTouchListener = false;
+      Services.obs.notifyObservers(this.browser, "Session:NotifyLocationChange", null);
     } else {
       setTimeout(function() {
         this.sendViewportUpdate();
       }.bind(this), 0);
     }
   },
 
   _stripAboutReaderURL: function (url) {
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -107,16 +107,17 @@ SessionStore.prototype = {
     let observerService = Services.obs;
     switch (aTopic) {
       case "app-startup":
         observerService.addObserver(this, "final-ui-startup", true);
         observerService.addObserver(this, "domwindowopened", true);
         observerService.addObserver(this, "domwindowclosed", true);
         observerService.addObserver(this, "browser:purge-session-history", true);
         observerService.addObserver(this, "Session:Restore", true);
+        observerService.addObserver(this, "Session:NotifyLocationChange", true);
         observerService.addObserver(this, "application-background", true);
         observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
         observerService.addObserver(this, "ClosedTabs:StopNotifications", true);
         observerService.addObserver(this, "last-pb-context-exited", true);
         observerService.addObserver(this, "Session:RestoreRecentTabs", true);
         observerService.addObserver(this, "Tabs:OpenMultiple", true);
         break;
       case "final-ui-startup":
@@ -183,16 +184,24 @@ SessionStore.prototype = {
           let data = JSON.parse(aData);
           this.restoreLastSession(data.sessionString);
         } else {
           // Not doing a restore; just send restore message
           Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
         }
         break;
       }
+      case "Session:NotifyLocationChange": {
+        let browser = aSubject;
+        if (browser.__SS_restoreDataOnLocationChange) {
+          delete browser.__SS_restoreDataOnLocationChange;
+          this._restoreZoom(browser.__SS_data.scrolldata, browser);
+        }
+        break;
+      }
       case "Tabs:OpenMultiple": {
         let data = JSON.parse(aData);
 
         this._openTabs(data);
 
         if (data.shouldNotifyTabsOpenedToJava) {
           Messaging.sendRequest({
             type: "Tabs:TabsOpened"
@@ -279,43 +288,63 @@ SessionStore.prototype = {
       case "load": {
         let browser = aEvent.currentTarget;
 
         // Skip subframe loads.
         if (browser.contentDocument !== aEvent.originalTarget) {
           return;
         }
 
-        // Handle restoring the scroll position and text data into the content
-        // and frames. We wait until the main content and all frames are loaded
+        // Handle restoring the text data into the content and frames.
+        // We wait until the main content and all frames are loaded
         // before trying to restore this data.
         log("load for tab " + window.BrowserApp.getTabForBrowser(browser).id);
         if (browser.__SS_restoreDataOnLoad) {
           delete browser.__SS_restoreDataOnLoad;
           this._restoreTextData(browser.__SS_data.formdata, browser);
+        }
+        break;
+      }
+      case "pageshow": {
+        let browser = aEvent.currentTarget;
+
+        // Skip subframe pageshows.
+        if (browser.contentDocument !== aEvent.originalTarget) {
+          return;
+        }
+
+        // Restoring the scroll position needs to happen after the zoom level has been
+        // restored, which is done by the MobileViewportManager either on first paint
+        // or on load, whichever comes first.
+        // In the latter case, our load handler runs before the MVM's one, which is the
+        // wrong way around, so we have to use a later event instead.
+        log("pageshow for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+        if (browser.__SS_restoreDataOnPageshow) {
+          delete browser.__SS_restoreDataOnPageshow;
           this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
         } else {
-          // We're not restoring, capture the initial scroll position on load.
+          // We're not restoring, capture the initial scroll position on pageshow.
           this.onTabScroll(window, browser);
         }
         break;
       }
       case "change":
       case "input":
       case "DOMAutoComplete": {
         let browser = aEvent.currentTarget;
         log("TabInput for tab " + window.BrowserApp.getTabForBrowser(browser).id);
         this.onTabInput(window, browser);
         break;
       }
+      case "resize":
       case "scroll": {
         let browser = aEvent.currentTarget;
         // Duplicated logging check to avoid calling getTabForBrowser on each scroll event.
         if (loggingEnabled) {
-          log("scroll for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+          log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
         }
         if (!this._scrollSavePending) {
           this._scrollSavePending =
             window.setTimeout(() => {
               this._scrollSavePending = null;
               this.onTabScroll(window, browser);
             }, 500);
         }
@@ -392,40 +421,47 @@ SessionStore.prototype = {
 
   onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
     // Use DOMTitleChange to catch the initial load and restore history
     aBrowser.addEventListener("DOMTitleChanged", this, true);
 
     // Use load to restore text data
     aBrowser.addEventListener("load", this, true);
 
+    // Gecko might set the initial zoom level after the JS "load" event,
+    // so we have to restore zoom and scroll position after that.
+    aBrowser.addEventListener("pageshow", this, true);
+
     // Use a combination of events to watch for text data changes
     aBrowser.addEventListener("change", this, true);
     aBrowser.addEventListener("input", this, true);
     aBrowser.addEventListener("DOMAutoComplete", this, true);
 
-    // Record the current scroll position
+    // Record the current scroll position and zoom level.
     aBrowser.addEventListener("scroll", this, true);
+    aBrowser.addEventListener("resize", this, true);
 
     log("onTabAdd() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
         ", aNoNotification = " + aNoNotification);
     if (!aNoNotification) {
       this.saveStateDelayed();
     }
     this._updateCrashReportURL(aWindow);
   },
 
   onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
     // Cleanup event listeners
     aBrowser.removeEventListener("DOMTitleChanged", this, true);
     aBrowser.removeEventListener("load", this, true);
+    aBrowser.removeEventListener("pageshow", this, true);
     aBrowser.removeEventListener("change", this, true);
     aBrowser.removeEventListener("input", this, true);
     aBrowser.removeEventListener("DOMAutoComplete", this, true);
     aBrowser.removeEventListener("scroll", this, true);
+    aBrowser.removeEventListener("resize", this, true);
 
     let tabId = aWindow.BrowserApp.getTabForBrowser(aBrowser).id;
 
     // If this browser is being restored, skip any session save activity
     if (aBrowser.__SS_restore) {
       log("onTabRemove() ran for zombie tab " + tabId + ", aNoNotification = " + aNoNotification);
       return;
     }
@@ -502,19 +538,19 @@ SessionStore.prototype = {
     let scrolldata;
     if (aBrowser.__SS_data) {
       formdata = aBrowser.__SS_data.formdata;
       scrolldata = aBrowser.__SS_data.scrolldata;
     }
     delete aBrowser.__SS_data;
 
     this._collectTabData(aWindow, aBrowser, data);
-    if (aBrowser.__SS_restoreDataOnLoad) {
-      // If the tab has been freshly restored and the "load" event
-      // hasn't yet fired, we need to restore any form data and
+    if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
+      // If the tab has been freshly restored and the "load" or "pageshow"
+      // events haven't yet fired, we need to preserve any form data and
       // scroll positions that might have been present.
       aBrowser.__SS_data.formdata = formdata;
       aBrowser.__SS_data.scrolldata = scrolldata;
     } else {
       // When navigating via the forward/back buttons, Gecko restores
       // the form data all by itself and doesn't invoke any input events.
       // As _collectTabData() doesn't save any form data, we need to manually
       // capture it to bridge the time until the next input event arrives.
@@ -629,17 +665,17 @@ SessionStore.prototype = {
 
     // Don't bother trying to save scroll positions if we don't have history yet.
     let data = aBrowser.__SS_data;
     if (!data || data.entries.length == 0) {
       return;
     }
 
     // Neither bother if we're yet to restore the previous scroll position.
-    if (aBrowser.__SS_restoreDataOnLoad) {
+    if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
       return;
     }
 
     // Start with storing the main content.
     let content = aBrowser.contentWindow;
 
     // Store the main content.
     let scrolldata = ScrollPosition.collect(content) || {};
@@ -655,22 +691,28 @@ SessionStore.prototype = {
       }
     }
 
     // If any frame had scroll positions, add them to the main scroll data.
     if (children.length) {
       scrolldata.children = children;
     }
 
-    // If we found any scroll positions, main content or frames, let's save them.
-    if (Object.keys(scrolldata).length) {
-      data.scrolldata = scrolldata;
-      log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
-      this.saveStateDelayed();
-    }
+    // Save the current document resolution.
+    let zoom = { value: 1 };
+    content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
+      Ci.nsIDOMWindowUtils).getResolution(zoom);
+    scrolldata.zoom = {};
+    scrolldata.zoom.resolution = zoom.value;
+    log("onTabScroll() zoom level: " + zoom.value);
+
+    // Save zoom and scroll data.
+    data.scrolldata = scrolldata;
+    log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+    this.saveStateDelayed();
   },
 
   saveStateDelayed: function ss_saveStateDelayed() {
     if (!this._saveTimer) {
       // Interval until the next disk operation is allowed
       let minimalDelay = this._lastSaveTime + this._interval - Date.now();
 
       // If we have to wait, set a timer, otherwise saveState directly
@@ -1204,19 +1246,27 @@ SessionStore.prototype = {
     // aTabData shouldn't be empty here, but if it is,
     // _restoreHistory() will crash otherwise.
     if (!aTabData || aTabData.entries.length == 0) {
       Cu.reportError("SessionStore.js: Error trying to restore tab with empty tabdata");
       return;
     }
     this._restoreHistory(aTabData, aBrowser.sessionHistory);
 
-    // Restoring text data and scroll position requires waiting for the content
-    // to load. So we set a flag and delay this until the appropriate event.
+    // Various bits of state can only be restored if page loading has progressed far enough:
+    // The MobileViewportManager needs to be told as early as possible about
+    // our desired zoom level so it can take it into account during the
+    // initial document resolution calculation.
+    aBrowser.__SS_restoreDataOnLocationChange = true;
+    // Restoring saved form data requires the input fields to be available,
+    // so we have to wait for the content to load.
     aBrowser.__SS_restoreDataOnLoad = true;
+    // Restoring the scroll position depends on the document resolution having been set,
+    // which is only guaranteed to have happened *after* we receive the load event.
+    aBrowser.__SS_restoreDataOnPageshow = true;
   },
 
   /**
   * Takes serialized history data and create news entries into the given
   * nsISessionHistory object.
   */
   _restoreHistory: function ss_restoreHistory(aTabData, aHistory) {
     if (aHistory.count > 0) {
@@ -1254,16 +1304,30 @@ SessionStore.prototype = {
   _restoreTextData: function ss_restoreTextData(aFormData, aBrowser) {
     if (aFormData) {
       log("_restoreTextData()");
       FormData.restoreTree(aBrowser.contentWindow, aFormData);
     }
   },
 
   /**
+  * Restores the zoom level of the window. This needs to be called before
+  * first paint/load (whichever comes first) to take any effect.
+  */
+  _restoreZoom: function ss_restoreZoom(aScrollData, aBrowser) {
+    if (aScrollData && aScrollData.zoom) {
+      log("_restoreZoom(), resolution: " + aScrollData.zoom.resolution);
+      let utils = aBrowser.contentWindow.QueryInterface(
+        Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      // Restore zoom level.
+      utils.setRestoreResolution(aScrollData.zoom.resolution);
+    }
+  },
+
+  /**
   * Takes serialized scroll positions and restores them into the given browser.
   */
   _restoreScrollPosition: function ss_restoreScrollPosition(aScrollData, aBrowser) {
     if (aScrollData) {
       log("_restoreScrollPosition()");
       ScrollPosition.restoreTree(aBrowser.contentWindow, aScrollData);
     }
   },