/* -*- 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 #include #include #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& aRects) { const size_t length = aRects.Length(); auto rects = jni::ObjectArray::New(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 mGeckoEditable; }; RefPtr GeckoEditableSupport::GetComposition() const { nsCOMPtr 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 widget = GetWidget(); RefPtr 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(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(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 self(this); nsAppShell::PostEvent([this, self] { nsCOMPtr 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 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(mIMEPendingTextChange.mStartOffset); textTransaction.oldEnd = static_cast(mIMEPendingTextChange.mRemovedEndOffset); textTransaction.newEnd = static_cast(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(querySelectedTextEvent.mReply->AnchorOffset()); selEnd = static_cast(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 widget = GetWidget(); RefPtr 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()); } else { rects = ConvertRectArrayToJavaRectFArray( CopyableTArray()); } 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 widget = GetWidget(); NS_ENSURE_TRUE(mDispatcher && widget, false); NS_ENSURE_SUCCESS(BeginInputTransaction(mDispatcher), false); RefPtr 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 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 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 mWidget; RefPtr mDispatcher; uint32_t mOffset; uint32_t mLength; }; void GeckoEditableSupport::OnImeRequestCommit() { AutoGeckoEditableBlocker blocker(this); if (mIMEMaskEventsCount > 0) { // Not focused. return; } nsCOMPtr widget = GetWidget(); if (NS_WARN_IF(!widget)) { return; } AutoSelectionRestore restore(widget, mDispatcher); RemoveComposition(COMMIT_IME_COMPOSITION); } void GeckoEditableSupport::AsyncNotifyIME(int32_t aNotification) { RefPtr 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 self(this); RefPtr 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 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::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 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( aNotification.mSelectionChangeData.AnchorOffset()); mCachedSelection.mEndOffset = static_cast( 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 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(this), context = mInputContext, action = aAction] { nsCOMPtr 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(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 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(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 listener = widget->GetNativeTextEventDispatcherListener(); if (!listener || listener.get() == static_cast(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::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 dummy = new widget::GeckoEditableSupport(/* child */ nullptr); NS_ENSURE_TRUE_VOID(*reinterpret_cast(listener.get()) == *reinterpret_cast(dummy.get())); const auto support = static_cast(listener.get()); if (!support->mEditableAttached) { // Temporarily attach so we can receive the initial editable parent. jni::NativeWeakPtrHolder::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 byteStream; nsresult rv = NS_NewByteInputStream( getter_AddRefs(byteStream), mozilla::Span( reinterpret_cast(aData->GetElements().Elements()), aData->Length()), NS_ASSIGNMENT_COPY); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr 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 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