dom/browser-element/BrowserElementParent.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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 let Cu = Components.utils;
     8 let Ci = Components.interfaces;
     9 let Cc = Components.classes;
    10 let Cr = Components.results;
    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  */
    17 this.EXPORTED_SYMBOLS = ["BrowserElementParentBuilder"];
    19 Cu.import("resource://gre/modules/Services.jsm");
    20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    21 Cu.import("resource://gre/modules/BrowserElementPromptService.jsm");
    23 XPCOMUtils.defineLazyGetter(this, "DOMApplicationRegistry", function () {
    24   Cu.import("resource://gre/modules/Webapps.jsm");
    25   return DOMApplicationRegistry;
    26 });
    28 const TOUCH_EVENTS_ENABLED_PREF = "dom.w3c_touch_events.enabled";
    30 function debug(msg) {
    31   //dump("BrowserElementParent.jsm - " + msg + "\n");
    32 }
    34 function getIntPref(prefName, def) {
    35   try {
    36     return Services.prefs.getIntPref(prefName);
    37   }
    38   catch(err) {
    39     return def;
    40   }
    41 }
    43 function exposeAll(obj) {
    44   // Filter for Objects and Arrays.
    45   if (typeof obj !== "object" || !obj)
    46     return;
    48   // Recursively expose our children.
    49   Object.keys(obj).forEach(function(key) {
    50     exposeAll(obj[key]);
    51   });
    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 }
    63 function defineAndExpose(obj, name, value) {
    64   obj[name] = value;
    65   if (!('__exposedProps__' in obj))
    66     obj.__exposedProps__ = {};
    67   obj.__exposedProps__[name] = 'r';
    68 }
    70 function visibilityChangeHandler(e) {
    71   // The visibilitychange event's target is the document.
    72   let win = e.target.defaultView;
    74   if (!win._browserElementParents) {
    75     return;
    76   }
    78   let beps = Cu.nondeterministicGetWeakMapKeys(win._browserElementParents);
    79   if (beps.length == 0) {
    80     win.removeEventListener('visibilitychange', visibilityChangeHandler);
    81     return;
    82   }
    84   for (let i = 0; i < beps.length; i++) {
    85     beps[i]._ownerVisibilityChange();
    86   }
    87 }
    89 this.BrowserElementParentBuilder = {
    90   create: function create(frameLoader, hasRemoteFrame, isPendingFrame) {
    91     return new BrowserElementParent(frameLoader, hasRemoteFrame);
    92   }
    93 }
    96 // The active input method iframe.
    97 let activeInputFrame = null;
    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 = [];
   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   }
   114   Services.obs.addObserver(this, 'ask-children-to-exit-fullscreen', /* ownsWeak = */ true);
   115   Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true);
   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   }
   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   };
   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   }
   151   // Define methods on the frame element.
   152   defineNoReturnMethod('setVisible', this._setVisible);
   153   defineDOMRequestMethod('getVisible', 'get-visible');
   154   defineNoReturnMethod('sendMouseEvent', this._sendMouseEvent);
   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');
   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   }
   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   }
   196   this._window._browserElementParents.set(this, null);
   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 }
   211 BrowserElementParent.prototype = {
   213   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
   214                                          Ci.nsISupportsWeakReference]),
   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   },
   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   },
   245   _setupMessageListener: function() {
   246     this._mm = this._frameLoader.messageManager;
   247     let self = this;
   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     };
   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   },
   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   },
   304   get _window() {
   305     return this._frameElement.ownerDocument.defaultView;
   306   },
   308   get _windowUtils() {
   309     return this._window.QueryInterface(Ci.nsIInterfaceRequestor)
   310                        .getInterface(Ci.nsIDOMWindowUtils);
   311   },
   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     };
   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       };
   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     }
   344     defineAndExpose(evt.detail, 'cancel', function() {
   345       cancelCallback();
   346     });
   348     this._frameElement.dispatchEvent(evt);
   350     if (!evt.defaultPrevented) {
   351       cancelCallback();
   352     }
   353   },
   355   _sendAsyncMsg: function(msg, data) {
   356     try {
   357       if (!data) {
   358         data = { };
   359       }
   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   },
   369   _recvHello: function() {
   370     debug("recvHello");
   372     this._ready = true;
   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     }
   382     return {
   383       name: this._frameElement.getAttribute('name'),
   384       fullscreenAllowed:
   385         this._frameElement.hasAttribute('allowfullscreen') ||
   386         this._frameElement.hasAttribute('mozallowfullscreen')
   387     };
   388   },
   390   _fireCtxMenuEvent: function(data) {
   391     let detail = data.json;
   392     let evtName = detail.msg_name;
   394     debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail);
   395     let evt = this._createEvent(evtName, detail, /* cancellable */ true);
   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     }
   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   },
   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;
   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     }
   424     debug('fireEventFromMsg: ' + name + ', ' + JSON.stringify(detail));
   425     let evt = this._createEvent(name, detail,
   426                                 /* cancelable = */ false);
   427     this._frameElement.dispatchEvent(evt);
   428   },
   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.
   442     let detail = data.json;
   443     debug('handleShowPrompt ' + JSON.stringify(detail));
   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);
   453     let self = this;
   454     let unblockMsgSent = false;
   455     function sendUnblockMsg() {
   456       if (unblockMsgSent) {
   457         return;
   458       }
   459       unblockMsgSent = true;
   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.
   464       let data = { windowID: windowID,
   465                    returnValue: evt.detail.returnValue };
   466       self._sendAsyncMsg('unblock-modal-prompt', data);
   467     }
   469     defineAndExpose(evt.detail, 'unblock', function() {
   470       sendUnblockMsg();
   471     });
   473     this._frameElement.dispatchEvent(evt);
   475     if (!evt.defaultPrevented) {
   476       // Unblock the inner frame immediately.  Otherwise we'll unblock upon
   477       // evt.detail.unblock().
   478       sendUnblockMsg();
   479     }
   480   },
   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     }
   493     return new this._window.Event('mozbrowser' + evtName,
   494                                   { bubbles: true,
   495                                     cancelable: cancelable });
   496   },
   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     }
   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   },
   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   },
   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];
   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   },
   574   _setVisible: function(visible) {
   575     this._sendAsyncMsg('set-visible', {visible: visible});
   576     this._frameLoader.visible = visible;
   577   },
   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   },
   590   _sendTouchEvent: function(type, identifiers, touchesX, touchesY,
   591                             radiisX, radiisY, rotationAngles, forces,
   592                             count, modifiers) {
   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   },
   622   _goBack: function() {
   623     this._sendAsyncMsg('go-back');
   624   },
   626   _goForward: function() {
   627     this._sendAsyncMsg('go-forward');
   628   },
   630   _reload: function(hardReload) {
   631     this._sendAsyncMsg('reload', {hardReload: hardReload});
   632   },
   634   _stop: function() {
   635     this._sendAsyncMsg('stop');
   636   },
   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     }
   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     }
   655     return this._sendDOMRequest('get-screenshot',
   656                                 {width: width, height: height,
   657                                  mimeType: mimeType});
   658   },
   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   },
   672   _addNextPaintListener: function(listener) {
   673     if (typeof listener != 'function')
   674       throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
   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   },
   688   _removeNextPaintListener: function(listener) {
   689     if (typeof listener != 'function')
   690       throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG);
   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       }
   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   },
   711   _setInputMethodActive: function(isActive) {
   712     if (typeof isActive !== 'boolean') {
   713       throw Components.Exception("Invalid argument",
   714                                  Cr.NS_ERROR_INVALID_ARG);
   715     }
   717     let req = Services.DOMRequest.createRequest(this._window);
   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);
   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);
   737           if (this._ready) {
   738             setActive();
   739             return;
   740           }
   742           // Wait for the hello event from BrowserElementChild
   743           let onReady = function(aMsg) {
   744             if (this._isAlive() && (aMsg.data.msg_name === 'hello')) {
   745               setActive();
   747               this._mm.removeMessageListener('browser-element-api:call',
   748                 onReady);
   749             }
   750           }.bind(this);
   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   },
   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   },
   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);
   782     this._frameElement.dispatchEvent(evt);
   783   },
   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   },
   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;
   805     this._fireEventFromMsg(data);
   806   },
   808   _exitFullscreen: function() {
   809     this._windowUtils.exitFullscreen();
   810   },
   812   _remoteFullscreenOriginChange: function(data) {
   813     let origin = data.json._payload_;
   814     this._windowUtils.remoteFrameFullscreenChanged(this._frameElement, origin);
   815   },
   817   _remoteFrameFullscreenReverted: function(data) {
   818     this._windowUtils.remoteFrameFullscreenReverted();
   819   },
   821   _fireFatalError: function() {
   822     let evt = this._createEvent('error', {type: 'fatal'},
   823                                 /* cancelable = */ false);
   824     this._frameElement.dispatchEvent(evt);
   825   },
   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 };

mercurial