Bug 1520983 - part 1: Add new content command event for inserting text r=smaug

For inserting text from OS in special cases, e.g., when inserting 2 or more characters
per keydown or inserting text without key press, we use a set of composition events on
macOS, but the other browsers don't use composition events.  Instead, they expose only
`beforeinput` event and `input` event.  We should follow their behavior for web-compat
because `beforeinput` events for IME composition are never cancelable, but the
`beforeinput` events for the cases are cancelable of the other browsers.

Differential Revision: https://phabricator.services.mozilla.com/D114826
This commit is contained in:
Masayuki Nakano 2021-05-17 23:52:43 +00:00
Родитель 7820e16a1d
Коммит 1bb2df9ea3
13 изменённых файлов: 400 добавлений и 5 удалений

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

@ -2003,6 +2003,25 @@ nsDOMWindowUtils::GetIMEStatus(uint32_t* aState) {
return NS_OK;
}
NS_IMETHODIMP
nsDOMWindowUtils::GetInputContextOrigin(uint32_t* aOrigin) {
NS_ENSURE_ARG_POINTER(aOrigin);
nsCOMPtr<nsIWidget> widget = GetWidget();
if (!widget) {
return NS_ERROR_FAILURE;
}
InputContext context = widget->GetInputContext();
static_assert(InputContext::Origin::ORIGIN_MAIN == INPUT_CONTEXT_ORIGIN_MAIN);
static_assert(InputContext::Origin::ORIGIN_CONTENT ==
INPUT_CONTEXT_ORIGIN_CONTENT);
MOZ_ASSERT(context.mOrigin == InputContext::Origin::ORIGIN_MAIN ||
context.mOrigin == InputContext::Origin::ORIGIN_CONTENT);
*aOrigin = static_cast<uint32_t>(context.mOrigin);
return NS_OK;
}
NS_IMETHODIMP
nsDOMWindowUtils::GetFocusedInputType(nsAString& aType) {
nsCOMPtr<nsIWidget> widget = GetWidget();
@ -2337,7 +2356,8 @@ nsDOMWindowUtils::SendSelectionSetEvent(uint32_t aOffset, uint32_t aLength,
NS_IMETHODIMP
nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType,
nsITransferable* aTransferable) {
nsITransferable* aTransferable,
const nsAString& aString) {
// get the widget to send the event to
nsCOMPtr<nsIWidget> widget = GetWidget();
if (!widget) return NS_ERROR_FAILURE;
@ -2355,6 +2375,8 @@ nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType,
msg = eContentCommandUndo;
} else if (aType.EqualsLiteral("redo")) {
msg = eContentCommandRedo;
} else if (aType.EqualsLiteral("insertText")) {
msg = eContentCommandInsertText;
} else if (aType.EqualsLiteral("pasteTransferable")) {
msg = eContentCommandPasteTransferable;
} else {
@ -2362,6 +2384,9 @@ nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType,
}
WidgetContentCommandEvent event(true, msg, widget);
if (msg == eContentCommandInsertText) {
event.mString.emplace(aString);
}
if (msg == eContentCommandPasteTransferable) {
event.mTransferable = aTransferable;
}

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

@ -875,6 +875,9 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
case eContentCommandLookUpDictionary:
DoContentCommandEvent(aEvent->AsContentCommandEvent());
break;
case eContentCommandInsertText:
DoContentCommandInsertTextEvent(aEvent->AsContentCommandEvent());
break;
case eContentCommandScroll:
DoContentCommandScrollEvent(aEvent->AsContentCommandEvent());
break;
@ -5923,6 +5926,43 @@ nsresult EventStateManager::DoContentCommandEvent(
return NS_OK;
}
nsresult EventStateManager::DoContentCommandInsertTextEvent(
WidgetContentCommandEvent* aEvent) {
MOZ_ASSERT(aEvent);
MOZ_ASSERT(aEvent->mMessage == eContentCommandInsertText);
MOZ_DIAGNOSTIC_ASSERT(aEvent->mString.isSome());
MOZ_DIAGNOSTIC_ASSERT(!aEvent->mString.ref().IsEmpty());
aEvent->mIsEnabled = false;
aEvent->mSucceeded = false;
NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE);
if (XRE_IsParentProcess()) {
// Handle it in focused content process if there is.
if (BrowserParent* remote = BrowserParent::GetFocused()) {
remote->SendInsertText(aEvent->mString.ref());
aEvent->mIsEnabled = true; // XXX it can be a lie...
aEvent->mSucceeded = true;
return NS_OK;
}
}
// If there is no active editor in this process, we should treat the command
// is disabled.
RefPtr<TextEditor> activeEditor =
nsContentUtils::GetActiveEditor(mPresContext);
if (!activeEditor) {
aEvent->mSucceeded = true;
return NS_OK;
}
nsresult rv = activeEditor->InsertTextAsAction(aEvent->mString.ref());
aEvent->mIsEnabled = rv != NS_SUCCESS_DOM_NO_OPERATION;
aEvent->mSucceeded = NS_SUCCEEDED(rv);
return NS_OK;
}
nsresult EventStateManager::DoContentCommandScrollEvent(
WidgetContentCommandEvent* aEvent) {
NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE);

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

@ -1039,6 +1039,8 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
MOZ_CAN_RUN_SCRIPT
nsresult DoContentCommandEvent(WidgetContentCommandEvent* aEvent);
MOZ_CAN_RUN_SCRIPT
nsresult DoContentCommandInsertTextEvent(WidgetContentCommandEvent* aEvent);
nsresult DoContentCommandScrollEvent(WidgetContentCommandEvent* aEvent);
dom::BrowserParent* GetCrossProcessTarget();

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

@ -1089,6 +1089,14 @@ interface nsIDOMWindowUtils : nsISupports {
*/
readonly attribute unsigned long IMEStatus;
/**
* Get whether current input context (including IME status) in the widget
* is set by content or not.
*/
const unsigned long INPUT_CONTEXT_ORIGIN_MAIN = 0;
const unsigned long INPUT_CONTEXT_ORIGIN_CONTENT = 1;
readonly attribute unsigned long inputContextOrigin;
/**
* Get the number of screen pixels per CSS pixel.
*
@ -1156,12 +1164,16 @@ interface nsIDOMWindowUtils : nsISupports {
* Will throw a DOM security error if called without chrome privileges.
*
* @param aType Type of command content event to send. Can be one of "cut",
* "copy", "paste", "delete", "undo", "redo", or "pasteTransferable".
* "copy", "paste", "delete", "undo", "redo", "insertText" or
* "pasteTransferable".
* @param aTransferable an instance of nsITransferable when aType is
* "pasteTransferable"
* @param aString The string to be inserted into focused editor when aType is
* "insertText"
*/
void sendContentCommandEvent(in AString aType,
[optional] in nsITransferable aTransferable);
[optional] in nsITransferable aTransferable,
[optional] in AString aString);
/**
* If sendQueryContentEvent()'s aAdditionalFlags argument is

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

@ -2105,6 +2105,21 @@ mozilla::ipc::IPCResult BrowserChild::RecvNormalPrioritySelectionEvent(
return RecvSelectionEvent(aEvent);
}
mozilla::ipc::IPCResult BrowserChild::RecvInsertText(
const nsString& aStringToInsert) {
// Use normal event path to reach focused document.
WidgetContentCommandEvent localEvent(true, eContentCommandInsertText,
mPuppetWidget);
localEvent.mString = Some(aStringToInsert);
DispatchWidgetEventViaAPZ(localEvent);
return IPC_OK();
}
mozilla::ipc::IPCResult BrowserChild::RecvNormalPriorityInsertText(
const nsString& aStringToInsert) {
return RecvInsertText(aStringToInsert);
}
mozilla::ipc::IPCResult BrowserChild::RecvPasteTransferable(
const IPCDataTransfer& aDataTransfer, const bool& aIsPrivateData,
nsIPrincipal* aRequestingPrincipal,

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

@ -417,6 +417,11 @@ class BrowserChild final : public nsMessageManagerScriptExecutor,
mozilla::ipc::IPCResult RecvSetIsUnderHiddenEmbedderElement(
const bool& aIsUnderHiddenEmbedderElement);
mozilla::ipc::IPCResult RecvInsertText(const nsString& aStringToInsert);
mozilla::ipc::IPCResult RecvNormalPriorityInsertText(
const nsString& aStringToInsert);
MOZ_CAN_RUN_SCRIPT_BOUNDARY
mozilla::ipc::IPCResult RecvPasteTransferable(
const IPCDataTransfer& aDataTransfer, const bool& aIsPrivateData,

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

@ -3072,6 +3072,15 @@ bool BrowserParent::SendSelectionEvent(WidgetSelectionEvent& aEvent) {
return true;
}
bool BrowserParent::SendInsertText(const nsString& aStringToInsert) {
if (mIsDestroyed) {
return false;
}
return Manager()->IsInputPriorityEventEnabled()
? PBrowserParent::SendInsertText(aStringToInsert)
: PBrowserParent::SendNormalPriorityInsertText(aStringToInsert);
}
bool BrowserParent::SendPasteTransferable(
const IPCDataTransfer& aDataTransfer, const bool& aIsPrivateData,
nsIPrincipal* aRequestingPrincipal,

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

@ -608,6 +608,8 @@ class BrowserParent final : public PBrowserParent,
bool HandleQueryContentEvent(mozilla::WidgetQueryContentEvent& aEvent);
bool SendInsertText(const nsString& aStringToInsert);
bool SendPasteTransferable(const IPCDataTransfer& aDataTransfer,
const bool& aIsPrivateData,
nsIPrincipal* aRequestingPrincipal,

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

@ -869,6 +869,12 @@ child:
[Priority=input] async SelectionEvent(WidgetSelectionEvent event);
async NormalPrioritySelectionEvent(WidgetSelectionEvent event);
/**
* Dispatch eContentCommandInsertText event in the remote process.
*/
[Priority=input] async InsertText(nsString aStringToInsert);
async NormalPriorityInsertText(nsString aStringToInsert);
/**
* Call PasteTransferable via a controller on the content process
* to handle the command content event, "pasteTransferable".

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

@ -1,7 +1,6 @@
[browser_bug527935.js]
skip-if = toolkit == 'android'
prefs =
editor.white_space_normalization.blink_compatible=true
support-files =
bug527935.html
bug527935_2.html
[browser_content_command_insert_text.js]

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

@ -0,0 +1,271 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { CustomizableUITestUtils } = ChromeUtils.import(
"resource://testing-common/CustomizableUITestUtils.jsm"
);
const { ContentTaskUtils } = ChromeUtils.import(
"resource://testing-common/ContentTaskUtils.jsm"
);
const gCUITestUtils = new CustomizableUITestUtils(window);
const gDOMWindowUtils = EventUtils._getDOMWindowUtils(window);
add_task(async function test_setup() {
await gCUITestUtils.addSearchBar();
registerCleanupFunction(() => {
gCUITestUtils.removeSearchBar();
});
});
function promiseResettingSearchBarAndFocus() {
const waitForFocusInSearchBar = BrowserTestUtils.waitForEvent(
BrowserSearch.searchBar.textbox,
"focus"
);
BrowserSearch.searchBar.textbox.focus();
BrowserSearch.searchBar.textbox.value = "";
return Promise.all([
waitForFocusInSearchBar,
TestUtils.waitForCondition(
() =>
gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED &&
gDOMWindowUtils.inputContextOrigin ===
Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_MAIN
),
]);
}
function promiseIMEStateEnabledByRemote() {
return TestUtils.waitForCondition(
() =>
gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED &&
gDOMWindowUtils.inputContextOrigin ===
Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_CONTENT
);
}
function promiseContentTick(browser) {
return SpecialPowers.spawn(browser, [], async () => {
await new Promise(r => {
content.requestAnimationFrame(() => {
content.requestAnimationFrame(r);
});
});
});
}
add_task(async function test_text_editor_in_chrome() {
await promiseResettingSearchBarAndFocus();
let events = [];
function logEvent(event) {
events.push(event);
}
BrowserSearch.searchBar.textbox.addEventListener("beforeinput", logEvent);
gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ");
is(
BrowserSearch.searchBar.textbox.value,
"XYZ",
"The string should be inserted into the focused search bar"
);
is(
events.length,
1,
"One beforeinput event should be fired in the searchbar"
);
is(events[0]?.inputType, "insertText", 'inputType should be "insertText"');
is(events[0]?.data, "XYZ", 'inputType should be "XYZ"');
is(events[0]?.cancelable, true, "beforeinput event should be cancelable");
BrowserSearch.searchBar.textbox.removeEventListener("beforeinput", logEvent);
BrowserSearch.searchBar.textbox.blur();
});
add_task(async function test_text_editor_in_content() {
for (const test of [
{
tag: "input",
target: "input",
nonTarget: "input + input",
page: 'data:text/html,<input value="abc"><input value="def">',
},
{
tag: "textarea",
target: "textarea",
nonTarget: "textarea + textarea",
page: "data:text/html,<textarea>abc</textarea><textarea>def</textarea>",
},
]) {
// Once, move focus to chrome's searchbar.
await promiseResettingSearchBarAndFocus();
await BrowserTestUtils.withNewTab(test.page, async browser => {
await SpecialPowers.spawn(browser, [test], async function(aTest) {
content.window.focus();
await ContentTaskUtils.waitForCondition(() =>
content.document.hasFocus()
);
const target = content.document.querySelector(aTest.target);
target.focus();
target.selectionStart = target.selectionEnd = 2;
content.document.documentElement.scrollTop; // Flush pending things
});
await promiseIMEStateEnabledByRemote();
const waitForBeforeInputEvent = SpecialPowers.spawn(
browser,
[test],
async function(aTest) {
await new Promise(resolve => {
content.document.querySelector(aTest.target).addEventListener(
"beforeinput",
event => {
is(
event.inputType,
"insertText",
`The inputType of beforeinput event fired on <${aTest.target}> should be "insertText"`
);
is(
event.data,
"XYZ",
`The data of beforeinput event fired on <${aTest.target}> should be "XYZ"`
);
is(
event.cancelable,
true,
`The beforeinput event fired on <${aTest.target}> should be cancelable`
);
resolve();
},
{ once: true }
);
});
}
);
const waitForInputEvent = BrowserTestUtils.waitForContentEvent(
browser,
"input"
);
await promiseContentTick(browser); // Ensure "input" event listener in the remote process
gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ");
await waitForBeforeInputEvent;
await waitForInputEvent;
await SpecialPowers.spawn(browser, [test], async function(aTest) {
is(
content.document.querySelector(aTest.target).value,
"abXYZc",
`The string should be inserted into the focused <${aTest.target}> element`
);
is(
content.document.querySelector(aTest.nonTarget).value,
"def",
`The string should not be inserted into the non-focused <${aTest.nonTarget}> element`
);
});
});
is(
BrowserSearch.searchBar.textbox.value,
"",
"The string should not be inserted into the previously focused search bar"
);
}
});
add_task(async function test_html_editor_in_content() {
for (const test of [
{
mode: "contenteditable",
target: "div",
page: "data:text/html,<div contenteditable>abc</div>",
},
{
mode: "designMode",
target: "div",
page: "data:text/html,<div>abc</div>",
},
]) {
// Once, move focus to chrome's searchbar.
await promiseResettingSearchBarAndFocus();
await BrowserTestUtils.withNewTab(test.page, async browser => {
await SpecialPowers.spawn(browser, [test], async function(aTest) {
content.window.focus();
await ContentTaskUtils.waitForCondition(() =>
content.document.hasFocus()
);
const target = content.document.querySelector(aTest.target);
if (aTest.mode == "designMode") {
content.document.designMode = "on";
content.window.focus();
} else {
target.focus();
}
content.window.getSelection().collapse(target.firstChild, 2);
content.document.documentElement.scrollTop; // Flush pending things
});
await promiseIMEStateEnabledByRemote();
const waitForBeforeInputEvent = SpecialPowers.spawn(
browser,
[test],
async function(aTest) {
await new Promise(resolve => {
const eventTarget =
aTest.mode === "designMode"
? content.document
: content.document.querySelector(aTest.target);
eventTarget.addEventListener(
"beforeinput",
event => {
is(
event.inputType,
"insertText",
`The inputType of beforeinput event fired on ${aTest.mode} editor should be "insertText"`
);
is(
event.data,
"XYZ",
`The data of beforeinput event fired on ${aTest.mode} editor should be "XYZ"`
);
is(
event.cancelable,
true,
`The beforeinput event fired on ${aTest.mode} editor should be cancelable`
);
resolve();
},
{ once: true }
);
});
}
);
const waitForInputEvent = BrowserTestUtils.waitForContentEvent(
browser,
"input"
);
await promiseContentTick(browser); // Ensure "input" event listener in the remote process
gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ");
await waitForBeforeInputEvent;
await waitForInputEvent;
await SpecialPowers.spawn(browser, [test], async function(aTest) {
is(
content.document.querySelector(aTest.target).innerHTML,
"abXYZc",
`The string should be inserted into the focused ${aTest.mode} editor`
);
});
});
is(
BrowserSearch.searchBar.textbox.value,
"",
"The string should not be inserted into the previously focused search bar"
);
}
});

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

@ -327,6 +327,9 @@ NS_EVENT_MESSAGE(eContentCommandPaste)
NS_EVENT_MESSAGE(eContentCommandDelete)
NS_EVENT_MESSAGE(eContentCommandUndo)
NS_EVENT_MESSAGE(eContentCommandRedo)
// eContentCommandInsertText tries to insert text with replacing selection
// in focused editor.
NS_EVENT_MESSAGE(eContentCommandInsertText)
NS_EVENT_MESSAGE(eContentCommandPasteTransferable)
NS_EVENT_MESSAGE(eContentCommandLookUpDictionary)
// eContentCommandScroll scrolls the nearest scrollable element to the

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

@ -9,10 +9,12 @@
#include <stdint.h>
#include "mozilla/BasicEvents.h"
#include "mozilla/Maybe.h"
#include "nsCOMPtr.h"
#include "nsAtom.h"
#include "nsGkAtoms.h"
#include "nsITransferable.h"
#include "nsString.h"
namespace mozilla {
@ -47,6 +49,9 @@ class WidgetContentCommandEvent : public WidgetGUIEvent {
return nullptr;
}
// eContentCommandInsertText
mozilla::Maybe<nsString> mString; // [in]
// eContentCommandPasteTransferable
nsCOMPtr<nsITransferable> mTransferable; // [in]
@ -72,6 +77,7 @@ class WidgetContentCommandEvent : public WidgetGUIEvent {
bool aCopyTargets) {
AssignGUIEventData(aEvent, aCopyTargets);
mString = aEvent.mString;
mScroll = aEvent.mScroll;
mOnlyEnabledCheck = aEvent.mOnlyEnabledCheck;
mSucceeded = aEvent.mSucceeded;