--- a/widget/gtk/IMContextWrapper.cpp
+++ b/widget/gtk/IMContextWrapper.cpp
@@ -54,16 +54,83 @@ GetEventType(GdkEventKey* aKeyEvent)
return "GDK_KEY_PRESS";
case GDK_KEY_RELEASE:
return "GDK_KEY_RELEASE";
default:
return "Unknown";
}
}
+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:
explicit GetWritingModeName(const WritingMode& aWritingMode)
{
if (!aWritingMode.IsVertical()) {
AssignLiteral("Horizontal");
return;
@@ -169,44 +236,92 @@ IMContextWrapper::IMContextWrapper(nsWin
, mLastFocusedWindow(nullptr)
, mContext(nullptr)
, mSimpleContext(nullptr)
, mDummyContext(nullptr)
, mComposingContext(nullptr)
, mCompositionStart(UINT32_MAX)
, mProcessingKeyEvent(nullptr)
, mCompositionState(eCompositionState_NotComposing)
+ , mIMContextID(IMContextID::eUnknown)
, mIsIMFocused(false)
, mFallbackToKeyEvent(false)
, mKeyboardEventWasDispatched(false)
, mIsDeletingSurrounding(false)
, mLayoutChanged(false)
, mSetCursorPositionOnKeyEvent(true)
, mPendingResettingIMContext(false)
, mRetrieveSurroundingSignalReceived(false)
, mMaybeInDeadKeySequence(false)
+ , mIsIMInAsyncKeyHandlingMode(false)
{
static bool sFirstInstance = true;
if (sFirstInstance) {
sFirstInstance = false;
sUseSimpleContext =
Preferences::GetBool(
"intl.ime.use_simple_context_on_password_field",
kUseSimpleContextDefault);
}
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));
// NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
// So, we don't need to check the result.
// Normal context.
@@ -219,16 +334,41 @@ IMContextWrapper::Init()
g_signal_connect(mContext, "delete_surrounding",
G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback), this);
g_signal_connect(mContext, "commit",
G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback), this);
g_signal_connect(mContext, "preedit_start",
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) {
mSimpleContext = gtk_im_context_simple_new();
gtk_im_context_set_client_window(mSimpleContext, gdkWindow);
g_signal_connect(mSimpleContext, "preedit_changed",
G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback),
this);
@@ -247,16 +387,23 @@ IMContextWrapper::Init()
g_signal_connect(mSimpleContext, "preedit_end",
G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
this);
}
// 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()
{
if (this == sLastFocusedContext) {
sLastFocusedContext = nullptr;
}
MOZ_LOG(gGtkIMLog, LogLevel::Info,
@@ -493,26 +640,33 @@ IMContextWrapper::OnKeyEvent(nsWindow* a
if (!mInputContext.mIMEState.MaybeEditable() ||
MOZ_UNLIKELY(IsDestroyed())) {
return false;
}
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,
("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
"window, mLastFocusedWindow=0x%p",
this, mLastFocusedWindow));
return false;
}
@@ -533,16 +687,88 @@ IMContextWrapper::OnKeyEvent(nsWindow* a
}
// Let's support dead key event even if active keyboard layout also
// supports complicated composition like CJK IME.
bool isDeadKey =
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
+ // <input type="password"> 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;
gboolean isFiltered =
gtk_im_context_filter_keypress(currentContext, aEvent);
// The caller of this shouldn't handle aEvent anymore if we've dispatched
// composition events or modified content with other events.
@@ -564,19 +790,19 @@ IMContextWrapper::OnKeyEvent(nsWindow* a
DispatchCompositionCommitEvent(currentContext, &EmptyString());
mProcessingKeyEvent = aEvent;
// In this case, even though we handle the keyboard event here,
// but we should dispatch keydown event as
filterThisEvent = false;
}
// 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.
}
mProcessingKeyEvent = nullptr;
if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) {
// If the key event hasn't been handled by active IME nor keyboard
@@ -584,21 +810,22 @@ IMContextWrapper::OnKeyEvent(nsWindow* a
// ended. Note that we should not reset it when the key event is
// GDK_KEY_RELEASE since it may not be filtered by active keyboard
// layout even in composition.
mMaybeInDeadKeySequence = false;
}
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;
}
void
IMContextWrapper::OnFocusChangeInGecko(bool aFocus)
{
MOZ_LOG(gGtkIMLog, LogLevel::Info,
--- a/widget/gtk/IMContextWrapper.h
+++ b/widget/gtk/IMContextWrapper.h
@@ -92,16 +92,47 @@ public:
const InputContext* aContext,
const InputContextAction* aAction);
InputContext GetInputContext();
void OnUpdateComposition();
void OnLayoutChange();
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();
// Owner of an instance of this class. This should be top level window.
// The owner window must release the contexts when it's destroyed because
// the IME contexts need the native window. If OnDestroyWindow() is called
// with the owner window, it'll release IME contexts. Otherwise, it'll
// just clean up any existing composition if it's related to the destroying
@@ -170,17 +201,18 @@ protected:
mLength = UINT32_MAX;
}
};
// current target offset and length of IME composition
Range mCompositionTargetRange;
// mCompositionState indicates current status of composition.
- enum eCompositionState {
+ enum eCompositionState : uint8_t
+ {
eCompositionState_NotComposing,
eCompositionState_CompositionStartDispatched,
eCompositionState_CompositionChangeEventDispatched
};
eCompositionState mCompositionState;
bool IsComposing() const
{
@@ -222,16 +254,20 @@ protected:
return "CompositionStartDispatched";
case eCompositionState_CompositionChangeEventDispatched:
return "CompositionChangeEventDispatched";
default:
return "InvaildState";
}
}
+ // mIMContextID indicates the ID of mContext. This is actually indicates
+ // IM which user selected.
+ IMContextID mIMContextID;
+
struct Selection final
{
nsString mString;
uint32_t mOffset;
WritingMode mWritingMode;
Selection()
: mOffset(UINT32_MAX)
@@ -316,16 +352,20 @@ protected:
// sequence. For example, when you press dead key grave with ibus Spanish
// keyboard layout, it just consumes the key event when we call
// gtk_im_context_filter_keypress(). Then, pressing "Escape" key cancels
// the dead key sequence but we don't receive any signal and it's consumed
// by gtk_im_context_filter_keypress() normally. On the other hand, when
// 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,
// this is cleared. So, this refers valid pointer always.
static IMContextWrapper* sLastFocusedContext;
// sUseSimpleContext indeicates if password editors and editors with
// |ime-mode: disabled;| should use GtkIMContextSimple.