Bug 921942 - Broadcast scroll positions r=yoric

From 5f535195e10d6cccbedbdf607ff194450a40c4ed Mon Sep 17 00:00:00 2001
This commit is contained in:
Tim Taubert 2013-12-02 06:18:44 +01:00
Родитель 741546f1ca
Коммит c9b3ca4e13
6 изменённых файлов: 412 добавлений и 27 удалений

Просмотреть файл

@ -16,12 +16,12 @@ let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource:///modules/sessionstore/Utils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
"resource:///modules/sessionstore/PageStyle.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource:///modules/sessionstore/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
"resource:///modules/sessionstore/SessionHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
@ -29,6 +29,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
"resource:///modules/sessionstore/TextAndScrollData.jsm");
Cu.import("resource:///modules/sessionstore/FrameTree.jsm", this);
let gFrameTree = new FrameTree(this);
/**
* Returns a lazy function that will evaluate the given
* function |fn| only once and cache its return value.
@ -78,7 +81,7 @@ let EventListener = {
handleEvent: function (event) {
switch (event.type) {
case "pageshow":
if (event.persisted)
if (event.persisted && event.target == content.document)
sendAsyncMessage("SessionStore:pageshow");
break;
case "input":
@ -198,6 +201,43 @@ let ProgressListener = {
Ci.nsISupportsWeakReference])
};
/**
* Listens for scroll position changes. Whenever the user scrolls the top-most
* frame we update the scroll position and will restore it when requested.
*
* Causes a SessionStore:update message to be sent that contains the current
* scroll positions as a tree of strings. If no frame of the whole frame tree
* is scrolled this will return null so that we don't tack a property onto
* the tabData object in the parent process.
*
* Example:
* {scroll: "100,100", children: [null, null, {scroll: "200,200"}]}
*/
let ScrollPositionListener = {
init: function () {
addEventListener("scroll", this);
gFrameTree.addObserver(this);
},
handleEvent: function (event) {
let frame = event.target && event.target.defaultView;
// Don't collect scroll data for frames created at or after the load event
// as SessionStore can't restore scroll data for those.
if (frame && gFrameTree.contains(frame)) {
MessageQueue.push("scroll", () => this.collect());
}
},
onFrameTreeReset: function () {
MessageQueue.push("scroll", () => null);
},
collect: function () {
return gFrameTree.map(ScrollPosition.collect);
}
};
/**
* Listens for changes to the page style. Whenever a different page style is
* selected or author styles are enabled/disabled we send a message with the
@ -489,5 +529,6 @@ SyncHandler.init();
ProgressListener.init();
PageStyleListener.init();
SessionStorageListener.init();
ScrollPositionListener.init();
DocShellCapabilitiesListener.init();
PrivacyListener.init();

Просмотреть файл

@ -0,0 +1,217 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FrameTree"];
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
const EXPORTED_METHODS = ["addObserver", "contains", "map"];
/**
* A FrameTree represents all frames that were reachable when the document
* was loaded. We use this information to ignore frames when collecting
* sessionstore data as we can't currently restore anything for frames that
* have been created dynamically after or at the load event.
*
* @constructor
*/
function FrameTree(chromeGlobal) {
let internal = new FrameTreeInternal(chromeGlobal);
let external = {};
for (let method of EXPORTED_METHODS) {
external[method] = internal[method].bind(internal);
}
return Object.freeze(external);
}
/**
* The internal frame tree API that the public one points to.
*
* @constructor
*/
function FrameTreeInternal(chromeGlobal) {
// A WeakMap that uses frames (DOMWindows) as keys and their initial indices
// in their parents' child lists as values. Suppose we have a root frame with
// three subframes i.e. a page with three iframes. The WeakMap would have
// four entries and look as follows:
//
// root -> 0
// subframe1 -> 0
// subframe2 -> 1
// subframe3 -> 2
//
// Should one of the subframes disappear we will stop collecting data for it
// as |this._frames.has(frame) == false|. All other subframes will maintain
// their initial indices to ensure we can restore frame data appropriately.
this._frames = new WeakMap();
// The Set of observers that will be notified when the frame changes.
this._observers = new Set();
// The chrome global we use to retrieve the current DOMWindow.
this._chromeGlobal = chromeGlobal;
// Register a web progress listener to be notified about new page loads.
let docShell = chromeGlobal.docShell;
let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
let webProgress = ifreq.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
}
FrameTreeInternal.prototype = {
// Returns the docShell's current global.
get content() {
return this._chromeGlobal.content;
},
/**
* Adds a given observer |obs| to the set of observers that will be notified
* when the frame tree is reset (when a new document starts loading) or
* recollected (when a document finishes loading).
*
* @param obs (object)
*/
addObserver: function (obs) {
this._observers.add(obs);
},
/**
* Notifies all observers that implement the given |method|.
*
* @param method (string)
*/
notifyObservers: function (method) {
for (let obs of this._observers) {
if (obs.hasOwnProperty(method)) {
obs[method]();
}
}
},
/**
* Checks whether a given |frame| is contained in the collected frame tree.
* If it is not, this indicates that we should not collect data for it.
*
* @param frame (nsIDOMWindow)
* @return bool
*/
contains: function (frame) {
return this._frames.has(frame);
},
/**
* Recursively applies the given function |cb| to the stored frame tree. Use
* this method to collect sessionstore data for all reachable frames stored
* in the frame tree.
*
* If a given function |cb| returns a value, it must be an object. It may
* however return "null" to indicate that there is no data to be stored for
* the given frame.
*
* The object returned by |cb| cannot have any property named "children" as
* that is used to store information about subframes in the tree returned
* by |map()| and might be overridden.
*
* @param cb (function)
* @return object
*/
map: function (cb) {
let frames = this._frames;
function walk(frame) {
let obj = cb(frame) || {};
if (frames.has(frame)) {
let children = [];
Array.forEach(frame.frames, subframe => {
// Don't collect any data if the frame is not contained in the
// initial frame tree. It's a dynamic frame added later.
if (!frames.has(subframe)) {
return;
}
// Retrieve the frame's original position in its parent's child list.
let index = frames.get(subframe);
// Recursively collect data for the current subframe.
let result = walk(subframe, cb);
if (result && Object.keys(result).length) {
children[index] = result;
}
});
if (children.length) {
obj.children = children;
}
}
return Object.keys(obj).length ? obj : null;
}
return walk(this.content);
},
/**
* Stores a given |frame| and its children in the frame tree.
*
* @param frame (nsIDOMWindow)
* @param index (int)
* The index in the given frame's parent's child list.
*/
collect: function (frame, index = 0) {
// Mark the given frame as contained in the frame tree.
this._frames.set(frame, index);
// Mark the given frame's subframes as contained in the tree.
Array.forEach(frame.frames, this.collect, this);
},
/**
* @see nsIWebProgressListener.onStateChange
*
* We want to be notified about:
* - new documents that start loading to clear the current frame tree;
* - completed document loads to recollect reachable frames.
*/
onStateChange: function (webProgress, request, stateFlags, status) {
// Ignore state changes for subframes because we're only interested in the
// top-document starting or stopping its load. We thus only care about any
// changes to the root of the frame tree, not to any of its nodes/leafs.
if (!webProgress.isTopLevel || webProgress.DOMWindow != this.content) {
return;
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
// Clear the list of frames until we can recollect it.
this._frames.clear();
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeReset");
} else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
// The document and its resources have finished loading.
this.collect(webProgress.DOMWindow);
// Notify observers that the frame tree has been reset.
this.notifyObservers("onFrameTreeCollected");
}
},
// Unused nsIWebProgressListener methods.
onLocationChange: function () {},
onProgressChange: function () {},
onSecurityChange: function () {},
onStatusChange: function () {},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference])
};

Просмотреть файл

@ -0,0 +1,90 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["ScrollPosition"];
const Ci = Components.interfaces;
/**
* It provides methods to collect and restore scroll positions for single
* frames and frame trees.
*
* This is a child process module.
*/
this.ScrollPosition = Object.freeze({
/**
* Collects scroll position data for any given |frame| in the frame hierarchy.
*
* @param frame (DOMWindow)
*
* @return {scroll: "x,y"} e.g. {scroll: "100,200"}
* Returns null when there is no scroll data we want to store for the
* given |frame|.
*/
collect: function (frame) {
let ifreq = frame.QueryInterface(Ci.nsIInterfaceRequestor);
let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {};
utils.getScrollXY(false /* no layout flush */, scrollX, scrollY);
if (scrollX.value || scrollY.value) {
return {scroll: scrollX.value + "," + scrollY.value};
}
return null;
},
/**
* Restores scroll position data for any given |frame| in the frame hierarchy.
*
* @param frame (DOMWindow)
* @param value (object, see collect())
*/
restore: function (frame, value) {
let match;
if (value && (match = /(\d+),(\d+)/.exec(value))) {
frame.scrollTo(match[1], match[2]);
}
},
/**
* Restores scroll position data for the current frame hierarchy starting at
* |root| using the given scroll position |data|.
*
* If the given |root| frame's hierarchy doesn't match that of the given
* |data| object we will silently discard data for unreachable frames. We
* may as well assign scroll positions to the wrong frames if some were
* reordered or removed.
*
* @param root (DOMWindow)
* @param data (object)
* {
* scroll: "100,200",
* children: [
* {scroll: "100,200"},
* null,
* {scroll: "200,300", children: [ ... ]}
* ]
* }
*/
restoreTree: function (root, data) {
if (data.hasOwnProperty("scroll")) {
this.restore(root, data.scroll);
}
if (!data.hasOwnProperty("children")) {
return;
}
let frames = root.frames;
data.children.forEach((child, index) => {
if (child && index < frames.length) {
this.restoreTree(frames[index], child);
}
});
}
});

Просмотреть файл

@ -110,6 +110,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
"resource:///modules/devtools/scratchpad-manager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource:///modules/sessionstore/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
"resource:///modules/sessionstore/SessionSaver.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
@ -630,14 +632,17 @@ let SessionStoreInternal = {
let browser;
switch (aEvent.type) {
case "load":
// If __SS_restore_data is set, then we need to restore the document
// (form data, scrolling, etc.). This will only happen when a tab is
// first restored.
browser = aEvent.currentTarget;
TabStateCache.delete(browser);
if (browser.__SS_restore_data)
this.restoreDocument(win, browser, aEvent);
this.onTabLoad(win, browser);
// Ignore load events from subframes.
if (aEvent.target == browser.contentDocument) {
// If __SS_restore_data is set, then we need to restore the document
// (form data, scrolling, etc.). This will only happen when a tab is
// first restored.
TabStateCache.delete(browser);
if (browser.__SS_restore_data)
this.restoreDocument(win, browser, aEvent);
this.onTabLoad(win, browser);
}
break;
case "SwapDocShells":
browser = aEvent.currentTarget;
@ -2654,6 +2659,7 @@ let SessionStoreInternal = {
// Update the persistent tab state cache with |tabData| information.
TabStateCache.updatePersistent(browser, {
scroll: tabData.scroll || null,
storage: tabData.storage || null,
disallow: tabData.disallow || null,
pageStyle: tabData.pageStyle || null
@ -2821,8 +2827,15 @@ let SessionStoreInternal = {
// restore those aspects of the currently active documents which are not
// preserved in the plain history entries (mainly scroll state and text data)
browser.__SS_restore_data = tabData.entries[activeIndex] || {};
browser.__SS_restore_pageStyle = tabData.pageStyle || "";
browser.__SS_restore_tab = aTab;
if (tabData.pageStyle) {
RestoreData.set(browser, "pageStyle", tabData.pageStyle);
}
if (tabData.scroll) {
RestoreData.set(browser, "scroll", tabData.scroll);
}
didStartLoad = true;
try {
// In order to work around certain issues in session history, we need to
@ -2837,7 +2850,6 @@ let SessionStoreInternal = {
}
} else {
browser.__SS_restore_data = {};
browser.__SS_restore_pageStyle = "";
browser.__SS_restore_tab = aTab;
browser.loadURIWithFlags("about:blank",
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
@ -2949,7 +2961,11 @@ let SessionStoreInternal = {
}
let frameList = this.getFramesToRestore(aBrowser);
PageStyle.restore(aBrowser.docShell, frameList, aBrowser.__SS_restore_pageStyle);
let pageStyle = RestoreData.get(aBrowser, "pageStyle") || "";
let scrollPositions = RestoreData.get(aBrowser, "scroll") || {};
PageStyle.restore(aBrowser.docShell, frameList, pageStyle);
ScrollPosition.restoreTree(aBrowser.contentWindow, scrollPositions);
TextAndScrollData.restore(frameList);
let tab = aBrowser.__SS_restore_tab;
@ -2958,8 +2974,8 @@ let SessionStoreInternal = {
// done with that now.
delete aBrowser.__SS_data;
delete aBrowser.__SS_restore_data;
delete aBrowser.__SS_restore_pageStyle;
delete aBrowser.__SS_restore_tab;
RestoreData.clear(aBrowser);
// Notify the tabbrowser that this document has been completely
// restored. Do so after restoration is completely finished and
@ -4127,4 +4143,32 @@ let GlobalState = {
setFromState: function (aState) {
this.state = (aState && aState.global) || {};
}
}
};
/**
* Keeps track of data that needs to be restored after the tab's document
* has been loaded. This includes scroll positions, form data, and page style.
*/
let RestoreData = {
_data: new WeakMap(),
get: function (browser, key) {
if (!this._data.has(browser)) {
return null;
}
return this._data.get(browser).get(key);
},
set: function (browser, key, value) {
if (!this._data.has(browser)) {
this._data.set(browser, new Map());
}
this._data.get(browser).set(key, value);
},
clear: function (browser) {
this._data.delete(browser);
}
};

Просмотреть файл

@ -16,6 +16,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
"resource:///modules/sessionstore/DocumentUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
"resource:///modules/sessionstore/PrivacyLevel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
"resource:///modules/sessionstore/ScrollPosition.jsm");
/**
* The external API exported by this module.
@ -78,14 +80,6 @@ let TextAndScrollDataInternal = {
entry.innerHTML = content.document.body.innerHTML;
}
}
// get scroll position from nsIDOMWindowUtils, since it allows avoiding a
// flush of layout
let domWindowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {};
domWindowUtils.getScrollXY(false, scrollX, scrollY);
entry.scroll = scrollX.value + "," + scrollY.value;
},
isAboutSessionRestore: function (url) {
@ -146,9 +140,6 @@ let TextAndScrollDataInternal = {
}, 0);
}
let match;
if (data.scroll && (match = /(\d+),(\d+)/.exec(data.scroll)) != null) {
content.scrollTo(match[1], match[2]);
}
ScrollPosition.restore(content, data.scroll || "");
},
};

Просмотреть файл

@ -15,10 +15,12 @@ JS_MODULES_PATH = 'modules/sessionstore'
EXTRA_JS_MODULES = [
'DocShellCapabilities.jsm',
'DocumentUtils.jsm',
'FrameTree.jsm',
'Messenger.jsm',
'PageStyle.jsm',
'PrivacyLevel.jsm',
'RecentlyClosedTabsAndWindowsMenuUtils.jsm',
'ScrollPosition.jsm',
'SessionCookies.jsm',
'SessionFile.jsm',
'SessionHistory.jsm',