From 22ab980e4c2f8a8643677e3e4c80a475a1da60d7 Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Fri, 9 Mar 2018 00:46:52 +0900 Subject: [PATCH] Bug 1443421 - part 1: Make IMContextWrapper not dispatch eKeyDown and eKeyUp event if the native key event is being handled by other IME process r=m_kato ibus and fcitx have asynchronous key event handling mode and it's enabled in default settings. That is, when they receive a key event from application via a call of gtk_im_context_filter_keypress(), they may post the key event information to other IME process, then does nothing but store the copy of the event with gdk_event_copy() and returns true for the result of gtk_im_context_filter_keypress(). When the other IME process handles the event, returns the result to them in our process. Then, they send the stored key event to us again. Finally, they actually handles the event in our process actually. Therefore, we may receive every key event twice. So, this causes dispatching eKeyDown event and eKeyUp event twice. Preceding key event is always marked as "processed by IME" since gtk_im_context_filter_keypress() returns true temporarily and following key event is dispatched as expected. So, we need to ignore the first event only when gtk_im_context_filter_keypress() returns true but the event is posted to different process. Unfortunately, we cannot know if the key event is actually posted to different process directly. However, we can know if active IM is ibus, fcitx or another one and if ibus or fcitx is in asynchronous key handling mode. The former information is provided by gtk_im_multicontext_get_context_id(). It returns a string which is set to the IM multicontext instance by creator. We'll get "ibus" if IM is ibus, get "fcitx" if IM is fcitx. The latter information is not provided. However, they consider the mode from env value. ibus checks IBUS_ENABLE_SYNC_MODE. fcitx checks both IBUS_ENABLE_SYNC_MODE and FCITX_ENABLE_SYNC_MODE. Additionally, we can know if received key event has already been posted to other IME process. They use undefined bit of GdkEventKey::state to store if the key event has already been posted (1 << 25, they called "ignored" flag). Although their approach is really hacky but we can refer the information at least for now. Finally, when we guess a key event is posted to other IME process, let's IMContextWrapper::OnKeyEvent() not dispatch eKeyDown nor eKeyUp event. Note that if it's handled synchronously as unexpected, it may causes dispatching one or more composition events and/or delete content event. So, in such case, we dispatch a keyboard event for processing key event anyway. There is only once case we'll fail to dispatch keyboard event. If we receive signals to dispatch composition events or delete content command event when IM receives the result from other IME process but it doesn't send the key event to us. This will be fixed by the following patch. MozReview-Commit-ID: 94PrlnmQ3uJ --HG-- extra : rebase_source : fc31b0293ff0f0688dd39b0094fdf8f98b6c64d3 --- widget/gtk/IMContextWrapper.cpp | 261 +++++++++++++++++++++++++++++--- widget/gtk/IMContextWrapper.h | 42 ++++- widget/gtk/mozgtk/mozgtk.c | 1 + 3 files changed, 286 insertions(+), 18 deletions(-) diff --git a/widget/gtk/IMContextWrapper.cpp b/widget/gtk/IMContextWrapper.cpp index 69a8e49d7472..fe478f849efe 100644 --- a/widget/gtk/IMContextWrapper.cpp +++ b/widget/gtk/IMContextWrapper.cpp @@ -59,6 +59,73 @@ GetEventType(GdkEventKey* aKeyEvent) } } +class GetEventStateName : public nsAutoCString +{ +public: + explicit GetEventStateName(guint aState, + IMContextWrapper::IMContextID aIMContextID = + IMContextWrapper::IMContextID::eUnknown) + { + if (aState & GDK_SHIFT_MASK) { + AppendModifier("shift"); + } + if (aState & GDK_CONTROL_MASK) { + AppendModifier("control"); + } + if (aState & GDK_MOD1_MASK) { + AppendModifier("mod1"); + } + if (aState & GDK_MOD2_MASK) { + AppendModifier("mod2"); + } + if (aState & GDK_MOD3_MASK) { + AppendModifier("mod3"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod4"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod5"); + } + if (aState & GDK_MOD4_MASK) { + AppendModifier("mod5"); + } + switch (aIMContextID) { + case IMContextWrapper::IMContextID::eIBus: + static const guint IBUS_HANDLED_MASK = 1 << 24; + static const guint IBUS_IGNORED_MASK = 1 << 25; + if (aState & IBUS_HANDLED_MASK) { + AppendModifier("IBUS_HANDLED_MASK"); + } + if (aState & IBUS_IGNORED_MASK) { + AppendModifier("IBUS_IGNORED_MASK"); + } + break; + case IMContextWrapper::IMContextID::eFcitx: + static const guint FcitxKeyState_HandledMask = 1 << 24; + static const guint FcitxKeyState_IgnoredMask = 1 << 25; + if (aState & FcitxKeyState_HandledMask) { + AppendModifier("FcitxKeyState_HandledMask"); + } + if (aState & FcitxKeyState_IgnoredMask) { + AppendModifier("FcitxKeyState_IgnoredMask"); + } + break; + default: + break; + } + } + +private: + void AppendModifier(const char* aModifierName) + { + if (!IsEmpty()) { + AppendLiteral(" + "); + } + Append(aModifierName); + } +}; + class GetWritingModeName : public nsAutoCString { public: @@ -174,6 +241,7 @@ IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow) , mCompositionStart(UINT32_MAX) , mProcessingKeyEvent(nullptr) , mCompositionState(eCompositionState_NotComposing) + , mIMContextID(IMContextID::eUnknown) , mIsIMFocused(false) , mFallbackToKeyEvent(false) , mKeyboardEventWasDispatched(false) @@ -183,6 +251,7 @@ IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow) , mPendingResettingIMContext(false) , mRetrieveSurroundingSignalReceived(false) , mMaybeInDeadKeySequence(false) + , mIsIMInAsyncKeyHandlingMode(false) { static bool sFirstInstance = true; if (sFirstInstance) { @@ -195,13 +264,59 @@ IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow) Init(); } +static bool +IsIBusInSyncMode() +{ + // See ibus_im_context_class_init() in client/gtk2/ibusimcontext.c + // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L610 + const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE"); + + // See _get_boolean_env() in client/gtk2/ibusimcontext.c + // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L520-L537 + if (!env) { + return false; + } + nsDependentCString envStr(env); + if (envStr.IsEmpty() || + envStr.EqualsLiteral("0") || + envStr.EqualsLiteral("false") || + envStr.EqualsLiteral("False") || + envStr.EqualsLiteral("FALSE")) { + return false; + } + return true; +} + +static bool +GetFcitxBoolEnv(const char* aEnv) +{ + // See fcitx_utils_get_boolean_env in src/lib/fcitx-utils/utils.c + // https://github.com/fcitx/fcitx/blob/0c87840dc7d9460c2cb5feaeefec299d0d3d62ec/src/lib/fcitx-utils/utils.c#L721-L736 + const char* env = PR_GetEnv(aEnv); + if (!env) { + return false; + } + nsDependentCString envStr(env); + if (envStr.IsEmpty() || + envStr.EqualsLiteral("0") || + envStr.EqualsLiteral("false")) { + return false; + } + return true; +} + +static bool +IsFcitxInSyncMode() +{ + // See fcitx_im_context_class_init() in src/frontend/gtk2/fcitximcontext.c + // https://github.com/fcitx/fcitx/blob/78b98d9230dc9630e99d52e3172bdf440ffd08c4/src/frontend/gtk2/fcitximcontext.c#L395-L398 + return GetFcitxBoolEnv("IBUS_ENABLE_SYNC_MODE") || + GetFcitxBoolEnv("FCITX_ENABLE_SYNC_MODE"); +} + void IMContextWrapper::Init() { - MOZ_LOG(gGtkIMLog, LogLevel::Info, - ("0x%p Init(), mOwnerWindow=0x%p", - this, mOwnerWindow)); - MozContainer* container = mOwnerWindow->GetMozContainer(); NS_PRECONDITION(container, "container is null"); GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container)); @@ -224,6 +339,31 @@ IMContextWrapper::Init() G_CALLBACK(IMContextWrapper::OnStartCompositionCallback), this); g_signal_connect(mContext, "preedit_end", G_CALLBACK(IMContextWrapper::OnEndCompositionCallback), this); + nsDependentCString contextID; + const char* contextIDChar = + gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)); + if (contextIDChar) { + contextID.Rebind(contextIDChar); + } + if (contextID.EqualsLiteral("ibus")) { + mIMContextID = IMContextID::eIBus; + mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode(); + } else if (contextID.EqualsLiteral("fcitx")) { + mIMContextID = IMContextID::eFcitx; + mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode(); + } else if (contextID.EqualsLiteral("uim")) { + mIMContextID = IMContextID::eUim; + mIsIMInAsyncKeyHandlingMode = false; + } else if (contextID.EqualsLiteral("scim")) { + mIMContextID = IMContextID::eScim; + mIsIMInAsyncKeyHandlingMode = false; + } else if (contextID.EqualsLiteral("iiim")) { + mIMContextID = IMContextID::eIIIMF; + mIsIMInAsyncKeyHandlingMode = false; + } else { + mIMContextID = IMContextID::eUnknown; + mIsIMInAsyncKeyHandlingMode = false; + } // Simple context if (sUseSimpleContext) { @@ -252,6 +392,13 @@ IMContextWrapper::Init() // Dummy context mDummyContext = gtk_im_multicontext_new(); gtk_im_context_set_client_window(mDummyContext, gdkWindow); + + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p Init(), mOwnerWindow=%p, mContext=%p (%s), " + "mIsIMInAsyncKeyHandlingMode=%s, mSimpleContext=%p, " + "mDummyContext=%p", + this, mOwnerWindow, mContext, contextID.get(), + ToChar(mIsIMInAsyncKeyHandlingMode), mSimpleContext, mDummyContext)); } IMContextWrapper::~IMContextWrapper() @@ -498,16 +645,23 @@ IMContextWrapper::OnKeyEvent(nsWindow* aCaller, MOZ_LOG(gGtkIMLog, LogLevel::Info, ("0x%p OnKeyEvent(aCaller=0x%p, " - "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X }, " - "aKeyboardEventWasDispatched=%s), " - "mMaybeInDeadKeySequence=%s, " - "mCompositionState=%s, current context=0x%p, active context=0x%p, ", + "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X, state=%s, " + "time=%u, hardware_keycode=%u, group=%u }, " + "aKeyboardEventWasDispatched=%s)", this, aCaller, aEvent, GetEventType(aEvent), gdk_keyval_name(aEvent->keyval), gdk_keyval_to_unicode(aEvent->keyval), - ToChar(aKeyboardEventWasDispatched), - ToChar(mMaybeInDeadKeySequence), - GetCompositionStateName(), GetCurrentContext(), GetActiveContext())); + GetEventStateName(aEvent->state, mIMContextID).get(), + aEvent->time, aEvent->hardware_keycode, aEvent->group, + ToChar(aKeyboardEventWasDispatched))); + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), mMaybeInDeadKeySequence=%s, " + "mCompositionState=%s, current context=%p, active context=%p, " + "mIMContextID=%s, mIsIMInAsyncKeyHandlingMode=%s", + this, ToChar(mMaybeInDeadKeySequence), + GetCompositionStateName(), GetCurrentContext(), GetActiveContext(), + GetIMContextIDName(mIMContextID), + ToChar(mIsIMInAsyncKeyHandlingMode))); if (aCaller != mLastFocusedWindow) { MOZ_LOG(gGtkIMLog, LogLevel::Error, @@ -538,6 +692,78 @@ IMContextWrapper::OnKeyEvent(nsWindow* aCaller, KeymapWrapper::ComputeDOMKeyNameIndex(aEvent) == KEY_NAME_INDEX_Dead; mMaybeInDeadKeySequence |= isDeadKey; + // If current context is mSimpleContext, both ibus and fcitx handles key + // events synchronously. So, only when current context is mContext which + // is GtkIMMulticontext, the key event may be handled by IME asynchronously. + bool maybeHandledAsynchronously = + mIsIMInAsyncKeyHandlingMode && currentContext == mContext; + + // If IM is ibus or fcitx and it handles key events asynchronously, + // they mark aEvent->state as "handled by me" when they post key event + // to another process. Unfortunately, we need to check this hacky + // flag because it's difficult to store all pending key events by + // an array or a hashtable. + if (maybeHandledAsynchronously) { + switch (mIMContextID) { + case IMContextID::eIBus: + // ibus won't send back key press events in a dead key sequcne. + if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) { + maybeHandledAsynchronously = false; + break; + } + // ibus handles key events synchronously if focused editor is + // or |ime-mode: disabled;|. + if (mInputContext.mIMEState.mEnabled == IMEState::PASSWORD) { + maybeHandledAsynchronously = false; + break; + } + // See src/ibustypes.h + static const guint IBUS_IGNORED_MASK = 1 << 25; + // If IBUS_IGNORED_MASK was set to aEvent->state, the event + // has already been handled by another process and it wasn't + // used by IME. + if (aEvent->state & IBUS_IGNORED_MASK) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state has " + "IBUS_IGNORED_MASK, so, it won't be handled " + "asynchronously anymore", + this)); + maybeHandledAsynchronously = false; + break; + } + break; + case IMContextID::eFcitx: + // fcitx won't send back key press events in a dead key sequcne. + if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) { + maybeHandledAsynchronously = false; + break; + } + + // fcitx handles key events asynchronously even if focused + // editor cannot use IME actually. + + // See src/lib/fcitx-utils/keysym.h + static const guint FcitxKeyState_IgnoredMask = 1 << 25; + // If FcitxKeyState_IgnoredMask was set to aEvent->state, + // the event has already been handled by another process and + // it wasn't used by IME. + if (aEvent->state & FcitxKeyState_IgnoredMask) { + MOZ_LOG(gGtkIMLog, LogLevel::Info, + ("0x%p OnKeyEvent(), aEvent->state has " + "FcitxKeyState_IgnoredMask, so, it won't be handled " + "asynchronously anymore", + this)); + maybeHandledAsynchronously = false; + break; + } + break; + default: + MOZ_ASSERT_UNREACHABLE("IME may handle key event " + "asyncrhonously, but not yet confirmed if it comes agian " + "actually"); + } + } + mKeyboardEventWasDispatched = aKeyboardEventWasDispatched; mFallbackToKeyEvent = false; mProcessingKeyEvent = aEvent; @@ -569,9 +795,9 @@ IMContextWrapper::OnKeyEvent(nsWindow* aCaller, } // If IME handled the key event but we've not dispatched eKeyDown nor - // eKeyUp event yet, we need to dispatch here because the caller won't - // do it. - if (filterThisEvent) { + // eKeyUp event yet, we need to dispatch here unless the key event is + // now being handled by other IME process. + if (filterThisEvent && !maybeHandledAsynchronously) { MaybeDispatchKeyEventAsProcessedByIME(); // Be aware, the widget might have been gone here. } @@ -589,11 +815,12 @@ IMContextWrapper::OnKeyEvent(nsWindow* aCaller, MOZ_LOG(gGtkIMLog, LogLevel::Debug, ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s " - "(isFiltered=%s, mFallbackToKeyEvent=%s), mCompositionState=%s, " + "(isFiltered=%s, mFallbackToKeyEvent=%s, " + "maybeHandledAsynchronously=%s), mCompositionState=%s, " "mMaybeInDeadKeySequence=%s", this, ToChar(filterThisEvent), ToChar(isFiltered), - ToChar(mFallbackToKeyEvent), GetCompositionStateName(), - ToChar(mMaybeInDeadKeySequence))); + ToChar(mFallbackToKeyEvent), ToChar(maybeHandledAsynchronously), + GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence))); return filterThisEvent; } diff --git a/widget/gtk/IMContextWrapper.h b/widget/gtk/IMContextWrapper.h index bf536db5f276..55adf7908636 100644 --- a/widget/gtk/IMContextWrapper.h +++ b/widget/gtk/IMContextWrapper.h @@ -97,6 +97,37 @@ public: TextEventDispatcher* GetTextEventDispatcher(); + // TODO: Typically, new IM comes every several years. And now, our code + // becomes really IM behavior dependent. So, perhaps, we need prefs + // to control related flags for IM developers. + enum class IMContextID : uint8_t + { + eFcitx, + eIBus, + eIIIMF, + eScim, + eUim, + eUnknown, + }; + + static const char* GetIMContextIDName(IMContextID aIMContextID) + { + switch (aIMContextID) { + case IMContextID::eFcitx: + return "eFcitx"; + case IMContextID::eIBus: + return "eIBus"; + case IMContextID::eIIIMF: + return "eIIIMF"; + case IMContextID::eScim: + return "eScim"; + case IMContextID::eUim: + return "eUim"; + default: + return "eUnknown"; + } + } + protected: ~IMContextWrapper(); @@ -175,7 +206,8 @@ protected: Range mCompositionTargetRange; // mCompositionState indicates current status of composition. - enum eCompositionState { + enum eCompositionState : uint8_t + { eCompositionState_NotComposing, eCompositionState_CompositionStartDispatched, eCompositionState_CompositionChangeEventDispatched @@ -227,6 +259,10 @@ protected: } } + // mIMContextID indicates the ID of mContext. This is actually indicates + // IM which user selected. + IMContextID mIMContextID; + struct Selection final { nsString mString; @@ -321,6 +357,10 @@ protected: // pressing "Shift" key causes exactly same behavior but dead key sequence // isn't finished yet. bool mMaybeInDeadKeySequence; + // mIsIMInAsyncKeyHandlingMode is set to true if we know that IM handles + // key events asynchronously. I.e., filtered key event may come again + // later. + bool mIsIMInAsyncKeyHandlingMode; // sLastFocusedContext is a pointer to the last focused instance of this // class. When a instance is destroyed and sLastFocusedContext refers it, diff --git a/widget/gtk/mozgtk/mozgtk.c b/widget/gtk/mozgtk/mozgtk.c index 8ed3d7eae82c..b4a89c86a1fe 100644 --- a/widget/gtk/mozgtk/mozgtk.c +++ b/widget/gtk/mozgtk/mozgtk.c @@ -266,6 +266,7 @@ STUB(gtk_im_context_set_client_window) STUB(gtk_im_context_set_cursor_location) STUB(gtk_im_context_set_surrounding) STUB(gtk_im_context_simple_new) +STUB(gtk_im_multicontext_get_context_id) STUB(gtk_im_multicontext_get_type) STUB(gtk_im_multicontext_new) STUB(gtk_info_bar_get_type)