Bug 1441279 - 2. Add selection action JS modules; r?esawin
Add JS modules for listening to accessible caret events, and relaying
those events to Java.
MozReview-Commit-ID: JPLTMzK7Nzn
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/geckoview/GeckoViewSelectionActionContent.js
@@ -0,0 +1,249 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+
+ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "dump", () =>
+ ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
+ {}).AndroidLog.d.bind(null, "ViewSelectionActionContent"));
+
+function debug(aMsg) {
+ // dump(aMsg);
+}
+
+// Dispatches GeckoView:ShowSelectionAction and GeckoView:HideSelectionAction to
+// the GeckoSession on accessible caret changes.
+class GeckoViewSelectionActionContent extends GeckoViewContentModule {
+ constructor(aModuleName, aMessageManager) {
+ super(aModuleName, aMessageManager);
+
+ this._seqNo = 0;
+ this._isActive = false;
+ this._previousMessage = {};
+
+ this._actions = [{
+ id: "org.mozilla.geckoview.CUT",
+ predicate: e => !e.collapsed && e.selectionEditable,
+ perform: _ => this._domWindowUtils.sendContentCommandEvent("cut"),
+ }, {
+ id: "org.mozilla.geckoview.COPY",
+ predicate: e => !e.collapsed,
+ perform: _ => this._domWindowUtils.sendContentCommandEvent("copy"),
+ }, {
+ id: "org.mozilla.geckoview.PASTE",
+ predicate: e => e.selectionEditable &&
+ Services.clipboard.hasDataMatchingFlavors(
+ ["text/unicode"], 1, Ci.nsIClipboard.kGlobalClipboard),
+ perform: _ => this._domWindowUtils.sendContentCommandEvent("paste"),
+ }, {
+ id: "org.mozilla.geckoview.DELETE",
+ predicate: e => !e.collapsed && e.selectionEditable,
+ perform: _ => this._domWindowUtils.sendContentCommandEvent("delete"),
+ }, {
+ id: "org.mozilla.geckoview.COLLAPSE_TO_START",
+ predicate: e => !e.collapsed && e.selectionEditable,
+ perform: e => this._getSelection(e).collapseToStart(),
+ }, {
+ id: "org.mozilla.geckoview.COLLAPSE_TO_END",
+ predicate: e => !e.collapsed && e.selectionEditable,
+ perform: e => this._getSelection(e).collapseToEnd(),
+ }, {
+ id: "org.mozilla.geckoview.UNSELECT",
+ predicate: e => !e.collapsed && !e.selectionEditable,
+ perform: e => this._getSelection(e).removeAllRanges(),
+ }, {
+ id: "org.mozilla.geckoview.SELECT_ALL",
+ predicate: e => e.reason !== "longpressonemptycontent",
+ perform: e => this._getSelectionController(e).selectAll(),
+ }];
+ }
+
+ get _domWindowUtils() {
+ return content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ }
+
+ _getSelectionController(aEvent) {
+ if (aEvent.selectionEditable) {
+ const focus = aEvent.target.activeElement;
+ if (focus instanceof Ci.nsIDOMNSEditableElement && focus.editor) {
+ return focus.editor.selectionController;
+ }
+ }
+
+ return aEvent.target.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+ }
+
+ _getSelection(aEvent) {
+ return this._getSelectionController(aEvent)
+ .getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
+ }
+
+ _getFrameOffset(aEvent) {
+ // Get correct offset in case of nested iframe.
+ const offset = {
+ left: 0,
+ top: 0,
+ };
+
+ let currentWindow = aEvent.target.defaultView;
+ while (currentWindow.realFrameElement) {
+ const currentRect = currentWindow.realFrameElement.getBoundingClientRect();
+ currentWindow = currentWindow.realFrameElement.ownerGlobal;
+
+ offset.left += currentRect.left;
+ offset.top += currentRect.top;
+
+ let targetDocShell = currentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation);
+ if (targetDocShell.isMozBrowser) {
+ break;
+ }
+ }
+
+ return offset;
+ }
+
+ register() {
+ debug("register");
+ addEventListener("mozcaretstatechanged", this);
+ }
+
+ unregister() {
+ debug("unregister");
+ removeEventListener("mozcaretstatechanged", this);
+ }
+
+ /**
+ * Receive and act on AccessibleCarets caret state-change
+ * (mozcaretstatechanged) events.
+ */
+ handleEvent(aEvent) {
+ let reason = aEvent.reason;
+
+ if (this._isActive && !aEvent.caretVisible) {
+ // For mozcaretstatechanged, "visibilitychange" means the caret is hidden.
+ reason = "visibilitychange";
+ } else if (this._isActive &&
+ !aEvent.collapsed &&
+ !aEvent.selectionVisible) {
+ reason = "invisibleselection";
+ } else if (aEvent.selectionEditable &&
+ aEvent.collapsed &&
+ reason !== "longpressonemptycontent" &&
+ reason !== "taponcaret") {
+ // Don't show selection actions when merely focusing on an editor or
+ // repositioning the cursor. Wait until long press or the caret is tapped
+ // in order to match Android behavior.
+ reason = "visibilitychange";
+ }
+
+ debug("handleEvent " + reason + " " + aEvent);
+
+ if (["longpressonemptycontent",
+ "releasecaret",
+ "taponcaret",
+ "updateposition"].includes(reason)) {
+
+ const actions = this._actions.filter(
+ action => action.predicate.call(this, aEvent));
+
+ const offset = this._getFrameOffset(aEvent);
+
+ const msg = {
+ type: "GeckoView:ShowSelectionAction",
+ seqNo: this._seqNo,
+ collapsed: aEvent.collapsed,
+ editable: aEvent.selectionEditable,
+ selection: aEvent.selectedTextContent,
+ clientRect: !aEvent.boundingClientRect ? null : {
+ left: aEvent.boundingClientRect.left + offset.left,
+ top: aEvent.boundingClientRect.top + offset.top,
+ right: aEvent.boundingClientRect.right + offset.left,
+ bottom: aEvent.boundingClientRect.bottom + offset.top,
+ },
+ actions: actions.map(action => action.id),
+ };
+
+ try {
+ if (msg.clientRect) {
+ msg.clientRect.bottom += parseFloat(Services.prefs.getCharPref(
+ "layout.accessiblecaret.height", "0"));
+ }
+ } catch (e) {
+ }
+
+ if (this._isActive && JSON.stringify(msg) === this._previousMessage) {
+ // Don't call again if we're already active and things haven't changed.
+ return;
+ }
+
+ msg.seqNo = ++this._seqNo;
+ this._isActive = true;
+ this._previousMessage = JSON.stringify(msg);
+
+ debug("onShowSelectionAction " + JSON.stringify(msg));
+
+ // This event goes to GeckoViewSelectionAction.jsm, where the data is
+ // further transformed and then sent to GeckoSession.
+ this.eventDispatcher.sendRequest(msg, {
+ onSuccess: response => {
+ if (response.seqNo !== this._seqNo) {
+ // Stale action.
+ return;
+ }
+ let action = actions.find(action => action.id === response.id);
+ if (action) {
+ action.perform.call(this, aEvent, response);
+ } else {
+ dump("Invalid action " + response.id);
+ }
+ },
+ onError: _ => {
+ // Do nothing; we can get here if the delegate was just unregistered.
+ },
+ });
+
+ } else if (["invisibleselection",
+ "presscaret",
+ "scroll",
+ "visibilitychange"].includes(reason)) {
+
+ if (!this._isActive) {
+ return;
+ }
+
+ this._isActive = false;
+
+ // Mark previous actions as stale. Don't do this for "invisibleselection"
+ // or "scroll" because previous actions should still be valid even after
+ // these events occur.
+ if (reason !== "invisibleselection" && reason !== "scroll") {
+ this._seqNo++;
+ }
+
+ this.eventDispatcher.sendRequest({
+ type: "GeckoView:HideSelectionAction",
+ reason: reason,
+ });
+
+ } else {
+ dump("Unknown reason: " + reason);
+ }
+ }
+}
+
+var selectionActionListener =
+ new GeckoViewSelectionActionContent("GeckoViewSelectionAction", this);
--- a/mobile/android/chrome/geckoview/geckoview.js
+++ b/mobile/android/chrome/geckoview/geckoview.js
@@ -79,13 +79,15 @@ function startup() {
ModuleManager.add("resource://gre/modules/GeckoViewScroll.jsm",
"GeckoViewScroll");
ModuleManager.add("resource://gre/modules/GeckoViewTab.jsm",
"GeckoViewTab");
ModuleManager.add("resource://gre/modules/GeckoViewRemoteDebugger.jsm",
"GeckoViewRemoteDebugger");
ModuleManager.add("resource://gre/modules/GeckoViewTrackingProtection.jsm",
"GeckoViewTrackingProtection");
+ ModuleManager.add("resource://gre/modules/GeckoViewSelectionAction.jsm",
+ "GeckoViewSelectionAction");
// Move focus to the content window at the end of startup,
// so things like text selection can work properly.
browser.focus();
}
--- a/mobile/android/chrome/geckoview/jar.mn
+++ b/mobile/android/chrome/geckoview/jar.mn
@@ -7,8 +7,9 @@ geckoview.jar:
content/ErrorPageEventHandler.js
content/geckoview.xul
content/geckoview.js
content/GeckoViewContent.js
content/GeckoViewContentSettings.js
content/GeckoViewNavigationContent.js
content/GeckoViewScrollContent.js
+ content/GeckoViewSelectionActionContent.js
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewSelectionAction.jsm
@@ -0,0 +1,38 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["GeckoViewSelectionAction"];
+
+ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "dump", () =>
+ ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
+ {}).AndroidLog.d.bind(null, "ViewSelectionAction"));
+
+function debug(aMsg) {
+ // dump(aMsg);
+}
+
+// Handles inter-op between accessible carets and GeckoSession.
+class GeckoViewSelectionAction extends GeckoViewModule {
+ init() {
+ }
+
+ register() {
+ debug("register");
+ this.registerContent("chrome://geckoview/content/GeckoViewSelectionActionContent.js");
+ }
+
+ unregister() {
+ debug("unregister");
+ }
+
+ // Message manager event handler.
+ receiveMessage(aMsg) {
+ debug("receiveMessage " + aMsg.name);
+ }
+}
--- a/mobile/android/modules/geckoview/moz.build
+++ b/mobile/android/modules/geckoview/moz.build
@@ -8,15 +8,16 @@ EXTRA_JS_MODULES += [
'AndroidLog.jsm',
'GeckoViewContent.jsm',
'GeckoViewContentModule.jsm',
'GeckoViewModule.jsm',
'GeckoViewNavigation.jsm',
'GeckoViewProgress.jsm',
'GeckoViewRemoteDebugger.jsm',
'GeckoViewScroll.jsm',
+ 'GeckoViewSelectionAction.jsm',
'GeckoViewSettings.jsm',
'GeckoViewTab.jsm',
'GeckoViewTrackingProtection.jsm',
'GeckoViewUtils.jsm',
'LoadURIDelegate.jsm',
'Messaging.jsm',
]