Bug 1441279 - 2. Add selection action JS modules; r=esawin,snorp

Add JS modules for listening to accessible caret events, and relaying
those events to Java.

MozReview-Commit-ID: JPLTMzK7Nzn

--HG--
extra : rebase_source : 394cc9b74f53dc2c1ca2fb6062747c37d1d5b582
This commit is contained in:
Jim Chen 2018-04-02 17:13:45 -04:00
Родитель 2b57a55d1b
Коммит 474f97832a
5 изменённых файлов: 291 добавлений и 0 удалений

Просмотреть файл

@ -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);

Просмотреть файл

@ -84,6 +84,8 @@ function startup() {
"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.

Просмотреть файл

@ -12,3 +12,4 @@ geckoview.jar:
content/GeckoViewContentSettings.js
content/GeckoViewNavigationContent.js
content/GeckoViewScrollContent.js
content/GeckoViewSelectionActionContent.js

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -13,6 +13,7 @@ EXTRA_JS_MODULES += [
'GeckoViewProgress.jsm',
'GeckoViewRemoteDebugger.jsm',
'GeckoViewScroll.jsm',
'GeckoViewSelectionAction.jsm',
'GeckoViewSettings.jsm',
'GeckoViewTab.jsm',
'GeckoViewTrackingProtection.jsm',