Bug 1155493 - Part 2: Event hook for mozbrowser element. r=kanru
authorMorris Tseng <mtseng@mozilla.com>
Thu, 28 May 2015 01:57:00 -0400
changeset 246314 970957a8ad2a3e2e1a6b32a86999e0f38ead860f
parent 246313 c816a43492118415d01f7dc44148c720eb7dbb21
child 246315 6e532292984fdcd367a10920071b0cb477fb3a8c
push id28826
push userryanvm@gmail.com
push dateFri, 29 May 2015 20:58:36 +0000
treeherdermozilla-central@45a4d6336c73 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskanru
bugs1155493
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1155493 - Part 2: Event hook for mozbrowser element. r=kanru
b2g/chrome/content/shell.js
dom/browser-element/BrowserElementChild.js
dom/browser-element/BrowserElementCopyPaste.js
dom/browser-element/BrowserElementParent.js
dom/ipc/jar.mn
dom/ipc/preload.js
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -345,16 +345,17 @@ var shell = {
 
     window.addEventListener('MozApplicationManifest', this);
     window.addEventListener('MozAfterPaint', this);
     window.addEventListener('sizemodechange', this);
     window.addEventListener('unload', this);
     this.contentBrowser.addEventListener('mozbrowserloadstart', this, true);
     this.contentBrowser.addEventListener('mozbrowserselectionstatechanged', this, true);
     this.contentBrowser.addEventListener('mozbrowserscrollviewchange', this, true);
+    this.contentBrowser.addEventListener('mozbrowsercaretstatechanged', this);
 
     CustomEventManager.init();
     WebappsHelper.init();
     UserAgentOverrides.init();
     CaptivePortalLoginHelper.init();
 
     this.contentBrowser.src = homeURL;
     this.isHomeLoaded = false;
@@ -375,16 +376,17 @@ var shell = {
     window.removeEventListener('unload', this);
     window.removeEventListener('keydown', this, true);
     window.removeEventListener('keyup', this, true);
     window.removeEventListener('MozApplicationManifest', this);
     window.removeEventListener('sizemodechange', this);
     this.contentBrowser.removeEventListener('mozbrowserloadstart', this, true);
     this.contentBrowser.removeEventListener('mozbrowserselectionstatechanged', this, true);
     this.contentBrowser.removeEventListener('mozbrowserscrollviewchange', this, true);
+    this.contentBrowser.removeEventListener('mozbrowsercaretstatechanged', this);
     ppmm.removeMessageListener("content-handler", this);
 
     UserAgentOverrides.uninit();
   },
 
   // If this key event represents a hardware button which needs to be send as
   // a message, broadcasts it with the message set to 'xxx-button-press' or
   // 'xxx-button-release'.
@@ -485,16 +487,38 @@ var shell = {
         data.offsetY = offsetY;
 
         DoCommandHelper.setEvent(evt);
         shell.sendChromeEvent({
           type: 'selectionstatechanged',
           detail: data,
         });
         break;
+      case 'mozbrowsercaretstatechanged':
+        {
+          let elt = evt.target;
+          let win = elt.ownerDocument.defaultView;
+          let offsetX = win.mozInnerScreenX - window.mozInnerScreenX;
+          let offsetY = win.mozInnerScreenY - window.mozInnerScreenY;
+
+          let rect = elt.getBoundingClientRect();
+          offsetX += rect.left;
+          offsetY += rect.top;
+
+          let data = evt.detail;
+          data.offsetX = offsetX;
+          data.offsetY = offsetY;
+          data.sendDoCommandMsg = null;
+
+          shell.sendChromeEvent({
+            type: 'caretstatechanged',
+            detail: data,
+          });
+        }
+        break;
 
       case 'MozApplicationManifest':
         try {
           if (!Services.prefs.getBoolPref('browser.cache.offline.enable'))
             return;
 
           let contentWindow = evt.originalTarget.defaultView;
           let documentElement = contentWindow.document.documentElement;
@@ -713,16 +737,20 @@ var CustomEventManager = {
       case 'inputmethod-update-layouts':
       case 'inputregistry-add':
       case 'inputregistry-remove':
         KeyboardHelper.handleEvent(detail);
         break;
       case 'do-command':
         DoCommandHelper.handleEvent(detail.cmd);
         break;
+      case 'copypaste-do-command':
+        Services.obs.notifyObservers({ wrappedJSObject: shell.contentBrowser },
+                                     'ask-children-to-execute-copypaste-command', detail.cmd);
+        break;
     }
   }
 }
 
 let DoCommandHelper = {
   _event: null,
   setEvent: function docommand_setEvent(evt) {
     this._event = evt;
--- a/dom/browser-element/BrowserElementChild.js
+++ b/dom/browser-element/BrowserElementChild.js
@@ -29,22 +29,25 @@ function isTopBrowserElement(docShell) {
     if (docShell && docShell.isBrowserOrApp) {
       return false;
     }
   }
   return true;
 }
 
 if (!('BrowserElementIsPreloaded' in this)) {
-  if (isTopBrowserElement(docShell) &&
-      Services.prefs.getBoolPref("dom.mozInputMethod.enabled")) {
-    try {
-      Services.scriptloader.loadSubScript("chrome://global/content/forms.js");
-    } catch (e) {
+  if (isTopBrowserElement(docShell)) {
+    if (Services.prefs.getBoolPref("dom.mozInputMethod.enabled")) {
+      try {
+        Services.scriptloader.loadSubScript("chrome://global/content/forms.js");
+      } catch (e) {
+      }
     }
+
+    Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementCopyPaste.js");
   }
 
   if (Services.prefs.getIntPref("dom.w3c_touch_events.enabled") == 1) {
     if (docShell.asyncPanZoomEnabled === false) {
       Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementPanningAPZDisabled.js");
       ContentPanningAPZDisabled.init();
     }
 
new file mode 100644
--- /dev/null
+++ b/dom/browser-element/BrowserElementCopyPaste.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+dump("###################################### BrowserElementCopyPaste.js loaded\n");
+
+let CopyPasteAssistent = {
+  COMMAND_MAP: {
+    'cut': 'cmd_cut',
+    'copy': 'cmd_copyAndCollapseToEnd',
+    'paste': 'cmd_paste',
+    'selectall': 'cmd_selectAll'
+  },
+
+  init: function() {
+    addEventListener('mozcaretstatechanged',
+                     this._caretStateChangedHandler.bind(this),
+                     /* useCapture = */ true,
+                     /* wantsUntrusted = */ false);
+    addMessageListener('browser-element-api:call', this._browserAPIHandler.bind(this));
+  },
+
+  _browserAPIHandler: function(e) {
+    switch (e.data.msg_name) {
+      case 'copypaste-do-command':
+        if (this._isCommandEnabled(e.data.command)) {
+          docShell.doCommand(COMMAND_MAP[e.data.command]);
+        }
+        break;
+    }
+  },
+
+  _isCommandEnabled: function(cmd) {
+    let command = this.COMMAND_MAP[cmd];
+    if (!command) {
+      return false;
+    }
+
+    return docShell.isCommandEnabled(command);
+  },
+
+  _caretStateChangedHandler: function(e) {
+    e.stopPropagation();
+
+    let boundingClientRect = e.boundingClientRect;
+    let canPaste = this._isCommandEnabled("paste");
+    let zoomFactor = content.innerWidth == 0 ? 1 : content.screen.width / content.innerWidth;
+
+    let detail = {
+      rect: {
+        width: boundingClientRect ? boundingClientRect.width : 0,
+        height: boundingClientRect ? boundingClientRect.height : 0,
+        top: boundingClientRect ? boundingClientRect.top : 0,
+        bottom: boundingClientRect ? boundingClientRect.bottom : 0,
+        left: boundingClientRect ? boundingClientRect.left : 0,
+        right: boundingClientRect ? boundingClientRect.right : 0,
+      },
+      commands: {
+        canSelectAll: this._isCommandEnabled("selectall"),
+        canCut: this._isCommandEnabled("cut"),
+        canCopy: this._isCommandEnabled("copy"),
+        canPaste: this._isCommandEnabled("paste"),
+      },
+      zoomFactor: zoomFactor,
+      reason: e.reason,
+      collapsed: e.collapsed,
+      caretVisible: e.caretVisible,
+      selectionVisible: e.selectionVisible
+    };
+
+    // Get correct geometry information if we have nested iframe.
+    let currentWindow = e.target.defaultView;
+    while (currentWindow.realFrameElement) {
+      let currentRect = currentWindow.realFrameElement.getBoundingClientRect();
+      detail.rect.top += currentRect.top;
+      detail.rect.bottom += currentRect.top;
+      detail.rect.left += currentRect.left;
+      detail.rect.right += currentRect.left;
+      currentWindow = currentWindow.realFrameElement.ownerDocument.defaultView;
+    }
+
+    sendAsyncMsg('caretstatechanged', detail);
+  },
+};
+
+CopyPasteAssistent.init();
--- a/dom/browser-element/BrowserElementParent.js
+++ b/dom/browser-element/BrowserElementParent.js
@@ -82,16 +82,17 @@ function BrowserElementParent() {
   this._domRequestReady = false;
   this._pendingAPICalls = [];
   this._pendingDOMRequests = {};
   this._pendingSetInputMethodActive = [];
   this._nextPaintListeners = [];
 
   Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true);
   Services.obs.addObserver(this, 'copypaste-docommand', /* ownsWeak = */ true);
+  Services.obs.addObserver(this, 'ask-children-to-execute-copypaste-command', /* ownsWeak = */ true);
 }
 
 BrowserElementParent.prototype = {
 
   classDescription: "BrowserElementAPI implementation",
   classID: Components.ID("{9f171ac4-0939-4ef8-b360-3408aedc3060}"),
   contractID: "@mozilla.org/dom/browser-element-api;1",
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserElementAPI,
@@ -198,16 +199,17 @@ BrowserElementParent.prototype = {
       "entered-dom-fullscreen": this._enteredDomFullscreen,
       "fullscreen-origin-change": this._fullscreenOriginChange,
       "exited-dom-fullscreen": this._exitedDomFullscreen,
       "got-visible": this._gotDOMRequestResult,
       "visibilitychange": this._childVisibilityChange,
       "got-set-input-method-active": this._gotDOMRequestResult,
       "selectionstatechanged": this._handleSelectionStateChanged,
       "scrollviewchange": this._handleScrollViewChange,
+      "caretstatechanged": this._handleCaretStateChanged,
     };
 
     let mmSecuritySensitiveCalls = {
       "showmodalprompt": this._handleShowModalPrompt,
       "contextmenu": this._fireCtxMenuEvent,
       "securitychange": this._fireEventFromMsg,
       "locationchange": this._fireEventFromMsg,
       "iconchange": this._fireEventFromMsg,
@@ -433,16 +435,44 @@ BrowserElementParent.prototype = {
   },
 
   _handleSelectionStateChanged: function(data) {
     let evt = this._createEvent('selectionstatechanged', data.json,
                                 /* cancelable = */ false);
     this._frameElement.dispatchEvent(evt);
   },
 
+  // Called when state of accessible caret in child has changed.
+  // The fields of data is as following:
+  //  - rect: Contains bounding rectangle of selection, Include width, height,
+  //          top, bottom, left and right.
+  //  - commands: Describe what commands can be executed in child. Include canSelectAll,
+  //              canCut, canCopy and canPaste. For example: if we want to check if cut
+  //              command is available, using following code, if (data.commands.canCut) {}.
+  //  - zoomFactor: Current zoom factor in child frame.
+  //  - reason: The reason causes the state changed. Include "visibilitychange",
+  //            "updateposition", "longpressonemptycontent", "taponcaret", "presscaret",
+  //            "releasecaret".
+  //  - collapsed: Indicate current selection is collapsed or not.
+  //  - caretVisible: Indicate the caret visiibility.
+  //  - selectionVisible: Indicate current selection is visible or not.
+  _handleCaretStateChanged: function(data) {
+    let evt = this._createEvent('caretstatechanged', data.json,
+                                /* cancelable = */ false);
+
+    let self = this;
+    function sendDoCommandMsg(cmd) {
+      let data = { command: cmd };
+      self._sendAsyncMsg('copypaste-do-command', data);
+    }
+    Cu.exportFunction(sendDoCommandMsg, evt.detail, { defineAs: 'sendDoCommandMsg' });
+
+    this._frameElement.dispatchEvent(evt);
+  },
+
   _handleScrollViewChange: function(data) {
     let evt = this._createEvent("scrollviewchange", data.json,
                                 /* cancelable = */ false);
     this._frameElement.dispatchEvent(evt);
   },
 
   _createEvent: function(evtName, detail, cancelable) {
     // This will have to change if we ever want to send a CustomEvent with null
@@ -974,16 +1004,21 @@ BrowserElementParent.prototype = {
         this._fireFatalError();
       }
       break;
     case 'copypaste-docommand':
       if (this._isAlive() && this._frameElement.isEqualNode(subject.wrappedJSObject)) {
         this._sendAsyncMsg('do-command', { command: data });
       }
       break;
+    case 'ask-children-to-execute-copypaste-command':
+      if (this._isAlive() && this._frameElement == subject.wrappedJSObject) {
+        this._sendAsyncMsg('copypaste-do-command', { command: data });
+      }
+      break;
     default:
       debug('Unknown topic: ' + topic);
       break;
     };
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BrowserElementParent]);
--- a/dom/ipc/jar.mn
+++ b/dom/ipc/jar.mn
@@ -2,13 +2,14 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 toolkit.jar:
         content/global/test-ipc.xul (test.xul)
         content/global/remote-test-ipc.js (remote-test.js)
         content/global/BrowserElementChild.js (../browser-element/BrowserElementChild.js)
         content/global/BrowserElementChildPreload.js (../browser-element/BrowserElementChildPreload.js)
+        content/global/BrowserElementCopyPaste.js (../browser-element/BrowserElementCopyPaste.js)
         content/global/BrowserElementPanning.js (../browser-element/BrowserElementPanning.js)
 *       content/global/BrowserElementPanningAPZDisabled.js (../browser-element/BrowserElementPanningAPZDisabled.js)
         content/global/manifestMessages.js (manifestMessages.js)
         content/global/PushServiceChildPreload.js (../push/PushServiceChildPreload.js)
         content/global/preload.js (preload.js)
--- a/dom/ipc/preload.js
+++ b/dom/ipc/preload.js
@@ -98,16 +98,17 @@ const BrowserElementIsPreloaded = true;
         Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementPanningAPZDisabled.js", global);
       }
     } catch (e) {
     }
 
     Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementPanning.js", global);
   }
 
+  Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementCopyPaste.js", global);
   Services.scriptloader.loadSubScript("chrome://global/content/BrowserElementChildPreload.js", global);
 
   Services.io.getProtocolHandler("app");
   Services.io.getProtocolHandler("default");
 
   docShell.isActive = false;
   docShell.createAboutBlankContentViewer(null);