|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["CustomizationTabPreloader"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 |
|
13 Cu.import("resource://gre/modules/Services.jsm"); |
|
14 Cu.import("resource://gre/modules/Promise.jsm"); |
|
15 |
|
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"; |
|
20 |
|
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; |
|
24 |
|
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 } |
|
30 |
|
31 function clearTimer(timer) { |
|
32 if (timer) { |
|
33 timer.cancel(); |
|
34 } |
|
35 return null; |
|
36 } |
|
37 |
|
38 this.CustomizationTabPreloader = { |
|
39 uninit: function () { |
|
40 CustomizationTabPreloaderInternal.uninit(); |
|
41 }, |
|
42 |
|
43 newTab: function (aTab) { |
|
44 return CustomizationTabPreloaderInternal.newTab(aTab); |
|
45 }, |
|
46 |
|
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 }; |
|
56 |
|
57 Object.freeze(CustomizationTabPreloader); |
|
58 |
|
59 this.CustomizationTabPreloaderInternal = { |
|
60 _browser: null, |
|
61 |
|
62 uninit: function () { |
|
63 HostFrame.destroy(); |
|
64 |
|
65 if (this._browser) { |
|
66 this._browser.destroy(); |
|
67 this._browser = null; |
|
68 } |
|
69 }, |
|
70 |
|
71 newTab: function (aTab) { |
|
72 let win = aTab.ownerDocument.defaultView; |
|
73 if (win.gBrowser && this._browser) { |
|
74 return this._browser.swapWithNewTab(aTab); |
|
75 } |
|
76 |
|
77 return false; |
|
78 }, |
|
79 |
|
80 ensurePreloading: function () { |
|
81 if (!this._browser) { |
|
82 this._browser = new HiddenBrowser(); |
|
83 } |
|
84 } |
|
85 }; |
|
86 |
|
87 function HiddenBrowser() { |
|
88 this._createBrowser(); |
|
89 } |
|
90 |
|
91 HiddenBrowser.prototype = { |
|
92 _timer: null, |
|
93 |
|
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 }, |
|
100 |
|
101 swapWithNewTab: function (aTab) { |
|
102 if (!this.isPreloaded || this._timer) { |
|
103 return false; |
|
104 } |
|
105 |
|
106 let win = aTab.ownerDocument.defaultView; |
|
107 let tabbrowser = win.gBrowser; |
|
108 |
|
109 if (!tabbrowser) { |
|
110 return false; |
|
111 } |
|
112 |
|
113 // Swap docShells. |
|
114 tabbrowser.swapNewTabWithBrowser(aTab, this._browser); |
|
115 |
|
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)); |
|
120 |
|
121 // Remove the browser, it will be recreated by a timer. |
|
122 this._removeBrowser(); |
|
123 |
|
124 // Start a timer that will kick off preloading the next page. |
|
125 this._timer = createTimer(this, PRELOADER_INTERVAL_MS); |
|
126 |
|
127 // Signal that we swapped docShells. |
|
128 return true; |
|
129 }, |
|
130 |
|
131 observe: function () { |
|
132 this._timer = null; |
|
133 |
|
134 // Start pre-loading the customization page. |
|
135 this._createBrowser(); |
|
136 }, |
|
137 |
|
138 destroy: function () { |
|
139 this._removeBrowser(); |
|
140 this._timer = clearTimer(this._timer); |
|
141 }, |
|
142 |
|
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 }, |
|
154 |
|
155 _removeBrowser: function () { |
|
156 if (this._browser) { |
|
157 this._browser.remove(); |
|
158 this._browser = null; |
|
159 } |
|
160 } |
|
161 }; |
|
162 |
|
163 let HostFrame = { |
|
164 _frame: null, |
|
165 _deferred: null, |
|
166 |
|
167 get hiddenDOMDocument() { |
|
168 return Services.appShell.hiddenDOMWindow.document; |
|
169 }, |
|
170 |
|
171 get isReady() { |
|
172 return this.hiddenDOMDocument.readyState === "complete"; |
|
173 }, |
|
174 |
|
175 get: function () { |
|
176 if (!this._deferred) { |
|
177 this._deferred = Promise.defer(); |
|
178 this._create(); |
|
179 } |
|
180 |
|
181 return this._deferred.promise; |
|
182 }, |
|
183 |
|
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 } |
|
190 |
|
191 this._frame = null; |
|
192 this._deferred = null; |
|
193 } |
|
194 }, |
|
195 |
|
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 }, |
|
205 |
|
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 }; |