Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: c++; c-basic-offset: 2; tab-width: 4; indent-tabs-mode: nil; -*-
* vim: set sw=2 ts=4 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/. */
#include "GeckoEditableSupport.h"
#include "AndroidRect.h"
#include "KeyEvent.h"
#include "PuppetWidget.h"
#include "nsIContent.h"
#include "nsITransferable.h"
#include "nsStringStream.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/java/GeckoEditableChildWrappers.h"
#include "mozilla/java/GeckoServiceChildProcessWrappers.h"
#include "mozilla/jni/NativesInlines.h"
#include "mozilla/Logging.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_intl.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextEventDispatcherListener.h"
#include "mozilla/TextEvents.h"
#include "mozilla/ToString.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/widget/GeckoViewSupport.h"
#include <android/api-level.h>
#include <android/input.h>
#include <android/log.h>
#ifdef NIGHTLY_BUILD
static mozilla::LazyLogModule sGeckoEditableSupportLog("GeckoEditableSupport");
# define ALOGIME(...) \
MOZ_LOG(sGeckoEditableSupportLog, LogLevel::Debug, (__VA_ARGS__))
#else
# define ALOGIME(args...) \
do { \
} while (0)
#endif
static uint32_t ConvertAndroidKeyCodeToDOMKeyCode(int32_t androidKeyCode) {
// Special-case alphanumeric keycodes because they are most common.
if (androidKeyCode >= AKEYCODE_A && androidKeyCode <= AKEYCODE_Z) {
return androidKeyCode - AKEYCODE_A + NS_VK_A;
}
if (androidKeyCode >= AKEYCODE_0 && androidKeyCode <= AKEYCODE_9) {
return androidKeyCode - AKEYCODE_0 + NS_VK_0;
}
switch (androidKeyCode) {
// KEYCODE_UNKNOWN (0) ... KEYCODE_HOME (3)
case AKEYCODE_BACK:
return NS_VK_ESCAPE;
// KEYCODE_CALL (5) ... KEYCODE_POUND (18)
case AKEYCODE_DPAD_UP:
return NS_VK_UP;
case AKEYCODE_DPAD_DOWN:
return NS_VK_DOWN;
case AKEYCODE_DPAD_LEFT:
return NS_VK_LEFT;
case AKEYCODE_DPAD_RIGHT:
return NS_VK_RIGHT;
case AKEYCODE_DPAD_CENTER:
return NS_VK_RETURN;
case AKEYCODE_VOLUME_UP:
return NS_VK_VOLUME_UP;
case AKEYCODE_VOLUME_DOWN:
return NS_VK_VOLUME_DOWN;
// KEYCODE_VOLUME_POWER (26) ... KEYCODE_Z (54)
case AKEYCODE_COMMA:
return NS_VK_COMMA;
case AKEYCODE_PERIOD:
return NS_VK_PERIOD;
case AKEYCODE_ALT_LEFT:
return NS_VK_ALT;
case AKEYCODE_ALT_RIGHT:
return NS_VK_ALT;
case AKEYCODE_SHIFT_LEFT:
return NS_VK_SHIFT;
case AKEYCODE_SHIFT_RIGHT:
return NS_VK_SHIFT;
case AKEYCODE_TAB:
return NS_VK_TAB;
case AKEYCODE_SPACE:
return NS_VK_SPACE;
// KEYCODE_SYM (63) ... KEYCODE_ENVELOPE (65)
case AKEYCODE_ENTER:
return NS_VK_RETURN;
case AKEYCODE_DEL:
return NS_VK_BACK; // Backspace
case AKEYCODE_GRAVE:
return NS_VK_BACK_QUOTE;
// KEYCODE_MINUS (69)
case AKEYCODE_EQUALS:
return NS_VK_EQUALS;
case AKEYCODE_LEFT_BRACKET:
return NS_VK_OPEN_BRACKET;
case AKEYCODE_RIGHT_BRACKET:
return NS_VK_CLOSE_BRACKET;
case AKEYCODE_BACKSLASH:
return NS_VK_BACK_SLASH;
case AKEYCODE_SEMICOLON:
return NS_VK_SEMICOLON;
// KEYCODE_APOSTROPHE (75)
case AKEYCODE_SLASH:
return NS_VK_SLASH;
// KEYCODE_AT (77) ... KEYCODE_MEDIA_FAST_FORWARD (90)
case AKEYCODE_MUTE:
return NS_VK_VOLUME_MUTE;
case AKEYCODE_PAGE_UP:
return NS_VK_PAGE_UP;
case AKEYCODE_PAGE_DOWN:
return NS_VK_PAGE_DOWN;
// KEYCODE_PICTSYMBOLS (94) ... KEYCODE_BUTTON_MODE (110)
case AKEYCODE_ESCAPE:
return NS_VK_ESCAPE;
case AKEYCODE_FORWARD_DEL:
return NS_VK_DELETE;
case AKEYCODE_CTRL_LEFT:
return NS_VK_CONTROL;
case AKEYCODE_CTRL_RIGHT:
return NS_VK_CONTROL;
case AKEYCODE_CAPS_LOCK:
return NS_VK_CAPS_LOCK;
case AKEYCODE_SCROLL_LOCK:
return NS_VK_SCROLL_LOCK;
// KEYCODE_META_LEFT (117) ... KEYCODE_FUNCTION (119)
case AKEYCODE_SYSRQ:
return NS_VK_PRINTSCREEN;
case AKEYCODE_BREAK:
return NS_VK_PAUSE;
case AKEYCODE_MOVE_HOME:
return NS_VK_HOME;
case AKEYCODE_MOVE_END:
return NS_VK_END;
case AKEYCODE_INSERT:
return NS_VK_INSERT;
// KEYCODE_FORWARD (125) ... KEYCODE_MEDIA_RECORD (130)
case AKEYCODE_F1:
return NS_VK_F1;
case AKEYCODE_F2:
return NS_VK_F2;
case AKEYCODE_F3:
return NS_VK_F3;
case AKEYCODE_F4:
return NS_VK_F4;
case AKEYCODE_F5:
return NS_VK_F5;
case AKEYCODE_F6:
return NS_VK_F6;
case AKEYCODE_F7:
return NS_VK_F7;
case AKEYCODE_F8:
return NS_VK_F8;
case AKEYCODE_F9:
return NS_VK_F9;
case AKEYCODE_F10:
return NS_VK_F10;
case AKEYCODE_F11:
return NS_VK_F11;
case AKEYCODE_F12:
return NS_VK_F12;
case AKEYCODE_NUM_LOCK:
return NS_VK_NUM_LOCK;
case AKEYCODE_NUMPAD_0:
return NS_VK_NUMPAD0;
case AKEYCODE_NUMPAD_1:
return NS_VK_NUMPAD1;
case AKEYCODE_NUMPAD_2:
return NS_VK_NUMPAD2;
case AKEYCODE_NUMPAD_3:
return NS_VK_NUMPAD3;
case AKEYCODE_NUMPAD_4:
return NS_VK_NUMPAD4;
case AKEYCODE_NUMPAD_5:
return NS_VK_NUMPAD5;
case AKEYCODE_NUMPAD_6:
return NS_VK_NUMPAD6;
case AKEYCODE_NUMPAD_7:
return NS_VK_NUMPAD7;
case AKEYCODE_NUMPAD_8:
return NS_VK_NUMPAD8;
case AKEYCODE_NUMPAD_9:
return NS_VK_NUMPAD9;
case AKEYCODE_NUMPAD_DIVIDE:
return NS_VK_DIVIDE;
case AKEYCODE_NUMPAD_MULTIPLY:
return NS_VK_MULTIPLY;
case AKEYCODE_NUMPAD_SUBTRACT:
return NS_VK_SUBTRACT;
case AKEYCODE_NUMPAD_ADD:
return NS_VK_ADD;
case AKEYCODE_NUMPAD_DOT:
return NS_VK_DECIMAL;
case AKEYCODE_NUMPAD_COMMA:
return NS_VK_SEPARATOR;
case AKEYCODE_NUMPAD_ENTER:
return NS_VK_RETURN;
case AKEYCODE_NUMPAD_EQUALS:
return NS_VK_EQUALS;
// KEYCODE_NUMPAD_LEFT_PAREN (162) ... KEYCODE_CALCULATOR (210)
// Needs to confirm the behavior. If the key switches the open state
// of Japanese IME (or switches input character between Hiragana and
// Roman numeric characters), then, it might be better to use
// NS_VK_KANJI which is used for Alt+Zenkaku/Hankaku key on Windows.
case AKEYCODE_ZENKAKU_HANKAKU:
return 0;
case AKEYCODE_EISU:
return NS_VK_EISU;
case AKEYCODE_MUHENKAN:
return NS_VK_NONCONVERT;
case AKEYCODE_HENKAN:
return NS_VK_CONVERT;
case AKEYCODE_KATAKANA_HIRAGANA:
return 0;
case AKEYCODE_YEN:
return NS_VK_BACK_SLASH; // Same as other platforms.
case AKEYCODE_RO:
return NS_VK_BACK_SLASH; // Same as other platforms.
case AKEYCODE_KANA:
return NS_VK_KANA;
case AKEYCODE_ASSIST:
return NS_VK_HELP;
// the A key is the action key for gamepad devices.
case AKEYCODE_BUTTON_A:
return NS_VK_RETURN;
default:
ALOG(
"ConvertAndroidKeyCodeToDOMKeyCode: "
"No DOM keycode for Android keycode %d",
int(androidKeyCode));
return 0;
}
}
static KeyNameIndex ConvertAndroidKeyCodeToKeyNameIndex(
int32_t keyCode, int32_t action, int32_t domPrintableKeyValue) {
// Special-case alphanumeric keycodes because they are most common.
if (keyCode >= AKEYCODE_A && keyCode <= AKEYCODE_Z) {
return KEY_NAME_INDEX_USE_STRING;
}
if (keyCode >= AKEYCODE_0 && keyCode <= AKEYCODE_9) {
return KEY_NAME_INDEX_USE_STRING;
}
switch (keyCode) {
#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \
case aNativeKey: \
return aKeyNameIndex;
#include "NativeKeyToDOMKeyName.h"
#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX
// KEYCODE_0 (7) ... KEYCODE_9 (16)
case AKEYCODE_STAR: // '*' key
case AKEYCODE_POUND: // '#' key
// KEYCODE_A (29) ... KEYCODE_Z (54)
case AKEYCODE_COMMA: // ',' key
case AKEYCODE_PERIOD: // '.' key
case AKEYCODE_SPACE:
case AKEYCODE_GRAVE: // '`' key
case AKEYCODE_MINUS: // '-' key
case AKEYCODE_EQUALS: // '=' key
case AKEYCODE_LEFT_BRACKET: // '[' key
case AKEYCODE_RIGHT_BRACKET: // ']' key
case AKEYCODE_BACKSLASH: // '\' key
case AKEYCODE_SEMICOLON: // ';' key
case AKEYCODE_APOSTROPHE: // ''' key
case AKEYCODE_SLASH: // '/' key
case AKEYCODE_AT: // '@' key
case AKEYCODE_PLUS: // '+' key
case AKEYCODE_NUMPAD_0:
case AKEYCODE_NUMPAD_1:
case AKEYCODE_NUMPAD_2:
case AKEYCODE_NUMPAD_3:
case AKEYCODE_NUMPAD_4:
case AKEYCODE_NUMPAD_5:
case AKEYCODE_NUMPAD_6:
case AKEYCODE_NUMPAD_7:
case AKEYCODE_NUMPAD_8:
case AKEYCODE_NUMPAD_9:
case AKEYCODE_NUMPAD_DIVIDE:
case AKEYCODE_NUMPAD_MULTIPLY:
case AKEYCODE_NUMPAD_SUBTRACT:
case AKEYCODE_NUMPAD_ADD:
case AKEYCODE_NUMPAD_DOT:
case AKEYCODE_NUMPAD_COMMA:
case AKEYCODE_NUMPAD_EQUALS:
case AKEYCODE_NUMPAD_LEFT_PAREN:
case AKEYCODE_NUMPAD_RIGHT_PAREN:
case AKEYCODE_YEN: // yen sign key
case AKEYCODE_RO: // Japanese Ro key
return KEY_NAME_INDEX_USE_STRING;
case AKEYCODE_NUM: // XXX Not sure
case AKEYCODE_PICTSYMBOLS:
case AKEYCODE_BUTTON_A:
case AKEYCODE_BUTTON_B:
case AKEYCODE_BUTTON_C:
case AKEYCODE_BUTTON_X:
case AKEYCODE_BUTTON_Y:
case AKEYCODE_BUTTON_Z:
case AKEYCODE_BUTTON_L1:
case AKEYCODE_BUTTON_R1:
case AKEYCODE_BUTTON_L2:
case AKEYCODE_BUTTON_R2:
case AKEYCODE_BUTTON_THUMBL:
case AKEYCODE_BUTTON_THUMBR:
case AKEYCODE_BUTTON_START:
case AKEYCODE_BUTTON_SELECT:
case AKEYCODE_BUTTON_MODE:
case AKEYCODE_MEDIA_CLOSE:
case AKEYCODE_BUTTON_1:
case AKEYCODE_BUTTON_2:
case AKEYCODE_BUTTON_3:
case AKEYCODE_BUTTON_4:
case AKEYCODE_BUTTON_5:
case AKEYCODE_BUTTON_6:
case AKEYCODE_BUTTON_7:
case AKEYCODE_BUTTON_8:
case AKEYCODE_BUTTON_9:
case AKEYCODE_BUTTON_10:
case AKEYCODE_BUTTON_11:
case AKEYCODE_BUTTON_12:
case AKEYCODE_BUTTON_13:
case AKEYCODE_BUTTON_14:
case AKEYCODE_BUTTON_15:
case AKEYCODE_BUTTON_16:
return KEY_NAME_INDEX_Unidentified;
case AKEYCODE_UNKNOWN:
MOZ_ASSERT(action != AKEY_EVENT_ACTION_MULTIPLE,
"Don't call this when action is AKEY_EVENT_ACTION_MULTIPLE!");
// It's actually an unknown key if the action isn't ACTION_MULTIPLE.
// However, it might cause text input. So, let's check the value.
return domPrintableKeyValue ? KEY_NAME_INDEX_USE_STRING
: KEY_NAME_INDEX_Unidentified;
default:
ALOG(
"ConvertAndroidKeyCodeToKeyNameIndex: "
"No DOM key name index for Android keycode %d",
keyCode);
return KEY_NAME_INDEX_Unidentified;
}
}
static CodeNameIndex ConvertAndroidScanCodeToCodeNameIndex(int32_t scanCode) {
switch (scanCode) {
#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \
case aNativeKey: \
return aCodeNameIndex;
#include "NativeKeyToDOMCodeName.h"
#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX
default:
return CODE_NAME_INDEX_UNKNOWN;
}
}
static void InitKeyEvent(WidgetKeyboardEvent& aEvent, int32_t aAction,
int32_t aKeyCode, int32_t aScanCode,
int32_t aMetaState, int64_t aTime,
int32_t aDomPrintableKeyValue, int32_t aRepeatCount,
int32_t aFlags) {
const uint32_t domKeyCode = ConvertAndroidKeyCodeToDOMKeyCode(aKeyCode);
aEvent.mModifiers = nsWindow::GetModifiers(aMetaState);
aEvent.mKeyCode = domKeyCode;
aEvent.mIsRepeat =
(aEvent.mMessage == eKeyDown || aEvent.mMessage == eKeyPress) &&
((aFlags & java::sdk::KeyEvent::FLAG_LONG_PRESS) || aRepeatCount);
aEvent.mKeyNameIndex = ConvertAndroidKeyCodeToKeyNameIndex(
aKeyCode, aAction, aDomPrintableKeyValue);
aEvent.mCodeNameIndex = ConvertAndroidScanCodeToCodeNameIndex(aScanCode);
if (aEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING &&
aDomPrintableKeyValue) {
aEvent.mKeyValue = char16_t(aDomPrintableKeyValue);
}
aEvent.mLocation =
WidgetKeyboardEvent::ComputeLocationFromCodeValue(aEvent.mCodeNameIndex);
aEvent.mTimeStamp = nsWindow::GetEventTimeStamp(aTime);
}
static nscolor ConvertAndroidColor(uint32_t aArgb) {
return NS_RGBA((aArgb & 0x00ff0000) >> 16, (aArgb & 0x0000ff00) >> 8,
(aArgb & 0x000000ff), (aArgb & 0xff000000) >> 24);
}
static jni::ObjectArray::LocalRef ConvertRectArrayToJavaRectFArray(
const nsTArray<LayoutDeviceIntRect>& aRects) {
const size_t length = aRects.Length();
auto rects = jni::ObjectArray::New<java::sdk::RectF>(length);
for (size_t i = 0; i < length; i++) {
const LayoutDeviceIntRect& tmp = aRects[i];
auto rect = java::sdk::RectF::New(tmp.x, tmp.y, tmp.XMost(), tmp.YMost());
rects->SetElement(i, rect);
}
return rects;
}
namespace mozilla {
namespace widget {
NS_IMPL_ISUPPORTS(GeckoEditableSupport, TextEventDispatcherListener,
nsISupportsWeakReference)
// This is the blocker helper class whether disposing GeckoEditableChild now.
// During JNI call from GeckoEditableChild, we shouldn't dispose it.
class MOZ_RAII AutoGeckoEditableBlocker final {
public:
explicit AutoGeckoEditableBlocker(GeckoEditableSupport* aGeckoEditableSupport)
: mGeckoEditable(aGeckoEditableSupport) {
mGeckoEditable->AddBlocker();
}
~AutoGeckoEditableBlocker() { mGeckoEditable->ReleaseBlocker(); }
private:
RefPtr<GeckoEditableSupport> mGeckoEditable;
};
RefPtr<TextComposition> GeckoEditableSupport::GetComposition() const {
nsCOMPtr<nsIWidget> widget = GetWidget();
return widget ? IMEStateManager::GetTextCompositionFor(widget) : nullptr;
}
bool GeckoEditableSupport::RemoveComposition(RemoveCompositionFlag aFlag) {
if (!mDispatcher || !mDispatcher->IsComposing()) {
return false;
}
nsEventStatus status = nsEventStatus_eIgnore;
NS_ENSURE_SUCCESS(BeginInputTransaction(mDispatcher), false);
mDispatcher->CommitComposition(
status, aFlag == CANCEL_IME_COMPOSITION ? &EmptyString() : nullptr);
return true;
}
void GeckoEditableSupport::OnKeyEvent(int32_t aAction, int32_t aKeyCode,
int32_t aScanCode, int32_t aMetaState,
int32_t aKeyPressMetaState, int64_t aTime,
int32_t aDomPrintableKeyValue,
int32_t aRepeatCount, int32_t aFlags,
bool aIsSynthesizedImeKey,
jni::Object::Param aOriginalEvent) {
AutoGeckoEditableBlocker blocker(this);
nsCOMPtr<nsIWidget> widget = GetWidget();
RefPtr<TextEventDispatcher> dispatcher =
mDispatcher ? mDispatcher.get()
: widget ? widget->GetTextEventDispatcher()
: nullptr;
NS_ENSURE_TRUE_VOID(dispatcher && widget);
if (!aIsSynthesizedImeKey) {
if (nsWindow* window = GetNsWindow()) {
window->UserActivity();
}
} else if (aIsSynthesizedImeKey && mIMEMaskEventsCount > 0) {
// Don't synthesize editor keys when not focused.
return;
}
EventMessage msg;
if (aAction == java::sdk::KeyEvent::ACTION_DOWN) {
msg = eKeyDown;
} else if (aAction == java::sdk::KeyEvent::ACTION_UP) {
msg = eKeyUp;
} else if (aAction == java::sdk::KeyEvent::ACTION_MULTIPLE) {
// Keys with multiple action are handled in Java,
// and we should never see one here
MOZ_CRASH("Cannot handle key with multiple action");
} else {
NS_WARNING("Unknown key action event");
return;
}
nsEventStatus status = nsEventStatus_eIgnore;
WidgetKeyboardEvent event(true, msg, widget);
InitKeyEvent(event, aAction, aKeyCode, aScanCode, aMetaState, aTime,
aDomPrintableKeyValue, aRepeatCount, aFlags);
if (nsIWidget::UsePuppetWidgets()) {
// Don't use native key bindings.
event.PreventNativeKeyBindings();
}
if (aIsSynthesizedImeKey) {
// Keys synthesized by Java IME code are saved in the mIMEKeyEvents
// array until the next IME_REPLACE_TEXT event, at which point
// these keys are dispatched in sequence.
mIMEKeyEvents.AppendElement(UniquePtr<WidgetEvent>(event.Duplicate()));
} else {
NS_ENSURE_SUCCESS_VOID(BeginInputTransaction(dispatcher));
dispatcher->DispatchKeyboardEvent(msg, event, status);
if (widget->Destroyed() || status == nsEventStatus_eConsumeNoDefault) {
// Skip default processing.
return;
}
mEditable->OnDefaultKeyEvent(aOriginalEvent);
}
// Only send keypress after keydown.
if (msg != eKeyDown) {
return;
}
WidgetKeyboardEvent pressEvent(true, eKeyPress, widget);
InitKeyEvent(pressEvent, aAction, aKeyCode, aScanCode, aKeyPressMetaState,
aTime, aDomPrintableKeyValue, aRepeatCount, aFlags);
if (nsIWidget::UsePuppetWidgets()) {
// Don't use native key bindings.
pressEvent.PreventNativeKeyBindings();
}
if (aIsSynthesizedImeKey) {
mIMEKeyEvents.AppendElement(UniquePtr<WidgetEvent>(pressEvent.Duplicate()));
} else {
dispatcher->MaybeDispatchKeypressEvents(pressEvent, status);
}
}
/*
* Send dummy key events for pages that are unaware of input events,
* to provide web compatibility for pages that depend on key events.
*/
void GeckoEditableSupport::SendIMEDummyKeyEvent(nsIWidget* aWidget,
EventMessage msg) {
AutoGeckoEditableBlocker blocker(this);
nsEventStatus status = nsEventStatus_eIgnore;
MOZ_ASSERT(mDispatcher);
WidgetKeyboardEvent event(true, msg, aWidget);
// TODO: If we can know scan code of the key event which caused replacing
// composition string, we should set mCodeNameIndex here. Then,
// we should rename this method because it becomes not a "dummy"
// keyboard event.
event.mKeyCode = NS_VK_PROCESSKEY;
event.mKeyNameIndex = KEY_NAME_INDEX_Process;
// KeyboardEvents marked as "processed by IME" shouldn't cause any edit
// actions. So, we should set their native key binding to none before
// dispatch to avoid crash on PuppetWidget and avoid running redundant
// path to look for native key bindings.
if (nsIWidget::UsePuppetWidgets()) {
event.PreventNativeKeyBindings();
}
NS_ENSURE_SUCCESS_VOID(BeginInputTransaction(mDispatcher));
mDispatcher->DispatchKeyboardEvent(msg, event, status);
}
void GeckoEditableSupport::AddIMETextChange(
const IMENotification::TextChangeDataBase& aChange) {
mIMEPendingTextChange.MergeWith(aChange);
// We may not be in the middle of flushing,
// in which case this flag is meaningless.
mIMETextChangedDuringFlush = true;
}
void GeckoEditableSupport::PostFlushIMEChanges() {
if (mIMEPendingTextChange.IsValid() || mIMESelectionChanged) {
// Already posted
return;
}
RefPtr<GeckoEditableSupport> self(this);
nsAppShell::PostEvent([this, self] {
nsCOMPtr<nsIWidget> widget = GetWidget();
if (widget && !widget->Destroyed()) {
FlushIMEChanges();
}
});
}
void GeckoEditableSupport::FlushIMEChanges(FlushChangesFlag aFlags) {
// Only send change notifications if we are *not* masking events,
// i.e. if we have a focused editor,
NS_ENSURE_TRUE_VOID(!mIMEMaskEventsCount);
if (mIMEDelaySynchronizeReply && mIMEActiveCompositionCount > 0) {
// We are still expecting more composition events to be handled. Once
// that happens, FlushIMEChanges will be called again.
return;
}
nsCOMPtr<nsIWidget> widget = GetWidget();
NS_ENSURE_TRUE_VOID(widget);
struct TextRecord {
TextRecord() : start(-1), oldEnd(-1), newEnd(-1) {}
bool IsValid() const { return start >= 0; }
nsString text;
int32_t start;
int32_t oldEnd;
int32_t newEnd;
};
TextRecord textTransaction;
nsEventStatus status = nsEventStatus_eIgnore;
bool causedOnlyByComposition = mIMEPendingTextChange.IsValid() &&
mIMEPendingTextChange.mCausedOnlyByComposition;
mIMETextChangedDuringFlush = false;
auto shouldAbort = [=](bool aForce) -> bool {
if (!aForce && !mIMETextChangedDuringFlush) {
return false;
}
// A query event could have triggered more text changes to come in, as
// indicated by our flag. If that happens, try flushing IME changes
// again.
if (aFlags == FLUSH_FLAG_NONE) {
FlushIMEChanges(FLUSH_FLAG_RETRY);
} else {
// Don't retry if already retrying, to avoid infinite loops.
__android_log_print(ANDROID_LOG_WARN, "GeckoEditableSupport",
"Already retrying IME flush");
}
return true;
};
if (mIMEPendingTextChange.IsValid() &&
(mIMEPendingTextChange.mStartOffset !=
mIMEPendingTextChange.mRemovedEndOffset ||
mIMEPendingTextChange.mStartOffset !=
mIMEPendingTextChange.mAddedEndOffset)) {
WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
widget);
if (mIMEPendingTextChange.mAddedEndOffset !=
mIMEPendingTextChange.mStartOffset) {
queryTextContentEvent.InitForQueryTextContent(
mIMEPendingTextChange.mStartOffset,
mIMEPendingTextChange.mAddedEndOffset -
mIMEPendingTextChange.mStartOffset);
widget->DispatchEvent(&queryTextContentEvent, status);
if (shouldAbort(NS_WARN_IF(queryTextContentEvent.Failed()))) {
return;
}
textTransaction.text = queryTextContentEvent.mReply->DataRef();
}
textTransaction.start =
static_cast<int32_t>(mIMEPendingTextChange.mStartOffset);
textTransaction.oldEnd =
static_cast<int32_t>(mIMEPendingTextChange.mRemovedEndOffset);
textTransaction.newEnd =
static_cast<int32_t>(mIMEPendingTextChange.mAddedEndOffset);
}
int32_t selStart = -1;
int32_t selEnd = -1;
if (mIMESelectionChanged) {
if (mCachedSelection.IsValid()) {
selStart = mCachedSelection.mStartOffset;
selEnd = mCachedSelection.mEndOffset;
} else {
// XXX Unfortunately we don't know current selection via selection
// change notification.
// eQuerySelectedText might be newer data than text change data.
// It means that GeckoEditableChild.onSelectionChange may throw
// IllegalArgumentException since we don't merge with newer text
// change.
WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
widget);
widget->DispatchEvent(&querySelectedTextEvent, status);
if (shouldAbort(
NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection()))) {
return;
}
selStart =
static_cast<int32_t>(querySelectedTextEvent.mReply->AnchorOffset());
selEnd =
static_cast<int32_t>(querySelectedTextEvent.mReply->FocusOffset());
}
if (aFlags == FLUSH_FLAG_RECOVER && textTransaction.IsValid()) {
// Sometimes we get out-of-bounds selection during recovery.
// Limit the offsets so we don't crash.
const int32_t end = textTransaction.start + textTransaction.text.Length();
selStart = std::min(selStart, end);
selEnd = std::min(selEnd, end);
}
}
JNIEnv* const env = jni::GetGeckoThreadEnv();
auto flushOnException = [=]() -> bool {
if (!env->ExceptionCheck()) {
return false;
}
if (aFlags != FLUSH_FLAG_RECOVER) {
// First time seeing an exception; try flushing text.
env->ExceptionClear();
__android_log_print(ANDROID_LOG_WARN, "GeckoEditableSupport",
"Recovering from IME exception");
FlushIMEText(FLUSH_FLAG_RECOVER);
} else {
// Give up because we've already tried.
#ifdef RELEASE_OR_BETA
env->ExceptionClear();
#else
MOZ_CATCH_JNI_EXCEPTION(env);
#endif
}
return true;
};
// Commit the text change and selection change transaction.
mIMEPendingTextChange.Clear();
if (textTransaction.IsValid()) {
mEditable->OnTextChange(textTransaction.text, textTransaction.start,
textTransaction.oldEnd, textTransaction.newEnd,
causedOnlyByComposition);
if (flushOnException()) {
return;
}
}
while (mIMEDelaySynchronizeReply && mIMEActiveSynchronizeCount) {
mIMEActiveSynchronizeCount--;
mEditable->NotifyIME(EditableListener::NOTIFY_IME_REPLY_EVENT);
}
mIMEDelaySynchronizeReply = false;
mIMEActiveSynchronizeCount = 0;
mIMEActiveCompositionCount = 0;
if (mIMESelectionChanged) {
mIMESelectionChanged = false;
if (mDispatcher) {
// mCausedOnlyByComposition may be true on committing text.
// So even if true, there is no composition.
causedOnlyByComposition &= mDispatcher->IsComposing();
}
mEditable->OnSelectionChange(selStart, selEnd, causedOnlyByComposition);
flushOnException();
}
}
void GeckoEditableSupport::FlushIMEText(FlushChangesFlag aFlags) {
NS_WARNING_ASSERTION(
!mIMEDelaySynchronizeReply || !mIMEActiveCompositionCount,
"Cannot synchronize Java text with Gecko text");
// Notify Java of the newly focused content
mIMEPendingTextChange.Clear();
mIMESelectionChanged = true;
// Use 'INT32_MAX / 2' here because subsequent text changes might combine
// with this text change, and overflow might occur if we just use
// INT32_MAX.
IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE);
notification.mTextChangeData.mStartOffset = 0;
notification.mTextChangeData.mRemovedEndOffset = INT32_MAX / 2;
notification.mTextChangeData.mAddedEndOffset = INT32_MAX / 2;
NotifyIME(mDispatcher, notification);
FlushIMEChanges(aFlags);
}
void GeckoEditableSupport::UpdateCompositionRects() {
nsCOMPtr<nsIWidget> widget = GetWidget();
RefPtr<TextComposition> composition(GetComposition());
NS_ENSURE_TRUE_VOID(mDispatcher && widget);
jni::ObjectArray::LocalRef rects;
if (composition) {
nsEventStatus status = nsEventStatus_eIgnore;
uint32_t offset = composition->NativeOffsetOfStartComposition();
WidgetQueryContentEvent queryTextRectsEvent(true, eQueryTextRectArray,
widget);
queryTextRectsEvent.InitForQueryTextRectArray(
offset, composition->String().Length());
widget->DispatchEvent(&queryTextRectsEvent, status);
rects = ConvertRectArrayToJavaRectFArray(
queryTextRectsEvent.Succeeded()
? queryTextRectsEvent.mReply->mRectArray
: CopyableTArray<mozilla::LayoutDeviceIntRect>());
} else {
rects = ConvertRectArrayToJavaRectFArray(
CopyableTArray<mozilla::LayoutDeviceIntRect>());
}
WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, widget);
WidgetQueryContentEvent::Options options;
options.mRelativeToInsertionPoint = true;
queryCaretRectEvent.InitForQueryCaretRect(0, options);
nsEventStatus status = nsEventStatus_eIgnore;
widget->DispatchEvent(&queryCaretRectEvent, status);
auto caretRect =
queryCaretRectEvent.Succeeded()
? java::sdk::RectF::New(queryCaretRectEvent.mReply->mRect.x,
queryCaretRectEvent.mReply->mRect.y,
queryCaretRectEvent.mReply->mRect.XMost(),
queryCaretRectEvent.mReply->mRect.YMost())
: java::sdk::RectF::New();
mEditable->UpdateCompositionRects(rects, caretRect);
}
void GeckoEditableSupport::OnImeSynchronize() {
AutoGeckoEditableBlocker blocker(this);
if (mIMEDelaySynchronizeReply) {
// If we are waiting for other events to reply,
// queue this reply as well.
mIMEActiveSynchronizeCount++;
return;
}
if (!mIMEMaskEventsCount) {
FlushIMEChanges();
}
mEditable->NotifyIME(EditableListener::NOTIFY_IME_REPLY_EVENT);
}
void GeckoEditableSupport::OnImeReplaceText(int32_t aStart, int32_t aEnd,
jni::String::Param aText) {
AutoGeckoEditableBlocker blocker(this);
if (DoReplaceText(aStart, aEnd, aText)) {
mIMEDelaySynchronizeReply = true;
}
OnImeSynchronize();
}
bool GeckoEditableSupport::DoReplaceText(int32_t aStart, int32_t aEnd,
jni::String::Param aText) {
ALOGIME("IME: IME_REPLACE_TEXT: text=\"%s\"",
NS_ConvertUTF16toUTF8(aText->ToString()).get());
// Return true if processed and we should reply to the OnImeReplaceText
// event later. Return false if _not_ processed and we should reply to the
// OnImeReplaceText event now.
if (mIMEMaskEventsCount > 0) {
// Not focused; still reply to events, but don't do anything else.
return false;
}
if (nsWindow* window = GetNsWindow()) {
window->UserActivity();
}
/*
Replace text in Gecko thread from aStart to aEnd with the string text.
*/
nsCOMPtr<nsIWidget> widget = GetWidget();
NS_ENSURE_TRUE(mDispatcher && widget, false);
NS_ENSURE_SUCCESS(BeginInputTransaction(mDispatcher), false);
RefPtr<TextComposition> composition(GetComposition());
MOZ_ASSERT(!composition || !composition->IsEditorHandlingEvent());
nsString string(aText->ToString());
const bool composing = !mIMERanges->IsEmpty();
nsEventStatus status = nsEventStatus_eIgnore;
bool textChanged = composing;
// Whether deleting content before setting or committing composition text.
bool performDeletion = false;
// Dispatch composition start to set current composition.
bool needDispatchCompositionStart = false;
if (!mIMEKeyEvents.IsEmpty() || !composition || !mDispatcher->IsComposing() ||
uint32_t(aStart) != composition->NativeOffsetOfStartComposition() ||
uint32_t(aEnd) != composition->NativeOffsetOfStartComposition() +
composition->String().Length()) {
// Only start a new composition if we have key events,
// if we don't have an existing composition, or
// the replaced text does not match our composition.
textChanged |= RemoveComposition();
#ifdef NIGHTLY_BUILD
{
nsEventStatus status = nsEventStatus_eIgnore;
WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
widget);
widget->DispatchEvent(&querySelectedTextEvent, status);
if (querySelectedTextEvent.Succeeded()) {
ALOGIME(
"IME: Current selection: %s",
ToString(querySelectedTextEvent.mReply->mOffsetAndData).c_str());
}
}
#endif
// If aStart or aEnd is negative value, we use current selection instead
// of updating the selection.
if (aStart >= 0 && aEnd >= 0) {
// Use text selection to set target position(s) for
// insert, or replace, of text.
WidgetSelectionEvent event(true, eSetSelection, widget);
event.mOffset = uint32_t(aStart);
event.mLength = uint32_t(aEnd - aStart);
event.mExpandToClusterBoundary = false;
event.mReason = nsISelectionListener::IME_REASON;
widget->DispatchEvent(&event, status);
}
if (!mIMEKeyEvents.IsEmpty()) {
bool ignoreNextKeyPress = false;
for (uint32_t i = 0; i < mIMEKeyEvents.Length(); i++) {
const auto event = mIMEKeyEvents[i]->AsKeyboardEvent();
// widget for duplicated events is initially nullptr.
event->mWidget = widget;
status = nsEventStatus_eIgnore;
if (event->mMessage != eKeyPress) {
mDispatcher->DispatchKeyboardEvent(event->mMessage, *event, status);
// Skip default processing. It means that next key press shouldn't
// be dispatched.
ignoreNextKeyPress = event->mMessage == eKeyDown &&
status == nsEventStatus_eConsumeNoDefault;
} else {
if (ignoreNextKeyPress) {
// Don't dispatch key press since previous key down is consumed.
ignoreNextKeyPress = false;
continue;
}
mDispatcher->MaybeDispatchKeypressEvents(*event, status);
if (status == nsEventStatus_eConsumeNoDefault) {
textChanged = true;
}
}
if (!mDispatcher || widget->Destroyed()) {
// Don't wait for any text change event.
textChanged = false;
break;
}
}
mIMEKeyEvents.Clear();
return textChanged;
}
if (aStart != aEnd) {
if (composing) {
// Actually Gecko doesn't start composition, so it is unnecessary to
// delete content before setting composition string.
needDispatchCompositionStart = true;
} else {
// Perform a deletion first.
performDeletion = true;
}
}
} else if (composition->String().Equals(string)) {
// If the new text is the same as the existing composition text,
// the NS_COMPOSITION_CHANGE event does not generate a text
// change notification. However, the Java side still expects
// one, so we manually generate a notification.
//
// Also, since this is IME change, we have to set mCausedOnlyByComposition.
IMENotification::TextChangeData dummyChange(aStart, aEnd, aEnd, true,
false);
PostFlushIMEChanges();
mIMESelectionChanged = true;
AddIMETextChange(dummyChange);
textChanged = true;
}
SendIMEDummyKeyEvent(widget, eKeyDown);
if (!mDispatcher || widget->Destroyed()) {
return false;
}
if (needDispatchCompositionStart) {
// StartComposition sets composition string from selected string.
nsEventStatus status = nsEventStatus_eIgnore;
mDispatcher->StartComposition(status);
if (!mDispatcher || widget->Destroyed()) {
return false;
}
} else if (performDeletion) {
WidgetContentCommandEvent event(true, eContentCommandDelete, widget);
widget->DispatchEvent(&event, status);
if (!mDispatcher || widget->Destroyed()) {
return false;
}
textChanged = true;
}
if (composing) {
mDispatcher->SetPendingComposition(string, mIMERanges);
mDispatcher->FlushPendingComposition(status);
mIMEActiveCompositionCount++;
// Ensure IME ranges are empty.
mIMERanges->Clear();
} else if (!string.IsEmpty() || mDispatcher->IsComposing()) {
mDispatcher->CommitComposition(status, &string);
mIMEActiveCompositionCount++;
textChanged = true;
}
if (!mDispatcher || widget->Destroyed()) {
return false;
}
SendIMEDummyKeyEvent(widget, eKeyUp);
// Widget may be destroyed after dispatching the above event.
return textChanged;
}
void GeckoEditableSupport::OnImeAddCompositionRange(
int32_t aStart, int32_t aEnd, int32_t aRangeType, int32_t aRangeStyle,
int32_t aRangeLineStyle, bool aRangeBoldLine, int32_t aRangeForeColor,
int32_t aRangeBackColor, int32_t aRangeLineColor) {
AutoGeckoEditableBlocker blocker(this);
if (mIMEMaskEventsCount > 0) {
// Not focused.
return;
}
TextRange range;
range.mStartOffset = aStart;
range.mEndOffset = aEnd;
range.mRangeType = ToTextRangeType(aRangeType);
range.mRangeStyle.mDefinedStyles = aRangeStyle;
range.mRangeStyle.mLineStyle = TextRangeStyle::ToLineStyle(aRangeLineStyle);
range.mRangeStyle.mIsBoldLine = aRangeBoldLine;
range.mRangeStyle.mForegroundColor =
ConvertAndroidColor(uint32_t(aRangeForeColor));
range.mRangeStyle.mBackgroundColor =
ConvertAndroidColor(uint32_t(aRangeBackColor));
range.mRangeStyle.mUnderlineColor =
ConvertAndroidColor(uint32_t(aRangeLineColor));
mIMERanges->AppendElement(range);
}
void GeckoEditableSupport::OnImeUpdateComposition(int32_t aStart, int32_t aEnd,
int32_t aFlags) {
AutoGeckoEditableBlocker blocker(this);
if (DoUpdateComposition(aStart, aEnd, aFlags)) {
mIMEDelaySynchronizeReply = true;
}
}
bool GeckoEditableSupport::DoUpdateComposition(int32_t aStart, int32_t aEnd,
int32_t aFlags) {
if (mIMEMaskEventsCount > 0) {
// Not focused.
return false;
}
nsCOMPtr<nsIWidget> widget = GetWidget();
nsEventStatus status = nsEventStatus_eIgnore;
NS_ENSURE_TRUE(mDispatcher && widget, false);
const bool keepCurrent =
!!(aFlags & java::GeckoEditableChild::FLAG_KEEP_CURRENT_COMPOSITION);
// A composition with no ranges means we want to set the selection.
if (mIMERanges->IsEmpty()) {
if (keepCurrent && mDispatcher->IsComposing()) {
// Don't set selection if we want to keep current composition.
return false;
}
MOZ_ASSERT(aStart >= 0 && aEnd >= 0);
const bool compositionChanged = RemoveComposition();
WidgetSelectionEvent selEvent(true, eSetSelection, widget);
selEvent.mOffset = std::min(aStart, aEnd);
selEvent.mLength = std::max(aStart, aEnd) - selEvent.mOffset;
selEvent.mReversed = aStart > aEnd;
selEvent.mExpandToClusterBoundary = false;
widget->DispatchEvent(&selEvent, status);
return compositionChanged;
}
/**
* Update the composition from aStart to aEnd using information from added
* ranges. This is only used for visual indication and does not affect the
* text content. Only the offsets are specified and not the text content
* to eliminate the possibility of this event altering the text content
* unintentionally.
*/
nsString string;
RefPtr<TextComposition> composition(GetComposition());
MOZ_ASSERT(!composition || !composition->IsEditorHandlingEvent());
if (!composition || !mDispatcher->IsComposing() ||
uint32_t(aStart) != composition->NativeOffsetOfStartComposition() ||
uint32_t(aEnd) != composition->NativeOffsetOfStartComposition() +
composition->String().Length()) {
if (keepCurrent) {
// Don't start a new composition if we want to keep the current one.
mIMERanges->Clear();
return false;
}
// Only start new composition if we don't have an existing one,
// or if the existing composition doesn't match the new one.
RemoveComposition();
{
WidgetSelectionEvent event(true, eSetSelection, widget);
event.mOffset = uint32_t(aStart);
event.mLength = uint32_t(aEnd - aStart);
event.mExpandToClusterBoundary = false;
event.mReason = nsISelectionListener::IME_REASON;
widget->DispatchEvent(&event, status);
}
{
WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
widget);
widget->DispatchEvent(&querySelectedTextEvent, status);
MOZ_ASSERT(querySelectedTextEvent.Succeeded());
if (querySelectedTextEvent.FoundSelection()) {
string = querySelectedTextEvent.mReply->DataRef();
}
}
} else {
// If the new composition matches the existing composition,
// reuse the old composition.
string = composition->String();
}
ALOGIME("IME: IME_SET_TEXT: text=\"%s\", length=%zu, range=%zu",
NS_ConvertUTF16toUTF8(string).get(), string.Length(),
mIMERanges->Length());
if (NS_WARN_IF(NS_FAILED(BeginInputTransaction(mDispatcher)))) {
mIMERanges->Clear();
return false;
}
mDispatcher->SetPendingComposition(string, mIMERanges);
mDispatcher->FlushPendingComposition(status);
mIMEActiveCompositionCount++;
mIMERanges->Clear();
return true;
}
void GeckoEditableSupport::OnImeRequestCursorUpdates(int aRequestMode) {
AutoGeckoEditableBlocker blocker(this);
if (aRequestMode == EditableClient::ONE_SHOT) {
UpdateCompositionRects();
return;
}
mIMEMonitorCursor = (aRequestMode == EditableClient::START_MONITOR);
}
class MOZ_STACK_CLASS AutoSelectionRestore final {
public:
explicit AutoSelectionRestore(nsIWidget* widget,
TextEventDispatcher* dispatcher)
: mWidget(widget), mDispatcher(dispatcher) {
MOZ_ASSERT(widget);
if (!dispatcher || !dispatcher->IsComposing()) {
mOffset = UINT32_MAX;
mLength = UINT32_MAX;
return;
}
WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
widget);
nsEventStatus status = nsEventStatus_eIgnore;
widget->DispatchEvent(&querySelectedTextEvent, status);
if (querySelectedTextEvent.DidNotFindSelection()) {
mOffset = UINT32_MAX;
mLength = UINT32_MAX;
return;
}
mOffset = querySelectedTextEvent.mReply->StartOffset();
mLength = querySelectedTextEvent.mReply->DataLength();
}
~AutoSelectionRestore() {
if (mWidget->Destroyed() || mOffset == UINT32_MAX) {
return;
}
WidgetSelectionEvent selection(true, eSetSelection, mWidget);
selection.mOffset = mOffset;
selection.mLength = mLength;
selection.mExpandToClusterBoundary = false;
selection.mReason = nsISelectionListener::IME_REASON;
nsEventStatus status = nsEventStatus_eIgnore;
mWidget->DispatchEvent(&selection, status);
}
private:
nsCOMPtr<nsIWidget> mWidget;
RefPtr<TextEventDispatcher> mDispatcher;
uint32_t mOffset;
uint32_t mLength;
};
void GeckoEditableSupport::OnImeRequestCommit() {
AutoGeckoEditableBlocker blocker(this);
if (mIMEMaskEventsCount > 0) {
// Not focused.
return;
}
nsCOMPtr<nsIWidget> widget = GetWidget();
if (NS_WARN_IF(!widget)) {
return;
}
AutoSelectionRestore restore(widget, mDispatcher);
RemoveComposition(COMMIT_IME_COMPOSITION);
}
void GeckoEditableSupport::AsyncNotifyIME(int32_t aNotification) {
RefPtr<GeckoEditableSupport> self(this);
nsAppShell::PostEvent([this, self, aNotification] {
if (!mIMEMaskEventsCount) {
mEditable->NotifyIME(aNotification);
}
});
}
nsresult GeckoEditableSupport::NotifyIME(
TextEventDispatcher* aTextEventDispatcher,
const IMENotification& aNotification) {
MOZ_ASSERT(mEditable);
switch (aNotification.mMessage) {
case REQUEST_TO_COMMIT_COMPOSITION: {
ALOGIME("IME: REQUEST_TO_COMMIT_COMPOSITION");
RemoveComposition(COMMIT_IME_COMPOSITION);
AsyncNotifyIME(EditableListener::NOTIFY_IME_TO_COMMIT_COMPOSITION);
break;
}
case REQUEST_TO_CANCEL_COMPOSITION: {
ALOGIME("IME: REQUEST_TO_CANCEL_COMPOSITION");
RemoveComposition(CANCEL_IME_COMPOSITION);
AsyncNotifyIME(EditableListener::NOTIFY_IME_TO_CANCEL_COMPOSITION);
break;
}
case NOTIFY_IME_OF_FOCUS: {
ALOGIME("IME: NOTIFY_IME_OF_FOCUS");
mIMEFocusCount++;
RefPtr<GeckoEditableSupport> self(this);
RefPtr<TextEventDispatcher> dispatcher = aTextEventDispatcher;
// Post an event because we have to flush the text before sending a
// focus event, and we may not be able to flush text during the
// NotifyIME call.
nsAppShell::PostEvent([this, self, dispatcher] {
nsCOMPtr<nsIWidget> widget = dispatcher->GetWidget();
--mIMEMaskEventsCount;
if (!mIMEFocusCount || !widget || widget->Destroyed()) {
return;
}
mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_TOKEN);
if (mIsRemote) {
if (!mEditableAttached) {
// Re-attach on focus; see OnRemovedFrom().
jni::NativeWeakPtrHolder<GeckoEditableSupport>::AttachExisting(
mEditable, do_AddRef(this));
mEditableAttached = true;
}
// Because GeckoEditableSupport in content process doesn't
// manage the active input context, we need to retrieve the
// input context from the widget, for use by
// OnImeReplaceText.
mInputContext = widget->GetInputContext();
}
mDispatcher = dispatcher;
mIMEKeyEvents.Clear();
mCachedSelection.Reset();
mIMEDelaySynchronizeReply = false;
mIMEActiveCompositionCount = 0;
FlushIMEText();
// IME will call requestCursorUpdates after getting context.
// So reset cursor update mode before getting context.
mIMEMonitorCursor = false;
mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_FOCUS);
});
break;
}
case NOTIFY_IME_OF_BLUR: {
ALOGIME("IME: NOTIFY_IME_OF_BLUR");
mIMEFocusCount--;
MOZ_ASSERT(mIMEFocusCount >= 0);
RefPtr<GeckoEditableSupport> self(this);
nsAppShell::PostEvent([this, self] {
if (!mIMEFocusCount) {
mIMEDelaySynchronizeReply = false;
mIMEActiveSynchronizeCount = 0;
mIMEActiveCompositionCount = 0;
mInputContext.ShutDown();
mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_BLUR);
OnRemovedFrom(mDispatcher);
}
});
// Mask events because we lost focus. Unmask on the next focus.
mIMEMaskEventsCount++;
break;
}
case NOTIFY_IME_OF_SELECTION_CHANGE: {
ALOGIME("IME: NOTIFY_IME_OF_SELECTION_CHANGE: SelectionChangeData=%s",
ToString(aNotification.mSelectionChangeData).c_str());
if (aNotification.mSelectionChangeData.HasRange()) {
mCachedSelection.mStartOffset = static_cast<int32_t>(
aNotification.mSelectionChangeData.AnchorOffset());
mCachedSelection.mEndOffset = static_cast<int32_t>(
aNotification.mSelectionChangeData.FocusOffset());
} else {
mCachedSelection.Reset();
}
PostFlushIMEChanges();
mIMESelectionChanged = true;
break;
}
case NOTIFY_IME_OF_TEXT_CHANGE: {
ALOGIME("IME: NOTIFY_IME_OF_TEXT_CHANGE: TextChangeData=%s",
ToString(aNotification.mTextChangeData).c_str());
/* Make sure Java's selection is up-to-date */
PostFlushIMEChanges();
mIMESelectionChanged = true;
AddIMETextChange(aNotification.mTextChangeData);
break;
}
case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: {
ALOGIME("IME: NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED");
// NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED isn't sent per IME call.
// Receiving this event means that Gecko has already handled all IME
// composing events in queue.
//
if (mIsRemote) {
OnNotifyIMEOfCompositionEventHandled();
} else {
// Also, when receiving this event, mIMEDelaySynchronizeReply won't
// update yet on non-e10s case since IME event is posted before updating
// it. So we have to delay handling of this event.
RefPtr<GeckoEditableSupport> self(this);
nsAppShell::PostEvent(
[this, self] { OnNotifyIMEOfCompositionEventHandled(); });
}
break;
}
default:
break;
}
return NS_OK;
}
void GeckoEditableSupport::OnNotifyIMEOfCompositionEventHandled() {
// NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED may be merged with multiple events,
// so reset count.
mIMEActiveCompositionCount = 0;
if (mIMEDelaySynchronizeReply) {
FlushIMEChanges();
}
// Hardware keyboard support requires each string rect.
if (mIMEMonitorCursor) {
UpdateCompositionRects();
}
}
void GeckoEditableSupport::OnRemovedFrom(
TextEventDispatcher* aTextEventDispatcher) {
mDispatcher = nullptr;
if (mIsRemote && mEditable->HasEditableParent()) {
// When we're remote, detach every time.
OnWeakNonIntrusiveDetach(NS_NewRunnableFunction(
"GeckoEditableSupport::OnRemovedFrom",
[editable = java::GeckoEditableChild::GlobalRef(mEditable)] {
DisposeNative(editable);
}));
}
}
void GeckoEditableSupport::WillDispatchKeyboardEvent(
TextEventDispatcher* aTextEventDispatcher,
WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
void* aData) {}
NS_IMETHODIMP_(IMENotificationRequests)
GeckoEditableSupport::GetIMENotificationRequests() {
return IMENotificationRequests(IMENotificationRequests::NOTIFY_TEXT_CHANGE);
}
static bool ShouldKeyboardDismiss(const nsAString& aInputType,
const nsAString& aInputMode) {
// Some input type uses the prompt to input value. So it is unnecessary to
// show software keyboard.
return aInputMode.EqualsLiteral("none") || aInputType.EqualsLiteral("date") ||
aInputType.EqualsLiteral("time") ||
aInputType.EqualsLiteral("month") ||
aInputType.EqualsLiteral("week") ||
aInputType.EqualsLiteral("datetime-local");
}
void GeckoEditableSupport::SetInputContext(const InputContext& aContext,
const InputContextAction& aAction) {
// SetInputContext is called from chrome process only
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(mEditable);
ALOGIME(
"IME: SetInputContext: aContext=%s, "
"aAction={mCause=%s, mFocusChange=%s}",
ToString(aContext).c_str(), ToString(aAction.mCause).c_str(),
ToString(aAction.mFocusChange).c_str());
mInputContext = aContext;
if (mInputContext.mIMEState.mEnabled != IMEEnabled::Disabled &&
!ShouldKeyboardDismiss(mInputContext.mHTMLInputType,
mInputContext.mHTMLInputMode) &&
aAction.UserMightRequestOpenVKB()) {
// Don't reset keyboard when we should simply open the vkb
mEditable->NotifyIME(EditableListener::NOTIFY_IME_OPEN_VKB);
return;
}
// Post an event to keep calls in order relative to NotifyIME.
nsAppShell::PostEvent([this, self = RefPtr<GeckoEditableSupport>(this),
context = mInputContext, action = aAction] {
nsCOMPtr<nsIWidget> widget = GetWidget();
if (!widget || widget->Destroyed()) {
return;
}
NotifyIMEContext(context, action);
});
}
void GeckoEditableSupport::NotifyIMEContext(const InputContext& aContext,
const InputContextAction& aAction) {
const bool inPrivateBrowsing = aContext.mInPrivateBrowsing;
// isUserAction is used whether opening virtual keyboard. But long press
// shouldn't open it.
const bool isUserAction =
aAction.mCause != InputContextAction::CAUSE_LONGPRESS &&
!(aAction.mCause == InputContextAction::CAUSE_UNKNOWN_CHROME &&
aContext.mIMEState.mEnabled == IMEEnabled::Enabled) &&
(aAction.IsHandlingUserInput() || aContext.mHasHandledUserInput);
const int32_t flags =
(inPrivateBrowsing ? EditableListener::IME_FLAG_PRIVATE_BROWSING : 0) |
(isUserAction ? EditableListener::IME_FLAG_USER_ACTION : 0) |
(aAction.mFocusChange == InputContextAction::FOCUS_NOT_CHANGED
? EditableListener::IME_FOCUS_NOT_CHANGED
: 0);
mEditable->NotifyIMEContext(static_cast<int32_t>(aContext.mIMEState.mEnabled),
aContext.mHTMLInputType, aContext.mHTMLInputMode,
aContext.mActionHint, aContext.mAutocapitalize,
flags);
}
InputContext GeckoEditableSupport::GetInputContext() {
// GetInputContext is called from chrome process only
MOZ_ASSERT(XRE_IsParentProcess());
InputContext context = mInputContext;
context.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
return context;
}
void GeckoEditableSupport::TransferParent(jni::Object::Param aEditableParent) {
AutoGeckoEditableBlocker blocker(this);
mEditable->SetParent(aEditableParent);
// If we are already focused, make sure the new parent has our token
// and focus information, so it can accept additional calls from us.
if (mIMEFocusCount > 0) {
mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_TOKEN);
if (mIsRemote) {
// GeckoEditableSupport::SetInputContext is called on chrome process
// only, so mInputContext may be still invalid since it is set after
// we have gotton focus.
RefPtr<GeckoEditableSupport> self(this);
nsAppShell::PostEvent([self = std::move(self)] {
NS_WARNING_ASSERTION(
self->mDispatcher,
"Text dispatcher is still null. Why don't we get focus yet?");
self->NotifyIMEContext(self->mInputContext, InputContextAction());
});
} else {
NotifyIMEContext(mInputContext, InputContextAction());
}
mEditable->NotifyIME(EditableListener::NOTIFY_IME_OF_FOCUS);
// We have focus, so don't destroy editable child.
return;
}
if (mIsRemote && !mDispatcher) {
// Detach now if we were only attached temporarily.
OnRemovedFrom(/* dispatcher */ nullptr);
}
}
void GeckoEditableSupport::SetOnBrowserChild(dom::BrowserChild* aBrowserChild) {
MOZ_ASSERT(!XRE_IsParentProcess());
NS_ENSURE_TRUE_VOID(aBrowserChild);
const dom::ContentChild* const contentChild =
dom::ContentChild::GetSingleton();
RefPtr<widget::PuppetWidget> widget(aBrowserChild->WebWidget());
NS_ENSURE_TRUE_VOID(contentChild && widget);
// Get the content/tab ID in order to get the correct
// IGeckoEditableParent object, which GeckoEditableChild uses to
// communicate with the parent process.
const uint64_t contentId = contentChild->GetID();
const uint64_t tabId = aBrowserChild->GetTabId();
NS_ENSURE_TRUE_VOID(contentId && tabId);
RefPtr<widget::TextEventDispatcherListener> listener =
widget->GetNativeTextEventDispatcherListener();
if (!listener ||
listener.get() ==
static_cast<widget::TextEventDispatcherListener*>(widget)) {
// We need to set a new listener.
const auto editableChild = java::GeckoEditableChild::New(
/* parent */ nullptr, /* default */ false);
// Temporarily attach so we can receive the initial editable parent.
auto editableSupport =
jni::NativeWeakPtrHolder<GeckoEditableSupport>::Attach(editableChild,
editableChild);
auto accEditableSupport(editableSupport.Access());
MOZ_RELEASE_ASSERT(accEditableSupport);
// Tell PuppetWidget to use our listener for IME operations.
widget->SetNativeTextEventDispatcherListener(
accEditableSupport.AsRefPtr().get());
accEditableSupport->mEditableAttached = true;
// Connect the new child to a parent that corresponds to the BrowserChild.
java::GeckoServiceChildProcess::GetEditableParent(editableChild, contentId,
tabId);
return;
}
// We need to update the existing listener to use the new parent.
// We expect the existing TextEventDispatcherListener to be a
// GeckoEditableSupport object, so we perform a sanity check to make
// sure, by comparing their respective vtable pointers.
const RefPtr<widget::GeckoEditableSupport> dummy =
new widget::GeckoEditableSupport(/* child */ nullptr);
NS_ENSURE_TRUE_VOID(*reinterpret_cast<const uintptr_t*>(listener.get()) ==
*reinterpret_cast<const uintptr_t*>(dummy.get()));
const auto support =
static_cast<widget::GeckoEditableSupport*>(listener.get());
if (!support->mEditableAttached) {
// Temporarily attach so we can receive the initial editable parent.
jni::NativeWeakPtrHolder<GeckoEditableSupport>::AttachExisting(
support->GetJavaEditable(), do_AddRef(support));
support->mEditableAttached = true;
}
// Transfer to a new parent that corresponds to the BrowserChild.
java::GeckoServiceChildProcess::GetEditableParent(support->GetJavaEditable(),
contentId, tabId);
}
nsIWidget* GeckoEditableSupport::GetWidget() const {
MOZ_ASSERT(NS_IsMainThread());
return mDispatcher ? mDispatcher->GetWidget() : GetNsWindow();
}
nsWindow* GeckoEditableSupport::GetNsWindow() const {
MOZ_ASSERT(NS_IsMainThread());
auto acc(mWindow.Access());
if (!acc) {
return nullptr;
}
return acc->GetNsWindow();
}
void GeckoEditableSupport::OnImeInsertImage(jni::ByteArray::Param aData,
jni::String::Param aMimeType) {
AutoGeckoEditableBlocker blocker(this);
nsCString mimeType(aMimeType->ToCString());
nsCOMPtr<nsIInputStream> byteStream;
nsresult rv = NS_NewByteInputStream(
getter_AddRefs(byteStream),
mozilla::Span(
reinterpret_cast<const char*>(aData->GetElements().Elements()),
aData->Length()),
NS_ASSIGNMENT_COPY);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
nsCOMPtr<nsITransferable> trans =
do_CreateInstance("@mozilla.org/widget/transferable;1");
if (NS_WARN_IF(!trans)) {
return;
}
trans->Init(nullptr);
rv = trans->SetTransferData(mimeType.get(), byteStream);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
nsCOMPtr<nsIWidget> widget = GetWidget();
if (NS_WARN_IF(!widget) || NS_WARN_IF(widget->Destroyed())) {
return;
}
WidgetContentCommandEvent command(true, eContentCommandPasteTransferable,
widget);
command.mTransferable = trans.forget();
nsEventStatus status;
widget->DispatchEvent(&command, status);
}
} // namespace widget
} // namespace mozilla