|
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 let Cu = Components.utils; |
|
8 let Ci = Components.interfaces; |
|
9 let Cc = Components.classes; |
|
10 let Cr = Components.results; |
|
11 |
|
12 /* BrowserElementParent injects script to listen for certain events in the |
|
13 * child. We then listen to messages from the child script and take |
|
14 * appropriate action here in the parent. |
|
15 */ |
|
16 |
|
17 this.EXPORTED_SYMBOLS = ["BrowserElementParentBuilder"]; |
|
18 |
|
19 Cu.import("resource://gre/modules/Services.jsm"); |
|
20 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
21 Cu.import("resource://gre/modules/BrowserElementPromptService.jsm"); |
|
22 |
|
23 XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function () { |
|
24 Cu.import("resource://gre/modules/Webapps.jsm"); |
|
25 return DOMApplicationRegistry; |
|
26 }); |
|
27 |
|
28 const TOUCH_EVENTS_ENABLED_PREF = "dom.w3c_touch_events.enabled"; |
|
29 |
|
30 function debug(msg) { |
|
31 //dump("BrowserElementParent.jsm - " + msg + "\n"); |
|
32 } |
|
33 |
|
34 function getIntPref(prefName, def) { |
|
35 try { |
|
36 return Services.prefs.getIntPref(prefName); |
|
37 } |
|
38 catch(err) { |
|
39 return def; |
|
40 } |
|
41 } |
|
42 |
|
43 function exposeAll(obj) { |
|
44 // Filter for Objects and Arrays. |
|
45 if (typeof obj !== "object" || !obj) |
|
46 return; |
|
47 |
|
48 // Recursively expose our children. |
|
49 Object.keys(obj).forEach(function(key) { |
|
50 exposeAll(obj[key]); |
|
51 }); |
|
52 |
|
53 // If we're not an Array, generate an __exposedProps__ object for ourselves. |
|
54 if (obj instanceof Array) |
|
55 return; |
|
56 var exposed = {}; |
|
57 Object.keys(obj).forEach(function(key) { |
|
58 exposed[key] = 'rw'; |
|
59 }); |
|
60 obj.__exposedProps__ = exposed; |
|
61 } |
|
62 |
|
63 function defineAndExpose(obj, name, value) { |
|
64 obj[name] = value; |
|
65 if (!('__exposedProps__' in obj)) |
|
66 obj.__exposedProps__ = {}; |
|
67 obj.__exposedProps__[name] = 'r'; |
|
68 } |
|
69 |
|
70 function visibilityChangeHandler(e) { |
|
71 // The visibilitychange event's target is the document. |
|
72 let win = e.target.defaultView; |
|
73 |
|
74 if (!win._browserElementParents) { |
|
75 return; |
|
76 } |
|
77 |
|
78 let beps = Cu.nondeterministicGetWeakMapKeys(win._browserElementParents); |
|
79 if (beps.length == 0) { |
|
80 win.removeEventListener('visibilitychange', visibilityChangeHandler); |
|
81 return; |
|
82 } |
|
83 |
|
84 for (let i = 0; i < beps.length; i++) { |
|
85 beps[i]._ownerVisibilityChange(); |
|
86 } |
|
87 } |
|
88 |
|
89 this.BrowserElementParentBuilder = { |
|
90 create: function create(frameLoader, hasRemoteFrame, isPendingFrame) { |
|
91 return new BrowserElementParent(frameLoader, hasRemoteFrame); |
|
92 } |
|
93 } |
|
94 |
|
95 |
|
96 // The active input method iframe. |
|
97 let activeInputFrame = null; |
|
98 |
|
99 function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) { |
|
100 debug("Creating new BrowserElementParent object for " + frameLoader); |
|
101 this._domRequestCounter = 0; |
|
102 this._pendingDOMRequests = {}; |
|
103 this._hasRemoteFrame = hasRemoteFrame; |
|
104 this._nextPaintListeners = []; |
|
105 |
|
106 this._frameLoader = frameLoader; |
|
107 this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement; |
|
108 let self = this; |
|
109 if (!this._frameElement) { |
|
110 debug("No frame element?"); |
|
111 return; |
|
112 } |
|
113 |
|
114 Services.obs.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true); |
|
115 Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true); |
|
116 |
|
117 let defineMethod = function(name, fn) { |
|
118 XPCNativeWrapper.unwrap(self._frameElement)[name] = function() { |
|
119 if (self._isAlive()) { |
|
120 return fn.apply(self, arguments); |
|
121 } |
|
122 }; |
|
123 } |
|
124 |
|
125 let defineNoReturnMethod = function(name, fn) { |
|
126 XPCNativeWrapper.unwrap(self._frameElement)[name] = function method() { |
|
127 if (!self._mm) { |
|
128 // Remote browser haven't been created, we just queue the API call. |
|
129 let args = Array.slice(arguments); |
|
130 args.unshift(self); |
|
131 self._pendingAPICalls.push(method.bind.apply(fn, args)); |
|
132 return; |
|
133 } |
|
134 if (self._isAlive()) { |
|
135 fn.apply(self, arguments); |
|
136 } |
|
137 }; |
|
138 }; |
|
139 |
|
140 let defineDOMRequestMethod = function(domName, msgName) { |
|
141 XPCNativeWrapper.unwrap(self._frameElement)[domName] = function() { |
|
142 if (!self._mm) { |
|
143 return self._queueDOMRequest; |
|
144 } |
|
145 if (self._isAlive()) { |
|
146 return self._sendDOMRequest(msgName); |
|
147 } |
|
148 }; |
|
149 } |
|
150 |
|
151 // Define methods on the frame element. |
|
152 defineNoReturnMethod('setVisible', this._setVisible); |
|
153 defineDOMRequestMethod('getVisible', 'get-visible'); |
|
154 defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent); |
|
155 |
|
156 // 0 = disabled, 1 = enabled, 2 - auto detect |
|
157 if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) { |
|
158 defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent); |
|
159 } |
|
160 defineNoReturnMethod('goBack', this._goBack); |
|
161 defineNoReturnMethod('goForward', this._goForward); |
|
162 defineNoReturnMethod('reload', this._reload); |
|
163 defineNoReturnMethod('stop', this._stop); |
|
164 defineDOMRequestMethod('purgeHistory', 'purge-history'); |
|
165 defineMethod('getScreenshot', this._getScreenshot); |
|
166 defineMethod('addNextPaintListener', this._addNextPaintListener); |
|
167 defineMethod('removeNextPaintListener', this._removeNextPaintListener); |
|
168 defineDOMRequestMethod('getCanGoBack', 'get-can-go-back'); |
|
169 defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward'); |
|
170 |
|
171 let principal = this._frameElement.ownerDocument.nodePrincipal; |
|
172 let perm = Services.perms |
|
173 .testExactPermissionFromPrincipal(principal, "input-manage"); |
|
174 if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) { |
|
175 defineMethod('setInputMethodActive', this._setInputMethodActive); |
|
176 } |
|
177 |
|
178 // Listen to visibilitychange on the iframe's owner window, and forward |
|
179 // changes down to the child. We want to do this while registering as few |
|
180 // visibilitychange listeners on _window as possible, because such a listener |
|
181 // may live longer than this BrowserElementParent object. |
|
182 // |
|
183 // To accomplish this, we register just one listener on the window, and have |
|
184 // it reference a WeakMap whose keys are all the BrowserElementParent objects |
|
185 // on the window. Then when the listener fires, we iterate over the |
|
186 // WeakMap's keys (which we can do, because we're chrome) to notify the |
|
187 // BrowserElementParents. |
|
188 if (!this._window._browserElementParents) { |
|
189 this._window._browserElementParents = new WeakMap(); |
|
190 this._window.addEventListener('visibilitychange', |
|
191 visibilityChangeHandler, |
|
192 /* useCapture = */ false, |
|
193 /* wantsUntrusted = */ false); |
|
194 } |
|
195 |
|
196 this._window._browserElementParents.set(this, null); |
|
197 |
|
198 // Insert ourself into the prompt service. |
|
199 BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this); |
|
200 if (!isPendingFrame) { |
|
201 this._setupMessageListener(); |
|
202 this._registerAppManifest(); |
|
203 } else { |
|
204 // if we are a pending frame, we setup message manager after |
|
205 // observing remote-browser-frame-shown |
|
206 this._pendingAPICalls = []; |
|
207 Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true); |
|
208 } |
|
209 } |
|
210 |
|
211 BrowserElementParent.prototype = { |
|
212 |
|
213 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
|
214 Ci.nsISupportsWeakReference]), |
|
215 |
|
216 _runPendingAPICall: function() { |
|
217 if (!this._pendingAPICalls) { |
|
218 return; |
|
219 } |
|
220 for (let i = 0; i < this._pendingAPICalls.length; i++) { |
|
221 try { |
|
222 this._pendingAPICalls[i](); |
|
223 } catch (e) { |
|
224 // throw the expections from pending functions. |
|
225 debug('Exception when running pending API call: ' + e); |
|
226 } |
|
227 } |
|
228 delete this._pendingAPICalls; |
|
229 }, |
|
230 |
|
231 _registerAppManifest: function() { |
|
232 // If this browser represents an app then let the Webapps module register for |
|
233 // any messages that it needs. |
|
234 let appManifestURL = |
|
235 this._frameElement.QueryInterface(Ci.nsIMozBrowserFrame).appManifestURL; |
|
236 if (appManifestURL) { |
|
237 let appId = |
|
238 DOMApplicationRegistry.getAppLocalIdByManifestURL(appManifestURL); |
|
239 if (appId != Ci.nsIScriptSecurityManager.NO_APP_ID) { |
|
240 DOMApplicationRegistry.registerBrowserElementParentForApp(this, appId); |
|
241 } |
|
242 } |
|
243 }, |
|
244 |
|
245 _setupMessageListener: function() { |
|
246 this._mm = this._frameLoader.messageManager; |
|
247 let self = this; |
|
248 |
|
249 // Messages we receive are handed to functions which take a (data) argument, |
|
250 // where |data| is the message manager's data object. |
|
251 // We use a single message and dispatch to various function based |
|
252 // on data.msg_name |
|
253 let mmCalls = { |
|
254 "hello": this._recvHello, |
|
255 "contextmenu": this._fireCtxMenuEvent, |
|
256 "locationchange": this._fireEventFromMsg, |
|
257 "loadstart": this._fireEventFromMsg, |
|
258 "loadend": this._fireEventFromMsg, |
|
259 "titlechange": this._fireEventFromMsg, |
|
260 "iconchange": this._fireEventFromMsg, |
|
261 "manifestchange": this._fireEventFromMsg, |
|
262 "metachange": this._fireEventFromMsg, |
|
263 "close": this._fireEventFromMsg, |
|
264 "resize": this._fireEventFromMsg, |
|
265 "activitydone": this._fireEventFromMsg, |
|
266 "opensearch": this._fireEventFromMsg, |
|
267 "securitychange": this._fireEventFromMsg, |
|
268 "error": this._fireEventFromMsg, |
|
269 "scroll": this._fireEventFromMsg, |
|
270 "firstpaint": this._fireEventFromMsg, |
|
271 "documentfirstpaint": this._fireEventFromMsg, |
|
272 "nextpaint": this._recvNextPaint, |
|
273 "keyevent": this._fireKeyEvent, |
|
274 "showmodalprompt": this._handleShowModalPrompt, |
|
275 "got-purge-history": this._gotDOMRequestResult, |
|
276 "got-screenshot": this._gotDOMRequestResult, |
|
277 "got-can-go-back": this._gotDOMRequestResult, |
|
278 "got-can-go-forward": this._gotDOMRequestResult, |
|
279 "fullscreen-origin-change": this._remoteFullscreenOriginChange, |
|
280 "rollback-fullscreen": this._remoteFrameFullscreenReverted, |
|
281 "exit-fullscreen": this._exitFullscreen, |
|
282 "got-visible": this._gotDOMRequestResult, |
|
283 "visibilitychange": this._childVisibilityChange, |
|
284 "got-set-input-method-active": this._gotDOMRequestResult |
|
285 }; |
|
286 |
|
287 this._mm.addMessageListener('browser-element-api:call', function(aMsg) { |
|
288 if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) { |
|
289 return mmCalls[aMsg.data.msg_name].apply(self, arguments); |
|
290 } |
|
291 }); |
|
292 }, |
|
293 |
|
294 /** |
|
295 * You shouldn't touch this._frameElement or this._window if _isAlive is |
|
296 * false. (You'll likely get an exception if you do.) |
|
297 */ |
|
298 _isAlive: function() { |
|
299 return !Cu.isDeadWrapper(this._frameElement) && |
|
300 !Cu.isDeadWrapper(this._frameElement.ownerDocument) && |
|
301 !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView); |
|
302 }, |
|
303 |
|
304 get _window() { |
|
305 return this._frameElement.ownerDocument.defaultView; |
|
306 }, |
|
307 |
|
308 get _windowUtils() { |
|
309 return this._window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
310 .getInterface(Ci.nsIDOMWindowUtils); |
|
311 }, |
|
312 |
|
313 promptAuth: function(authDetail, callback) { |
|
314 let evt; |
|
315 let self = this; |
|
316 let callbackCalled = false; |
|
317 let cancelCallback = function() { |
|
318 if (!callbackCalled) { |
|
319 callbackCalled = true; |
|
320 callback(false, null, null); |
|
321 } |
|
322 }; |
|
323 |
|
324 if (authDetail.isOnlyPassword) { |
|
325 // We don't handle password-only prompts, so just cancel it. |
|
326 cancelCallback(); |
|
327 return; |
|
328 } else { /* username and password */ |
|
329 let detail = { |
|
330 host: authDetail.host, |
|
331 realm: authDetail.realm |
|
332 }; |
|
333 |
|
334 evt = this._createEvent('usernameandpasswordrequired', detail, |
|
335 /* cancelable */ true); |
|
336 defineAndExpose(evt.detail, 'authenticate', function(username, password) { |
|
337 if (callbackCalled) |
|
338 return; |
|
339 callbackCalled = true; |
|
340 callback(true, username, password); |
|
341 }); |
|
342 } |
|
343 |
|
344 defineAndExpose(evt.detail, 'cancel', function() { |
|
345 cancelCallback(); |
|
346 }); |
|
347 |
|
348 this._frameElement.dispatchEvent(evt); |
|
349 |
|
350 if (!evt.defaultPrevented) { |
|
351 cancelCallback(); |
|
352 } |
|
353 }, |
|
354 |
|
355 _sendAsyncMsg: function(msg, data) { |
|
356 try { |
|
357 if (!data) { |
|
358 data = { }; |
|
359 } |
|
360 |
|
361 data.msg_name = msg; |
|
362 this._mm.sendAsyncMessage('browser-element-api:call', data); |
|
363 } catch (e) { |
|
364 return false; |
|
365 } |
|
366 return true; |
|
367 }, |
|
368 |
|
369 _recvHello: function() { |
|
370 debug("recvHello"); |
|
371 |
|
372 this._ready = true; |
|
373 |
|
374 // Inform our child if our owner element's document is invisible. Note |
|
375 // that we must do so here, rather than in the BrowserElementParent |
|
376 // constructor, because the BrowserElementChild may not be initialized when |
|
377 // we run our constructor. |
|
378 if (this._window.document.hidden) { |
|
379 this._ownerVisibilityChange(); |
|
380 } |
|
381 |
|
382 return { |
|
383 name: this._frameElement.getAttribute('name'), |
|
384 fullscreenAllowed: |
|
385 this._frameElement.hasAttribute('allowfullscreen') || |
|
386 this._frameElement.hasAttribute('mozallowfullscreen') |
|
387 }; |
|
388 }, |
|
389 |
|
390 _fireCtxMenuEvent: function(data) { |
|
391 let detail = data.json; |
|
392 let evtName = detail.msg_name; |
|
393 |
|
394 debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail); |
|
395 let evt = this._createEvent(evtName, detail, /* cancellable */ true); |
|
396 |
|
397 if (detail.contextmenu) { |
|
398 var self = this; |
|
399 defineAndExpose(evt.detail, 'contextMenuItemSelected', function(id) { |
|
400 self._sendAsyncMsg('fire-ctx-callback', {menuitem: id}); |
|
401 }); |
|
402 } |
|
403 |
|
404 // The embedder may have default actions on context menu events, so |
|
405 // we fire a context menu event even if the child didn't define a |
|
406 // custom context menu |
|
407 return !this._frameElement.dispatchEvent(evt); |
|
408 }, |
|
409 |
|
410 /** |
|
411 * Fire either a vanilla or a custom event, depending on the contents of |
|
412 * |data|. |
|
413 */ |
|
414 _fireEventFromMsg: function(data) { |
|
415 let detail = data.json; |
|
416 let name = detail.msg_name; |
|
417 |
|
418 // For events that send a "_payload_" property, we just want to transmit |
|
419 // this in the event. |
|
420 if ("_payload_" in detail) { |
|
421 detail = detail._payload_; |
|
422 } |
|
423 |
|
424 debug('fireEventFromMsg: ' + name + ', ' + JSON.stringify(detail)); |
|
425 let evt = this._createEvent(name, detail, |
|
426 /* cancelable = */ false); |
|
427 this._frameElement.dispatchEvent(evt); |
|
428 }, |
|
429 |
|
430 _handleShowModalPrompt: function(data) { |
|
431 // Fire a showmodalprmopt event on the iframe. When this method is called, |
|
432 // the child is spinning in a nested event loop waiting for an |
|
433 // unblock-modal-prompt message. |
|
434 // |
|
435 // If the embedder calls preventDefault() on the showmodalprompt event, |
|
436 // we'll block the child until event.detail.unblock() is called. |
|
437 // |
|
438 // Otherwise, if preventDefault() is not called, we'll send the |
|
439 // unblock-modal-prompt message to the child as soon as the event is done |
|
440 // dispatching. |
|
441 |
|
442 let detail = data.json; |
|
443 debug('handleShowPrompt ' + JSON.stringify(detail)); |
|
444 |
|
445 // Strip off the windowID property from the object we send along in the |
|
446 // event. |
|
447 let windowID = detail.windowID; |
|
448 delete detail.windowID; |
|
449 debug("Event will have detail: " + JSON.stringify(detail)); |
|
450 let evt = this._createEvent('showmodalprompt', detail, |
|
451 /* cancelable = */ true); |
|
452 |
|
453 let self = this; |
|
454 let unblockMsgSent = false; |
|
455 function sendUnblockMsg() { |
|
456 if (unblockMsgSent) { |
|
457 return; |
|
458 } |
|
459 unblockMsgSent = true; |
|
460 |
|
461 // We don't need to sanitize evt.detail.returnValue (e.g. converting the |
|
462 // return value of confirm() to a boolean); Gecko does that for us. |
|
463 |
|
464 let data = { windowID: windowID, |
|
465 returnValue: evt.detail.returnValue }; |
|
466 self._sendAsyncMsg('unblock-modal-prompt', data); |
|
467 } |
|
468 |
|
469 defineAndExpose(evt.detail, 'unblock', function() { |
|
470 sendUnblockMsg(); |
|
471 }); |
|
472 |
|
473 this._frameElement.dispatchEvent(evt); |
|
474 |
|
475 if (!evt.defaultPrevented) { |
|
476 // Unblock the inner frame immediately. Otherwise we'll unblock upon |
|
477 // evt.detail.unblock(). |
|
478 sendUnblockMsg(); |
|
479 } |
|
480 }, |
|
481 |
|
482 _createEvent: function(evtName, detail, cancelable) { |
|
483 // This will have to change if we ever want to send a CustomEvent with null |
|
484 // detail. For now, it's OK. |
|
485 if (detail !== undefined && detail !== null) { |
|
486 exposeAll(detail); |
|
487 return new this._window.CustomEvent('mozbrowser' + evtName, |
|
488 { bubbles: true, |
|
489 cancelable: cancelable, |
|
490 detail: detail }); |
|
491 } |
|
492 |
|
493 return new this._window.Event('mozbrowser' + evtName, |
|
494 { bubbles: true, |
|
495 cancelable: cancelable }); |
|
496 }, |
|
497 |
|
498 /** |
|
499 * If remote frame haven't been set up, we enqueue a function that get a |
|
500 * DOMRequest until the remote frame is ready and return another DOMRequest |
|
501 * to caller. When we get the real DOMRequest, we will help forward the |
|
502 * success/error callback to the DOMRequest that caller got. |
|
503 */ |
|
504 _queueDOMRequest: function(msgName, args) { |
|
505 if (!this._pendingAPICalls) { |
|
506 return; |
|
507 } |
|
508 |
|
509 let req = Services.DOMRequest.createRequest(this._window); |
|
510 let self = this; |
|
511 let getRealDOMRequest = function() { |
|
512 let realReq = self._sendDOMRequest(msgName, args); |
|
513 realReq.onsuccess = function(v) { |
|
514 Services.DOMRequest.fireSuccess(req, v); |
|
515 }; |
|
516 realReq.onerror = function(v) { |
|
517 Services.DOMRequest.fireError(req, v); |
|
518 }; |
|
519 }; |
|
520 this._pendingAPICalls.push(getRealDOMRequest); |
|
521 return req; |
|
522 }, |
|
523 |
|
524 /** |
|
525 * Kick off a DOMRequest in the child process. |
|
526 * |
|
527 * We'll fire an event called |msgName| on the child process, passing along |
|
528 * an object with two fields: |
|
529 * |
|
530 * - id: the ID of this request. |
|
531 * - arg: arguments to pass to the child along with this request. |
|
532 * |
|
533 * We expect the child to pass the ID back to us upon completion of the |
|
534 * request. See _gotDOMRequestResult. |
|
535 */ |
|
536 _sendDOMRequest: function(msgName, args) { |
|
537 let id = 'req_' + this._domRequestCounter++; |
|
538 let req = Services.DOMRequest.createRequest(this._window); |
|
539 if (this._sendAsyncMsg(msgName, {id: id, args: args})) { |
|
540 this._pendingDOMRequests[id] = req; |
|
541 } else { |
|
542 Services.DOMRequest.fireErrorAsync(req, "fail"); |
|
543 } |
|
544 return req; |
|
545 }, |
|
546 |
|
547 /** |
|
548 * Called when the child process finishes handling a DOMRequest. data.json |
|
549 * must have the fields [id, successRv], if the DOMRequest was successful, or |
|
550 * [id, errorMsg], if the request was not successful. |
|
551 * |
|
552 * The fields have the following meanings: |
|
553 * |
|
554 * - id: the ID of the DOM request (see _sendDOMRequest) |
|
555 * - successRv: the request's return value, if the request succeeded |
|
556 * - errorMsg: the message to pass to DOMRequest.fireError(), if the request |
|
557 * failed. |
|
558 * |
|
559 */ |
|
560 _gotDOMRequestResult: function(data) { |
|
561 let req = this._pendingDOMRequests[data.json.id]; |
|
562 delete this._pendingDOMRequests[data.json.id]; |
|
563 |
|
564 if ('successRv' in data.json) { |
|
565 debug("Successful gotDOMRequestResult."); |
|
566 Services.DOMRequest.fireSuccess(req, data.json.successRv); |
|
567 } |
|
568 else { |
|
569 debug("Got error in gotDOMRequestResult."); |
|
570 Services.DOMRequest.fireErrorAsync(req, data.json.errorMsg); |
|
571 } |
|
572 }, |
|
573 |
|
574 _setVisible: function(visible) { |
|
575 this._sendAsyncMsg('set-visible', {visible: visible}); |
|
576 this._frameLoader.visible = visible; |
|
577 }, |
|
578 |
|
579 _sendMouseEvent: function(type, x, y, button, clickCount, modifiers) { |
|
580 this._sendAsyncMsg("send-mouse-event", { |
|
581 "type": type, |
|
582 "x": x, |
|
583 "y": y, |
|
584 "button": button, |
|
585 "clickCount": clickCount, |
|
586 "modifiers": modifiers |
|
587 }); |
|
588 }, |
|
589 |
|
590 _sendTouchEvent: function(type, identifiers, touchesX, touchesY, |
|
591 radiisX, radiisY, rotationAngles, forces, |
|
592 count, modifiers) { |
|
593 |
|
594 let tabParent = this._frameLoader.tabParent; |
|
595 if (tabParent && tabParent.useAsyncPanZoom) { |
|
596 tabParent.injectTouchEvent(type, |
|
597 identifiers, |
|
598 touchesX, |
|
599 touchesY, |
|
600 radiisX, |
|
601 radiisY, |
|
602 rotationAngles, |
|
603 forces, |
|
604 count, |
|
605 modifiers); |
|
606 } else { |
|
607 this._sendAsyncMsg("send-touch-event", { |
|
608 "type": type, |
|
609 "identifiers": identifiers, |
|
610 "touchesX": touchesX, |
|
611 "touchesY": touchesY, |
|
612 "radiisX": radiisX, |
|
613 "radiisY": radiisY, |
|
614 "rotationAngles": rotationAngles, |
|
615 "forces": forces, |
|
616 "count": count, |
|
617 "modifiers": modifiers |
|
618 }); |
|
619 } |
|
620 }, |
|
621 |
|
622 _goBack: function() { |
|
623 this._sendAsyncMsg('go-back'); |
|
624 }, |
|
625 |
|
626 _goForward: function() { |
|
627 this._sendAsyncMsg('go-forward'); |
|
628 }, |
|
629 |
|
630 _reload: function(hardReload) { |
|
631 this._sendAsyncMsg('reload', {hardReload: hardReload}); |
|
632 }, |
|
633 |
|
634 _stop: function() { |
|
635 this._sendAsyncMsg('stop'); |
|
636 }, |
|
637 |
|
638 _getScreenshot: function(_width, _height, _mimeType) { |
|
639 let width = parseInt(_width); |
|
640 let height = parseInt(_height); |
|
641 let mimeType = (typeof _mimeType === 'string') ? |
|
642 _mimeType.trim().toLowerCase() : 'image/jpeg'; |
|
643 if (isNaN(width) || isNaN(height) || width < 0 || height < 0) { |
|
644 throw Components.Exception("Invalid argument", |
|
645 Cr.NS_ERROR_INVALID_ARG); |
|
646 } |
|
647 |
|
648 if (!this._mm) { |
|
649 // Child haven't been loaded. |
|
650 return this._queueDOMRequest('get-screenshot', |
|
651 {width: width, height: height, |
|
652 mimeType: mimeType}); |
|
653 } |
|
654 |
|
655 return this._sendDOMRequest('get-screenshot', |
|
656 {width: width, height: height, |
|
657 mimeType: mimeType}); |
|
658 }, |
|
659 |
|
660 _recvNextPaint: function(data) { |
|
661 let listeners = this._nextPaintListeners; |
|
662 this._nextPaintListeners = []; |
|
663 for (let listener of listeners) { |
|
664 try { |
|
665 listener(); |
|
666 } catch (e) { |
|
667 // If a listener throws we'll continue. |
|
668 } |
|
669 } |
|
670 }, |
|
671 |
|
672 _addNextPaintListener: function(listener) { |
|
673 if (typeof listener != 'function') |
|
674 throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); |
|
675 |
|
676 let self = this; |
|
677 let run = function() { |
|
678 if (self._nextPaintListeners.push(listener) == 1) |
|
679 self._sendAsyncMsg('activate-next-paint-listener'); |
|
680 }; |
|
681 if (!this._mm) { |
|
682 this._pendingAPICalls.push(run); |
|
683 } else { |
|
684 run(); |
|
685 } |
|
686 }, |
|
687 |
|
688 _removeNextPaintListener: function(listener) { |
|
689 if (typeof listener != 'function') |
|
690 throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); |
|
691 |
|
692 let self = this; |
|
693 let run = function() { |
|
694 for (let i = self._nextPaintListeners.length - 1; i >= 0; i--) { |
|
695 if (self._nextPaintListeners[i] == listener) { |
|
696 self._nextPaintListeners.splice(i, 1); |
|
697 break; |
|
698 } |
|
699 } |
|
700 |
|
701 if (self._nextPaintListeners.length == 0) |
|
702 self._sendAsyncMsg('deactivate-next-paint-listener'); |
|
703 }; |
|
704 if (!this._mm) { |
|
705 this._pendingAPICalls.push(run); |
|
706 } else { |
|
707 run(); |
|
708 } |
|
709 }, |
|
710 |
|
711 _setInputMethodActive: function(isActive) { |
|
712 if (typeof isActive !== 'boolean') { |
|
713 throw Components.Exception("Invalid argument", |
|
714 Cr.NS_ERROR_INVALID_ARG); |
|
715 } |
|
716 |
|
717 let req = Services.DOMRequest.createRequest(this._window); |
|
718 |
|
719 // Deactivate the old input method if needed. |
|
720 if (activeInputFrame && isActive) { |
|
721 if (Cu.isDeadWrapper(activeInputFrame)) { |
|
722 // If the activeInputFrame is already a dead object, |
|
723 // we should simply set it to null directly. |
|
724 activeInputFrame = null; |
|
725 this._sendSetInputMethodActiveDOMRequest(req, isActive); |
|
726 } else { |
|
727 let reqOld = XPCNativeWrapper.unwrap(activeInputFrame) |
|
728 .setInputMethodActive(false); |
|
729 |
|
730 // We wan't to continue regardless whether this req succeeded |
|
731 reqOld.onsuccess = reqOld.onerror = function() { |
|
732 let setActive = function() { |
|
733 activeInputFrame = null; |
|
734 this._sendSetInputMethodActiveDOMRequest(req, isActive); |
|
735 }.bind(this); |
|
736 |
|
737 if (this._ready) { |
|
738 setActive(); |
|
739 return; |
|
740 } |
|
741 |
|
742 // Wait for the hello event from BrowserElementChild |
|
743 let onReady = function(aMsg) { |
|
744 if (this._isAlive() && (aMsg.data.msg_name === 'hello')) { |
|
745 setActive(); |
|
746 |
|
747 this._mm.removeMessageListener('browser-element-api:call', |
|
748 onReady); |
|
749 } |
|
750 }.bind(this); |
|
751 |
|
752 this._mm.addMessageListener('browser-element-api:call', onReady); |
|
753 }.bind(this); |
|
754 } |
|
755 } else { |
|
756 this._sendSetInputMethodActiveDOMRequest(req, isActive); |
|
757 } |
|
758 return req; |
|
759 }, |
|
760 |
|
761 _sendSetInputMethodActiveDOMRequest: function(req, isActive) { |
|
762 let id = 'req_' + this._domRequestCounter++; |
|
763 let data = { |
|
764 id : id, |
|
765 args: { isActive: isActive } |
|
766 }; |
|
767 if (this._sendAsyncMsg('set-input-method-active', data)) { |
|
768 activeInputFrame = this._frameElement; |
|
769 this._pendingDOMRequests[id] = req; |
|
770 } else { |
|
771 Services.DOMRequest.fireErrorAsync(req, 'fail'); |
|
772 } |
|
773 }, |
|
774 |
|
775 _fireKeyEvent: function(data) { |
|
776 let evt = this._window.document.createEvent("KeyboardEvent"); |
|
777 evt.initKeyEvent(data.json.type, true, true, this._window, |
|
778 false, false, false, false, // modifiers |
|
779 data.json.keyCode, |
|
780 data.json.charCode); |
|
781 |
|
782 this._frameElement.dispatchEvent(evt); |
|
783 }, |
|
784 |
|
785 /** |
|
786 * Called when the visibility of the window which owns this iframe changes. |
|
787 */ |
|
788 _ownerVisibilityChange: function() { |
|
789 this._sendAsyncMsg('owner-visibility-change', |
|
790 {visible: !this._window.document.hidden}); |
|
791 }, |
|
792 |
|
793 /* |
|
794 * Called when the child notices that its visibility has changed. |
|
795 * |
|
796 * This is sometimes redundant; for example, the child's visibility may |
|
797 * change in response to a setVisible request that we made here! But it's |
|
798 * not always redundant; for example, the child's visibility may change in |
|
799 * response to its parent docshell being hidden. |
|
800 */ |
|
801 _childVisibilityChange: function(data) { |
|
802 debug("_childVisibilityChange(" + data.json.visible + ")"); |
|
803 this._frameLoader.visible = data.json.visible; |
|
804 |
|
805 this._fireEventFromMsg(data); |
|
806 }, |
|
807 |
|
808 _exitFullscreen: function() { |
|
809 this._windowUtils.exitFullscreen(); |
|
810 }, |
|
811 |
|
812 _remoteFullscreenOriginChange: function(data) { |
|
813 let origin = data.json._payload_; |
|
814 this._windowUtils.remoteFrameFullscreenChanged(this._frameElement, origin); |
|
815 }, |
|
816 |
|
817 _remoteFrameFullscreenReverted: function(data) { |
|
818 this._windowUtils.remoteFrameFullscreenReverted(); |
|
819 }, |
|
820 |
|
821 _fireFatalError: function() { |
|
822 let evt = this._createEvent('error', {type: 'fatal'}, |
|
823 /* cancelable = */ false); |
|
824 this._frameElement.dispatchEvent(evt); |
|
825 }, |
|
826 |
|
827 observe: function(subject, topic, data) { |
|
828 switch(topic) { |
|
829 case 'oop-frameloader-crashed': |
|
830 if (this._isAlive() && subject == this._frameLoader) { |
|
831 this._fireFatalError(); |
|
832 } |
|
833 break; |
|
834 case 'ask-children-to-exit-fullscreen': |
|
835 if (this._isAlive() && |
|
836 this._frameElement.ownerDocument == subject && |
|
837 this._hasRemoteFrame) { |
|
838 this._sendAsyncMsg('exit-fullscreen'); |
|
839 } |
|
840 break; |
|
841 case 'remote-browser-frame-shown': |
|
842 if (this._frameLoader == subject) { |
|
843 if (!this._mm) { |
|
844 this._setupMessageListener(); |
|
845 this._registerAppManifest(); |
|
846 this._runPendingAPICall(); |
|
847 } |
|
848 Services.obs.removeObserver(this, 'remote-browser-frame-shown'); |
|
849 } |
|
850 default: |
|
851 debug('Unknown topic: ' + topic); |
|
852 break; |
|
853 }; |
|
854 }, |
|
855 }; |