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
This commit is contained in:
Masayuki Nakano 2018-03-09 00:46:52 +09:00
Родитель b91f33e3d1
Коммит 22ab980e4c
3 изменённых файлов: 286 добавлений и 18 удалений

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

@ -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
// <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;
@ -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;
}

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

@ -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,

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

@ -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)