michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: let Cu = Components.utils; michael@0: let Ci = Components.interfaces; michael@0: let Cc = Components.classes; michael@0: let Cr = Components.results; michael@0: michael@0: /* BrowserElementParent injects script to listen for certain events in the michael@0: * child. We then listen to messages from the child script and take michael@0: * appropriate action here in the parent. michael@0: */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ["BrowserElementParentBuilder"]; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/BrowserElementPromptService.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function () { michael@0: Cu.import("resource://gre/modules/Webapps.jsm"); michael@0: return DOMApplicationRegistry; michael@0: }); michael@0: michael@0: const TOUCH_EVENTS_ENABLED_PREF = "dom.w3c_touch_events.enabled"; michael@0: michael@0: function debug(msg) { michael@0: //dump("BrowserElementParent.jsm - " + msg + "\n"); michael@0: } michael@0: michael@0: function getIntPref(prefName, def) { michael@0: try { michael@0: return Services.prefs.getIntPref(prefName); michael@0: } michael@0: catch(err) { michael@0: return def; michael@0: } michael@0: } michael@0: michael@0: function exposeAll(obj) { michael@0: // Filter for Objects and Arrays. michael@0: if (typeof obj !== "object" || !obj) michael@0: return; michael@0: michael@0: // Recursively expose our children. michael@0: Object.keys(obj).forEach(function(key) { michael@0: exposeAll(obj[key]); michael@0: }); michael@0: michael@0: // If we're not an Array, generate an __exposedProps__ object for ourselves. michael@0: if (obj instanceof Array) michael@0: return; michael@0: var exposed = {}; michael@0: Object.keys(obj).forEach(function(key) { michael@0: exposed[key] = 'rw'; michael@0: }); michael@0: obj.__exposedProps__ = exposed; michael@0: } michael@0: michael@0: function defineAndExpose(obj, name, value) { michael@0: obj[name] = value; michael@0: if (!('__exposedProps__' in obj)) michael@0: obj.__exposedProps__ = {}; michael@0: obj.__exposedProps__[name] = 'r'; michael@0: } michael@0: michael@0: function visibilityChangeHandler(e) { michael@0: // The visibilitychange event's target is the document. michael@0: let win = e.target.defaultView; michael@0: michael@0: if (!win._browserElementParents) { michael@0: return; michael@0: } michael@0: michael@0: let beps = Cu.nondeterministicGetWeakMapKeys(win._browserElementParents); michael@0: if (beps.length == 0) { michael@0: win.removeEventListener('visibilitychange', visibilityChangeHandler); michael@0: return; michael@0: } michael@0: michael@0: for (let i = 0; i < beps.length; i++) { michael@0: beps[i]._ownerVisibilityChange(); michael@0: } michael@0: } michael@0: michael@0: this.BrowserElementParentBuilder = { michael@0: create: function create(frameLoader, hasRemoteFrame, isPendingFrame) { michael@0: return new BrowserElementParent(frameLoader, hasRemoteFrame); michael@0: } michael@0: } michael@0: michael@0: michael@0: // The active input method iframe. michael@0: let activeInputFrame = null; michael@0: michael@0: function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) { michael@0: debug("Creating new BrowserElementParent object for " + frameLoader); michael@0: this._domRequestCounter = 0; michael@0: this._pendingDOMRequests = {}; michael@0: this._hasRemoteFrame = hasRemoteFrame; michael@0: this._nextPaintListeners = []; michael@0: michael@0: this._frameLoader = frameLoader; michael@0: this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement; michael@0: let self = this; michael@0: if (!this._frameElement) { michael@0: debug("No frame element?"); michael@0: return; michael@0: } michael@0: michael@0: Services.obs.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true); michael@0: Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true); michael@0: michael@0: let defineMethod = function(name, fn) { michael@0: XPCNativeWrapper.unwrap(self._frameElement)[name] = function() { michael@0: if (self._isAlive()) { michael@0: return fn.apply(self, arguments); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: let defineNoReturnMethod = function(name, fn) { michael@0: XPCNativeWrapper.unwrap(self._frameElement)[name] = function method() { michael@0: if (!self._mm) { michael@0: // Remote browser haven't been created, we just queue the API call. michael@0: let args = Array.slice(arguments); michael@0: args.unshift(self); michael@0: self._pendingAPICalls.push(method.bind.apply(fn, args)); michael@0: return; michael@0: } michael@0: if (self._isAlive()) { michael@0: fn.apply(self, arguments); michael@0: } michael@0: }; michael@0: }; michael@0: michael@0: let defineDOMRequestMethod = function(domName, msgName) { michael@0: XPCNativeWrapper.unwrap(self._frameElement)[domName] = function() { michael@0: if (!self._mm) { michael@0: return self._queueDOMRequest; michael@0: } michael@0: if (self._isAlive()) { michael@0: return self._sendDOMRequest(msgName); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: // Define methods on the frame element. michael@0: defineNoReturnMethod('setVisible', this._setVisible); michael@0: defineDOMRequestMethod('getVisible', 'get-visible'); michael@0: defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent); michael@0: michael@0: // 0 = disabled, 1 = enabled, 2 - auto detect michael@0: if (getIntPref(TOUCH_EVENTS_ENABLED_PREF, 0) != 0) { michael@0: defineNoReturnMethod('sendTouchEvent', this._sendTouchEvent); michael@0: } michael@0: defineNoReturnMethod('goBack', this._goBack); michael@0: defineNoReturnMethod('goForward', this._goForward); michael@0: defineNoReturnMethod('reload', this._reload); michael@0: defineNoReturnMethod('stop', this._stop); michael@0: defineDOMRequestMethod('purgeHistory', 'purge-history'); michael@0: defineMethod('getScreenshot', this._getScreenshot); michael@0: defineMethod('addNextPaintListener', this._addNextPaintListener); michael@0: defineMethod('removeNextPaintListener', this._removeNextPaintListener); michael@0: defineDOMRequestMethod('getCanGoBack', 'get-can-go-back'); michael@0: defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward'); michael@0: michael@0: let principal = this._frameElement.ownerDocument.nodePrincipal; michael@0: let perm = Services.perms michael@0: .testExactPermissionFromPrincipal(principal, "input-manage"); michael@0: if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: defineMethod('setInputMethodActive', this._setInputMethodActive); michael@0: } michael@0: michael@0: // Listen to visibilitychange on the iframe's owner window, and forward michael@0: // changes down to the child. We want to do this while registering as few michael@0: // visibilitychange listeners on _window as possible, because such a listener michael@0: // may live longer than this BrowserElementParent object. michael@0: // michael@0: // To accomplish this, we register just one listener on the window, and have michael@0: // it reference a WeakMap whose keys are all the BrowserElementParent objects michael@0: // on the window. Then when the listener fires, we iterate over the michael@0: // WeakMap's keys (which we can do, because we're chrome) to notify the michael@0: // BrowserElementParents. michael@0: if (!this._window._browserElementParents) { michael@0: this._window._browserElementParents = new WeakMap(); michael@0: this._window.addEventListener('visibilitychange', michael@0: visibilityChangeHandler, michael@0: /* useCapture = */ false, michael@0: /* wantsUntrusted = */ false); michael@0: } michael@0: michael@0: this._window._browserElementParents.set(this, null); michael@0: michael@0: // Insert ourself into the prompt service. michael@0: BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this); michael@0: if (!isPendingFrame) { michael@0: this._setupMessageListener(); michael@0: this._registerAppManifest(); michael@0: } else { michael@0: // if we are a pending frame, we setup message manager after michael@0: // observing remote-browser-frame-shown michael@0: this._pendingAPICalls = []; michael@0: Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true); michael@0: } michael@0: } michael@0: michael@0: BrowserElementParent.prototype = { michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: _runPendingAPICall: function() { michael@0: if (!this._pendingAPICalls) { michael@0: return; michael@0: } michael@0: for (let i = 0; i < this._pendingAPICalls.length; i++) { michael@0: try { michael@0: this._pendingAPICalls[i](); michael@0: } catch (e) { michael@0: // throw the expections from pending functions. michael@0: debug('Exception when running pending API call: ' + e); michael@0: } michael@0: } michael@0: delete this._pendingAPICalls; michael@0: }, michael@0: michael@0: _registerAppManifest: function() { michael@0: // If this browser represents an app then let the Webapps module register for michael@0: // any messages that it needs. michael@0: let appManifestURL = michael@0: this._frameElement.QueryInterface(Ci.nsIMozBrowserFrame).appManifestURL; michael@0: if (appManifestURL) { michael@0: let appId = michael@0: DOMApplicationRegistry.getAppLocalIdByManifestURL(appManifestURL); michael@0: if (appId != Ci.nsIScriptSecurityManager.NO_APP_ID) { michael@0: DOMApplicationRegistry.registerBrowserElementParentForApp(this, appId); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _setupMessageListener: function() { michael@0: this._mm = this._frameLoader.messageManager; michael@0: let self = this; michael@0: michael@0: // Messages we receive are handed to functions which take a (data) argument, michael@0: // where |data| is the message manager's data object. michael@0: // We use a single message and dispatch to various function based michael@0: // on data.msg_name michael@0: let mmCalls = { michael@0: "hello": this._recvHello, michael@0: "contextmenu": this._fireCtxMenuEvent, michael@0: "locationchange": this._fireEventFromMsg, michael@0: "loadstart": this._fireEventFromMsg, michael@0: "loadend": this._fireEventFromMsg, michael@0: "titlechange": this._fireEventFromMsg, michael@0: "iconchange": this._fireEventFromMsg, michael@0: "manifestchange": this._fireEventFromMsg, michael@0: "metachange": this._fireEventFromMsg, michael@0: "close": this._fireEventFromMsg, michael@0: "resize": this._fireEventFromMsg, michael@0: "activitydone": this._fireEventFromMsg, michael@0: "opensearch": this._fireEventFromMsg, michael@0: "securitychange": this._fireEventFromMsg, michael@0: "error": this._fireEventFromMsg, michael@0: "scroll": this._fireEventFromMsg, michael@0: "firstpaint": this._fireEventFromMsg, michael@0: "documentfirstpaint": this._fireEventFromMsg, michael@0: "nextpaint": this._recvNextPaint, michael@0: "keyevent": this._fireKeyEvent, michael@0: "showmodalprompt": this._handleShowModalPrompt, michael@0: "got-purge-history": this._gotDOMRequestResult, michael@0: "got-screenshot": this._gotDOMRequestResult, michael@0: "got-can-go-back": this._gotDOMRequestResult, michael@0: "got-can-go-forward": this._gotDOMRequestResult, michael@0: "fullscreen-origin-change": this._remoteFullscreenOriginChange, michael@0: "rollback-fullscreen": this._remoteFrameFullscreenReverted, michael@0: "exit-fullscreen": this._exitFullscreen, michael@0: "got-visible": this._gotDOMRequestResult, michael@0: "visibilitychange": this._childVisibilityChange, michael@0: "got-set-input-method-active": this._gotDOMRequestResult michael@0: }; michael@0: michael@0: this._mm.addMessageListener('browser-element-api:call', function(aMsg) { michael@0: if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) { michael@0: return mmCalls[aMsg.data.msg_name].apply(self, arguments); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * You shouldn't touch this._frameElement or this._window if _isAlive is michael@0: * false. (You'll likely get an exception if you do.) michael@0: */ michael@0: _isAlive: function() { michael@0: return !Cu.isDeadWrapper(this._frameElement) && michael@0: !Cu.isDeadWrapper(this._frameElement.ownerDocument) && michael@0: !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView); michael@0: }, michael@0: michael@0: get _window() { michael@0: return this._frameElement.ownerDocument.defaultView; michael@0: }, michael@0: michael@0: get _windowUtils() { michael@0: return this._window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: }, michael@0: michael@0: promptAuth: function(authDetail, callback) { michael@0: let evt; michael@0: let self = this; michael@0: let callbackCalled = false; michael@0: let cancelCallback = function() { michael@0: if (!callbackCalled) { michael@0: callbackCalled = true; michael@0: callback(false, null, null); michael@0: } michael@0: }; michael@0: michael@0: if (authDetail.isOnlyPassword) { michael@0: // We don't handle password-only prompts, so just cancel it. michael@0: cancelCallback(); michael@0: return; michael@0: } else { /* username and password */ michael@0: let detail = { michael@0: host: authDetail.host, michael@0: realm: authDetail.realm michael@0: }; michael@0: michael@0: evt = this._createEvent('usernameandpasswordrequired', detail, michael@0: /* cancelable */ true); michael@0: defineAndExpose(evt.detail, 'authenticate', function(username, password) { michael@0: if (callbackCalled) michael@0: return; michael@0: callbackCalled = true; michael@0: callback(true, username, password); michael@0: }); michael@0: } michael@0: michael@0: defineAndExpose(evt.detail, 'cancel', function() { michael@0: cancelCallback(); michael@0: }); michael@0: michael@0: this._frameElement.dispatchEvent(evt); michael@0: michael@0: if (!evt.defaultPrevented) { michael@0: cancelCallback(); michael@0: } michael@0: }, michael@0: michael@0: _sendAsyncMsg: function(msg, data) { michael@0: try { michael@0: if (!data) { michael@0: data = { }; michael@0: } michael@0: michael@0: data.msg_name = msg; michael@0: this._mm.sendAsyncMessage('browser-element-api:call', data); michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: return true; michael@0: }, michael@0: michael@0: _recvHello: function() { michael@0: debug("recvHello"); michael@0: michael@0: this._ready = true; michael@0: michael@0: // Inform our child if our owner element's document is invisible. Note michael@0: // that we must do so here, rather than in the BrowserElementParent michael@0: // constructor, because the BrowserElementChild may not be initialized when michael@0: // we run our constructor. michael@0: if (this._window.document.hidden) { michael@0: this._ownerVisibilityChange(); michael@0: } michael@0: michael@0: return { michael@0: name: this._frameElement.getAttribute('name'), michael@0: fullscreenAllowed: michael@0: this._frameElement.hasAttribute('allowfullscreen') || michael@0: this._frameElement.hasAttribute('mozallowfullscreen') michael@0: }; michael@0: }, michael@0: michael@0: _fireCtxMenuEvent: function(data) { michael@0: let detail = data.json; michael@0: let evtName = detail.msg_name; michael@0: michael@0: debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail); michael@0: let evt = this._createEvent(evtName, detail, /* cancellable */ true); michael@0: michael@0: if (detail.contextmenu) { michael@0: var self = this; michael@0: defineAndExpose(evt.detail, 'contextMenuItemSelected', function(id) { michael@0: self._sendAsyncMsg('fire-ctx-callback', {menuitem: id}); michael@0: }); michael@0: } michael@0: michael@0: // The embedder may have default actions on context menu events, so michael@0: // we fire a context menu event even if the child didn't define a michael@0: // custom context menu michael@0: return !this._frameElement.dispatchEvent(evt); michael@0: }, michael@0: michael@0: /** michael@0: * Fire either a vanilla or a custom event, depending on the contents of michael@0: * |data|. michael@0: */ michael@0: _fireEventFromMsg: function(data) { michael@0: let detail = data.json; michael@0: let name = detail.msg_name; michael@0: michael@0: // For events that send a "_payload_" property, we just want to transmit michael@0: // this in the event. michael@0: if ("_payload_" in detail) { michael@0: detail = detail._payload_; michael@0: } michael@0: michael@0: debug('fireEventFromMsg: ' + name + ', ' + JSON.stringify(detail)); michael@0: let evt = this._createEvent(name, detail, michael@0: /* cancelable = */ false); michael@0: this._frameElement.dispatchEvent(evt); michael@0: }, michael@0: michael@0: _handleShowModalPrompt: function(data) { michael@0: // Fire a showmodalprmopt event on the iframe. When this method is called, michael@0: // the child is spinning in a nested event loop waiting for an michael@0: // unblock-modal-prompt message. michael@0: // michael@0: // If the embedder calls preventDefault() on the showmodalprompt event, michael@0: // we'll block the child until event.detail.unblock() is called. michael@0: // michael@0: // Otherwise, if preventDefault() is not called, we'll send the michael@0: // unblock-modal-prompt message to the child as soon as the event is done michael@0: // dispatching. michael@0: michael@0: let detail = data.json; michael@0: debug('handleShowPrompt ' + JSON.stringify(detail)); michael@0: michael@0: // Strip off the windowID property from the object we send along in the michael@0: // event. michael@0: let windowID = detail.windowID; michael@0: delete detail.windowID; michael@0: debug("Event will have detail: " + JSON.stringify(detail)); michael@0: let evt = this._createEvent('showmodalprompt', detail, michael@0: /* cancelable = */ true); michael@0: michael@0: let self = this; michael@0: let unblockMsgSent = false; michael@0: function sendUnblockMsg() { michael@0: if (unblockMsgSent) { michael@0: return; michael@0: } michael@0: unblockMsgSent = true; michael@0: michael@0: // We don't need to sanitize evt.detail.returnValue (e.g. converting the michael@0: // return value of confirm() to a boolean); Gecko does that for us. michael@0: michael@0: let data = { windowID: windowID, michael@0: returnValue: evt.detail.returnValue }; michael@0: self._sendAsyncMsg('unblock-modal-prompt', data); michael@0: } michael@0: michael@0: defineAndExpose(evt.detail, 'unblock', function() { michael@0: sendUnblockMsg(); michael@0: }); michael@0: michael@0: this._frameElement.dispatchEvent(evt); michael@0: michael@0: if (!evt.defaultPrevented) { michael@0: // Unblock the inner frame immediately. Otherwise we'll unblock upon michael@0: // evt.detail.unblock(). michael@0: sendUnblockMsg(); michael@0: } michael@0: }, michael@0: michael@0: _createEvent: function(evtName, detail, cancelable) { michael@0: // This will have to change if we ever want to send a CustomEvent with null michael@0: // detail. For now, it's OK. michael@0: if (detail !== undefined && detail !== null) { michael@0: exposeAll(detail); michael@0: return new this._window.CustomEvent('mozbrowser' + evtName, michael@0: { bubbles: true, michael@0: cancelable: cancelable, michael@0: detail: detail }); michael@0: } michael@0: michael@0: return new this._window.Event('mozbrowser' + evtName, michael@0: { bubbles: true, michael@0: cancelable: cancelable }); michael@0: }, michael@0: michael@0: /** michael@0: * If remote frame haven't been set up, we enqueue a function that get a michael@0: * DOMRequest until the remote frame is ready and return another DOMRequest michael@0: * to caller. When we get the real DOMRequest, we will help forward the michael@0: * success/error callback to the DOMRequest that caller got. michael@0: */ michael@0: _queueDOMRequest: function(msgName, args) { michael@0: if (!this._pendingAPICalls) { michael@0: return; michael@0: } michael@0: michael@0: let req = Services.DOMRequest.createRequest(this._window); michael@0: let self = this; michael@0: let getRealDOMRequest = function() { michael@0: let realReq = self._sendDOMRequest(msgName, args); michael@0: realReq.onsuccess = function(v) { michael@0: Services.DOMRequest.fireSuccess(req, v); michael@0: }; michael@0: realReq.onerror = function(v) { michael@0: Services.DOMRequest.fireError(req, v); michael@0: }; michael@0: }; michael@0: this._pendingAPICalls.push(getRealDOMRequest); michael@0: return req; michael@0: }, michael@0: michael@0: /** michael@0: * Kick off a DOMRequest in the child process. michael@0: * michael@0: * We'll fire an event called |msgName| on the child process, passing along michael@0: * an object with two fields: michael@0: * michael@0: * - id: the ID of this request. michael@0: * - arg: arguments to pass to the child along with this request. michael@0: * michael@0: * We expect the child to pass the ID back to us upon completion of the michael@0: * request. See _gotDOMRequestResult. michael@0: */ michael@0: _sendDOMRequest: function(msgName, args) { michael@0: let id = 'req_' + this._domRequestCounter++; michael@0: let req = Services.DOMRequest.createRequest(this._window); michael@0: if (this._sendAsyncMsg(msgName, {id: id, args: args})) { michael@0: this._pendingDOMRequests[id] = req; michael@0: } else { michael@0: Services.DOMRequest.fireErrorAsync(req, "fail"); michael@0: } michael@0: return req; michael@0: }, michael@0: michael@0: /** michael@0: * Called when the child process finishes handling a DOMRequest. data.json michael@0: * must have the fields [id, successRv], if the DOMRequest was successful, or michael@0: * [id, errorMsg], if the request was not successful. michael@0: * michael@0: * The fields have the following meanings: michael@0: * michael@0: * - id: the ID of the DOM request (see _sendDOMRequest) michael@0: * - successRv: the request's return value, if the request succeeded michael@0: * - errorMsg: the message to pass to DOMRequest.fireError(), if the request michael@0: * failed. michael@0: * michael@0: */ michael@0: _gotDOMRequestResult: function(data) { michael@0: let req = this._pendingDOMRequests[data.json.id]; michael@0: delete this._pendingDOMRequests[data.json.id]; michael@0: michael@0: if ('successRv' in data.json) { michael@0: debug("Successful gotDOMRequestResult."); michael@0: Services.DOMRequest.fireSuccess(req, data.json.successRv); michael@0: } michael@0: else { michael@0: debug("Got error in gotDOMRequestResult."); michael@0: Services.DOMRequest.fireErrorAsync(req, data.json.errorMsg); michael@0: } michael@0: }, michael@0: michael@0: _setVisible: function(visible) { michael@0: this._sendAsyncMsg('set-visible', {visible: visible}); michael@0: this._frameLoader.visible = visible; michael@0: }, michael@0: michael@0: _sendMouseEvent: function(type, x, y, button, clickCount, modifiers) { michael@0: this._sendAsyncMsg("send-mouse-event", { michael@0: "type": type, michael@0: "x": x, michael@0: "y": y, michael@0: "button": button, michael@0: "clickCount": clickCount, michael@0: "modifiers": modifiers michael@0: }); michael@0: }, michael@0: michael@0: _sendTouchEvent: function(type, identifiers, touchesX, touchesY, michael@0: radiisX, radiisY, rotationAngles, forces, michael@0: count, modifiers) { michael@0: michael@0: let tabParent = this._frameLoader.tabParent; michael@0: if (tabParent && tabParent.useAsyncPanZoom) { michael@0: tabParent.injectTouchEvent(type, michael@0: identifiers, michael@0: touchesX, michael@0: touchesY, michael@0: radiisX, michael@0: radiisY, michael@0: rotationAngles, michael@0: forces, michael@0: count, michael@0: modifiers); michael@0: } else { michael@0: this._sendAsyncMsg("send-touch-event", { michael@0: "type": type, michael@0: "identifiers": identifiers, michael@0: "touchesX": touchesX, michael@0: "touchesY": touchesY, michael@0: "radiisX": radiisX, michael@0: "radiisY": radiisY, michael@0: "rotationAngles": rotationAngles, michael@0: "forces": forces, michael@0: "count": count, michael@0: "modifiers": modifiers michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: _goBack: function() { michael@0: this._sendAsyncMsg('go-back'); michael@0: }, michael@0: michael@0: _goForward: function() { michael@0: this._sendAsyncMsg('go-forward'); michael@0: }, michael@0: michael@0: _reload: function(hardReload) { michael@0: this._sendAsyncMsg('reload', {hardReload: hardReload}); michael@0: }, michael@0: michael@0: _stop: function() { michael@0: this._sendAsyncMsg('stop'); michael@0: }, michael@0: michael@0: _getScreenshot: function(_width, _height, _mimeType) { michael@0: let width = parseInt(_width); michael@0: let height = parseInt(_height); michael@0: let mimeType = (typeof _mimeType === 'string') ? michael@0: _mimeType.trim().toLowerCase() : 'image/jpeg'; michael@0: if (isNaN(width) || isNaN(height) || width < 0 || height < 0) { michael@0: throw Components.Exception("Invalid argument", michael@0: Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: if (!this._mm) { michael@0: // Child haven't been loaded. michael@0: return this._queueDOMRequest('get-screenshot', michael@0: {width: width, height: height, michael@0: mimeType: mimeType}); michael@0: } michael@0: michael@0: return this._sendDOMRequest('get-screenshot', michael@0: {width: width, height: height, michael@0: mimeType: mimeType}); michael@0: }, michael@0: michael@0: _recvNextPaint: function(data) { michael@0: let listeners = this._nextPaintListeners; michael@0: this._nextPaintListeners = []; michael@0: for (let listener of listeners) { michael@0: try { michael@0: listener(); michael@0: } catch (e) { michael@0: // If a listener throws we'll continue. michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _addNextPaintListener: function(listener) { michael@0: if (typeof listener != 'function') michael@0: throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); michael@0: michael@0: let self = this; michael@0: let run = function() { michael@0: if (self._nextPaintListeners.push(listener) == 1) michael@0: self._sendAsyncMsg('activate-next-paint-listener'); michael@0: }; michael@0: if (!this._mm) { michael@0: this._pendingAPICalls.push(run); michael@0: } else { michael@0: run(); michael@0: } michael@0: }, michael@0: michael@0: _removeNextPaintListener: function(listener) { michael@0: if (typeof listener != 'function') michael@0: throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); michael@0: michael@0: let self = this; michael@0: let run = function() { michael@0: for (let i = self._nextPaintListeners.length - 1; i >= 0; i--) { michael@0: if (self._nextPaintListeners[i] == listener) { michael@0: self._nextPaintListeners.splice(i, 1); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (self._nextPaintListeners.length == 0) michael@0: self._sendAsyncMsg('deactivate-next-paint-listener'); michael@0: }; michael@0: if (!this._mm) { michael@0: this._pendingAPICalls.push(run); michael@0: } else { michael@0: run(); michael@0: } michael@0: }, michael@0: michael@0: _setInputMethodActive: function(isActive) { michael@0: if (typeof isActive !== 'boolean') { michael@0: throw Components.Exception("Invalid argument", michael@0: Cr.NS_ERROR_INVALID_ARG); michael@0: } michael@0: michael@0: let req = Services.DOMRequest.createRequest(this._window); michael@0: michael@0: // Deactivate the old input method if needed. michael@0: if (activeInputFrame && isActive) { michael@0: if (Cu.isDeadWrapper(activeInputFrame)) { michael@0: // If the activeInputFrame is already a dead object, michael@0: // we should simply set it to null directly. michael@0: activeInputFrame = null; michael@0: this._sendSetInputMethodActiveDOMRequest(req, isActive); michael@0: } else { michael@0: let reqOld = XPCNativeWrapper.unwrap(activeInputFrame) michael@0: .setInputMethodActive(false); michael@0: michael@0: // We wan't to continue regardless whether this req succeeded michael@0: reqOld.onsuccess = reqOld.onerror = function() { michael@0: let setActive = function() { michael@0: activeInputFrame = null; michael@0: this._sendSetInputMethodActiveDOMRequest(req, isActive); michael@0: }.bind(this); michael@0: michael@0: if (this._ready) { michael@0: setActive(); michael@0: return; michael@0: } michael@0: michael@0: // Wait for the hello event from BrowserElementChild michael@0: let onReady = function(aMsg) { michael@0: if (this._isAlive() && (aMsg.data.msg_name === 'hello')) { michael@0: setActive(); michael@0: michael@0: this._mm.removeMessageListener('browser-element-api:call', michael@0: onReady); michael@0: } michael@0: }.bind(this); michael@0: michael@0: this._mm.addMessageListener('browser-element-api:call', onReady); michael@0: }.bind(this); michael@0: } michael@0: } else { michael@0: this._sendSetInputMethodActiveDOMRequest(req, isActive); michael@0: } michael@0: return req; michael@0: }, michael@0: michael@0: _sendSetInputMethodActiveDOMRequest: function(req, isActive) { michael@0: let id = 'req_' + this._domRequestCounter++; michael@0: let data = { michael@0: id : id, michael@0: args: { isActive: isActive } michael@0: }; michael@0: if (this._sendAsyncMsg('set-input-method-active', data)) { michael@0: activeInputFrame = this._frameElement; michael@0: this._pendingDOMRequests[id] = req; michael@0: } else { michael@0: Services.DOMRequest.fireErrorAsync(req, 'fail'); michael@0: } michael@0: }, michael@0: michael@0: _fireKeyEvent: function(data) { michael@0: let evt = this._window.document.createEvent("KeyboardEvent"); michael@0: evt.initKeyEvent(data.json.type, true, true, this._window, michael@0: false, false, false, false, // modifiers michael@0: data.json.keyCode, michael@0: data.json.charCode); michael@0: michael@0: this._frameElement.dispatchEvent(evt); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the visibility of the window which owns this iframe changes. michael@0: */ michael@0: _ownerVisibilityChange: function() { michael@0: this._sendAsyncMsg('owner-visibility-change', michael@0: {visible: !this._window.document.hidden}); michael@0: }, michael@0: michael@0: /* michael@0: * Called when the child notices that its visibility has changed. michael@0: * michael@0: * This is sometimes redundant; for example, the child's visibility may michael@0: * change in response to a setVisible request that we made here! But it's michael@0: * not always redundant; for example, the child's visibility may change in michael@0: * response to its parent docshell being hidden. michael@0: */ michael@0: _childVisibilityChange: function(data) { michael@0: debug("_childVisibilityChange(" + data.json.visible + ")"); michael@0: this._frameLoader.visible = data.json.visible; michael@0: michael@0: this._fireEventFromMsg(data); michael@0: }, michael@0: michael@0: _exitFullscreen: function() { michael@0: this._windowUtils.exitFullscreen(); michael@0: }, michael@0: michael@0: _remoteFullscreenOriginChange: function(data) { michael@0: let origin = data.json._payload_; michael@0: this._windowUtils.remoteFrameFullscreenChanged(this._frameElement, origin); michael@0: }, michael@0: michael@0: _remoteFrameFullscreenReverted: function(data) { michael@0: this._windowUtils.remoteFrameFullscreenReverted(); michael@0: }, michael@0: michael@0: _fireFatalError: function() { michael@0: let evt = this._createEvent('error', {type: 'fatal'}, michael@0: /* cancelable = */ false); michael@0: this._frameElement.dispatchEvent(evt); michael@0: }, michael@0: michael@0: observe: function(subject, topic, data) { michael@0: switch(topic) { michael@0: case 'oop-frameloader-crashed': michael@0: if (this._isAlive() && subject == this._frameLoader) { michael@0: this._fireFatalError(); michael@0: } michael@0: break; michael@0: case 'ask-children-to-exit-fullscreen': michael@0: if (this._isAlive() && michael@0: this._frameElement.ownerDocument == subject && michael@0: this._hasRemoteFrame) { michael@0: this._sendAsyncMsg('exit-fullscreen'); michael@0: } michael@0: break; michael@0: case 'remote-browser-frame-shown': michael@0: if (this._frameLoader == subject) { michael@0: if (!this._mm) { michael@0: this._setupMessageListener(); michael@0: this._registerAppManifest(); michael@0: this._runPendingAPICall(); michael@0: } michael@0: Services.obs.removeObserver(this, 'remote-browser-frame-shown'); michael@0: } michael@0: default: michael@0: debug('Unknown topic: ' + topic); michael@0: break; michael@0: }; michael@0: }, michael@0: };