Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["CustomizationTabPreloader"];
9 const Cu = Components.utils;
10 const Cc = Components.classes;
11 const Ci = Components.interfaces;
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/Promise.jsm");
16 const HTML_NS = "http://www.w3.org/1999/xhtml";
17 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
18 const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
19 const CUSTOMIZATION_URL = "about:customizing";
21 // The interval between swapping in a preload docShell and kicking off the
22 // next preload in the background.
23 const PRELOADER_INTERVAL_MS = 600;
25 function createTimer(obj, delay) {
26 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
27 timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
28 return timer;
29 }
31 function clearTimer(timer) {
32 if (timer) {
33 timer.cancel();
34 }
35 return null;
36 }
38 this.CustomizationTabPreloader = {
39 uninit: function () {
40 CustomizationTabPreloaderInternal.uninit();
41 },
43 newTab: function (aTab) {
44 return CustomizationTabPreloaderInternal.newTab(aTab);
45 },
47 /**
48 * ensurePreloading starts the preloading of the about:customizing
49 * content page. This function is idempotent (until a call to uninit),
50 * so multiple calls to it are fine.
51 */
52 ensurePreloading: function() {
53 CustomizationTabPreloaderInternal.ensurePreloading();
54 },
55 };
57 Object.freeze(CustomizationTabPreloader);
59 this.CustomizationTabPreloaderInternal = {
60 _browser: null,
62 uninit: function () {
63 HostFrame.destroy();
65 if (this._browser) {
66 this._browser.destroy();
67 this._browser = null;
68 }
69 },
71 newTab: function (aTab) {
72 let win = aTab.ownerDocument.defaultView;
73 if (win.gBrowser && this._browser) {
74 return this._browser.swapWithNewTab(aTab);
75 }
77 return false;
78 },
80 ensurePreloading: function () {
81 if (!this._browser) {
82 this._browser = new HiddenBrowser();
83 }
84 }
85 };
87 function HiddenBrowser() {
88 this._createBrowser();
89 }
91 HiddenBrowser.prototype = {
92 _timer: null,
94 get isPreloaded() {
95 return this._browser &&
96 this._browser.contentDocument &&
97 this._browser.contentDocument.readyState === "complete" &&
98 this._browser.currentURI.spec === CUSTOMIZATION_URL;
99 },
101 swapWithNewTab: function (aTab) {
102 if (!this.isPreloaded || this._timer) {
103 return false;
104 }
106 let win = aTab.ownerDocument.defaultView;
107 let tabbrowser = win.gBrowser;
109 if (!tabbrowser) {
110 return false;
111 }
113 // Swap docShells.
114 tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
116 // Load all default frame scripts attached to the target window.
117 let mm = aTab.linkedBrowser.messageManager;
118 let scripts = win.messageManager.getDelayedFrameScripts();
119 Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal));
121 // Remove the browser, it will be recreated by a timer.
122 this._removeBrowser();
124 // Start a timer that will kick off preloading the next page.
125 this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
127 // Signal that we swapped docShells.
128 return true;
129 },
131 observe: function () {
132 this._timer = null;
134 // Start pre-loading the customization page.
135 this._createBrowser();
136 },
138 destroy: function () {
139 this._removeBrowser();
140 this._timer = clearTimer(this._timer);
141 },
143 _createBrowser: function () {
144 HostFrame.get().then(aFrame => {
145 let doc = aFrame.document;
146 this._browser = doc.createElementNS(XUL_NS, "browser");
147 this._browser.setAttribute("type", "content");
148 this._browser.setAttribute("src", CUSTOMIZATION_URL);
149 this._browser.style.width = "400px";
150 this._browser.style.height = "400px";
151 doc.getElementById("win").appendChild(this._browser);
152 });
153 },
155 _removeBrowser: function () {
156 if (this._browser) {
157 this._browser.remove();
158 this._browser = null;
159 }
160 }
161 };
163 let HostFrame = {
164 _frame: null,
165 _deferred: null,
167 get hiddenDOMDocument() {
168 return Services.appShell.hiddenDOMWindow.document;
169 },
171 get isReady() {
172 return this.hiddenDOMDocument.readyState === "complete";
173 },
175 get: function () {
176 if (!this._deferred) {
177 this._deferred = Promise.defer();
178 this._create();
179 }
181 return this._deferred.promise;
182 },
184 destroy: function () {
185 if (this._frame) {
186 if (!Cu.isDeadWrapper(this._frame)) {
187 this._frame.removeEventListener("load", this, true);
188 this._frame.remove();
189 }
191 this._frame = null;
192 this._deferred = null;
193 }
194 },
196 handleEvent: function () {
197 let contentWindow = this._frame.contentWindow;
198 if (contentWindow.location.href === XUL_PAGE) {
199 this._frame.removeEventListener("load", this, true);
200 this._deferred.resolve(contentWindow);
201 } else {
202 contentWindow.location = XUL_PAGE;
203 }
204 },
206 _create: function () {
207 if (this.isReady) {
208 let doc = this.hiddenDOMDocument;
209 this._frame = doc.createElementNS(HTML_NS, "iframe");
210 this._frame.addEventListener("load", this, true);
211 doc.documentElement.appendChild(this._frame);
212 } else {
213 let flags = Ci.nsIThread.DISPATCH_NORMAL;
214 Services.tm.currentThread.dispatch(() => this._create(), flags);
215 }
216 }
217 };