--- a/widget/gtk/IMContextWrapper.cpp
+++ b/widget/gtk/IMContextWrapper.cpp
@@ -170,16 +170,18 @@ IMContextWrapper::IMContextWrapper(nsWin
, mContext(nullptr)
, mSimpleContext(nullptr)
, mDummyContext(nullptr)
, mComposingContext(nullptr)
, mCompositionStart(UINT32_MAX)
, mProcessingKeyEvent(nullptr)
, mCompositionState(eCompositionState_NotComposing)
, mIsIMFocused(false)
+ , mFallbackToKeyEvent(false)
+ , mKeyboardEventWasDispatched(false)
, mIsDeletingSurrounding(false)
, mLayoutChanged(false)
, mSetCursorPositionOnKeyEvent(true)
, mPendingResettingIMContext(false)
, mRetrieveSurroundingSignalReceived(false)
{
static bool sFirstInstance = true;
if (sFirstInstance) {
@@ -479,30 +481,30 @@ IMContextWrapper::OnBlurWindow(nsWindow*
}
Blur();
}
bool
IMContextWrapper::OnKeyEvent(nsWindow* aCaller,
GdkEventKey* aEvent,
- bool aKeyDownEventWasSent /* = false */)
+ bool aKeyboardEventWasDispatched /* = false */)
{
NS_PRECONDITION(aEvent, "aEvent must be non-null");
if (!mInputContext.mIMEState.MaybeEditable() ||
MOZ_UNLIKELY(IsDestroyed())) {
return false;
}
MOZ_LOG(gGtkIMLog, LogLevel::Info,
- ("0x%p OnKeyEvent(aCaller=0x%p, aKeyDownEventWasSent=%s), "
+ ("0x%p OnKeyEvent(aCaller=0x%p, aKeyboardEventWasDispatched=%s), "
"mCompositionState=%s, current context=0x%p, active context=0x%p, "
"aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X }",
- this, aCaller, ToChar(aKeyDownEventWasSent),
+ this, aCaller, ToChar(aKeyboardEventWasDispatched),
GetCompositionStateName(), GetCurrentContext(), GetActiveContext(),
aEvent, GetEventType(aEvent), gdk_keyval_name(aEvent->keyval),
gdk_keyval_to_unicode(aEvent->keyval)));
if (aCaller != mLastFocusedWindow) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
"window, mLastFocusedWindow=0x%p",
@@ -520,59 +522,61 @@ IMContextWrapper::OnKeyEvent(nsWindow* a
return false;
}
if (mSetCursorPositionOnKeyEvent) {
SetCursorPosition(currentContext);
mSetCursorPositionOnKeyEvent = false;
}
- mKeyDownEventWasSent = aKeyDownEventWasSent;
- mFilterKeyEvent = true;
+ mKeyboardEventWasDispatched = aKeyboardEventWasDispatched;
+ mFallbackToKeyEvent = false;
mProcessingKeyEvent = aEvent;
gboolean isFiltered =
gtk_im_context_filter_keypress(currentContext, aEvent);
- mProcessingKeyEvent = nullptr;
- // We filter the key event if the event was not committed (because
- // it's probably part of a composition) or if the key event was
- // committed _and_ changed. This way we still let key press
- // events go through as simple key press events instead of
- // composed characters.
- bool filterThisEvent = isFiltered && mFilterKeyEvent;
+ // The caller of this shouldn't handle aEvent anymore if we've dispatched
+ // composition events or modified content with other events.
+ bool filterThisEvent = isFiltered && !mFallbackToKeyEvent;
- if (IsComposingOnCurrentContext() && !isFiltered) {
- if (aEvent->type == GDK_KEY_PRESS) {
- if (!mDispatchedCompositionString.IsEmpty()) {
- // If there is composition string, we shouldn't dispatch
- // any keydown events during composition.
- filterThisEvent = true;
- } else {
- // A Hangul input engine for SCIM doesn't emit preedit_end
- // signal even when composition string becomes empty. On the
- // other hand, we should allow to make composition with empty
- // string for other languages because there *might* be such
- // IM. For compromising this issue, we should dispatch
- // compositionend event, however, we don't need to reset IM
- // actually.
- DispatchCompositionCommitEvent(currentContext, &EmptyString());
- filterThisEvent = false;
- }
- } else {
- // Key release event may not be consumed by IM, however, we
- // shouldn't dispatch any keyup event during composition.
- filterThisEvent = true;
- }
+ if (IsComposingOnCurrentContext() && !isFiltered &&
+ aEvent->type == GDK_KEY_PRESS &&
+ mDispatchedCompositionString.IsEmpty()) {
+ // A Hangul input engine for SCIM doesn't emit preedit_end
+ // signal even when composition string becomes empty. On the
+ // other hand, we should allow to make composition with empty
+ // string for other languages because there *might* be such
+ // IM. For compromising this issue, we should dispatch
+ // compositionend event, however, we don't need to reset IM
+ // actually.
+ // NOTE: Don't dispatch key events as "processed by IME" since
+ // we need to dispatch keyboard events as IME wasn't handled it.
+ mProcessingKeyEvent = nullptr;
+ 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) {
+ MaybeDispatchKeyEventAsProcessedByIME();
+ // Be aware, the widget might have been gone here.
+ }
+
+ mProcessingKeyEvent = nullptr;
+
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s "
- "(isFiltered=%s, mFilterKeyEvent=%s), mCompositionState=%s",
+ "(isFiltered=%s, mFallbackToKeyEvent=%s), mCompositionState=%s",
this, ToChar(filterThisEvent), ToChar(isFiltered),
- ToChar(mFilterKeyEvent), GetCompositionStateName()));
+ ToChar(mFallbackToKeyEvent), GetCompositionStateName()));
return filterThisEvent;
}
void
IMContextWrapper::OnFocusChangeInGecko(bool aFocus)
{
MOZ_LOG(gGtkIMLog, LogLevel::Info,
@@ -1297,36 +1301,41 @@ IMContextWrapper::OnCommitCompositionNat
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
("0x%p OnCommitCompositionNative(), Warning, does nothing "
"because has not started composition and commit string is empty",
this));
return;
}
// If IME doesn't change their keyevent that generated this commit,
- // don't send it through XIM - just send it as a normal key press
- // event.
+ // we should treat that IME didn't handle the key event because
+ // web applications want to receive "keydown" and "keypress" event
+ // in such case.
// NOTE: While a key event is being handled, this might be caused on
// current context. Otherwise, this may be caused on active context.
- if (!IsComposingOn(aContext) && mProcessingKeyEvent &&
+ if (!IsComposingOn(aContext) &&
+ mProcessingKeyEvent &&
+ mProcessingKeyEvent->type == GDK_KEY_PRESS &&
aContext == GetCurrentContext()) {
char keyval_utf8[8]; /* should have at least 6 bytes of space */
gint keyval_utf8_len;
guint32 keyval_unicode;
keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval);
keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8);
keyval_utf8[keyval_utf8_len] = '\0';
if (!strcmp(commitString, keyval_utf8)) {
MOZ_LOG(gGtkIMLog, LogLevel::Info,
("0x%p OnCommitCompositionNative(), "
"we'll send normal key event",
this));
- mFilterKeyEvent = false;
+ // In this case, eKeyDown and eKeyPress event should be dispatched
+ // by the caller of OnKeyEvent().
+ mFallbackToKeyEvent = true;
return;
}
}
NS_ConvertUTF8toUTF16 str(commitString);
// Be aware, widget can be gone
DispatchCompositionCommitEvent(aContext, &str);
}
@@ -1351,16 +1360,75 @@ IMContextWrapper::GetCompositionString(G
"aCompositionString=\"%s\"",
this, aContext, preedit_string));
pango_attr_list_unref(feedback_list);
g_free(preedit_string);
}
bool
+IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME()
+{
+ if (!mProcessingKeyEvent || mKeyboardEventWasDispatched ||
+ !mLastFocusedWindow) {
+ return true;
+ }
+
+ // A "keydown" or "keyup" event handler may change focus with the
+ // following event. In such case, we need to cancel this composition.
+ // So, we need to store IM context now because mComposingContext may be
+ // overwritten with different context if calling this method recursively.
+ // Note that we don't need to grab the context here because |context|
+ // will be used only for checking if it's same as mComposingContext.
+ GtkIMContext* oldCurrentContext = GetCurrentContext();
+ GtkIMContext* oldComposingContext = mComposingContext;
+
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+ mKeyboardEventWasDispatched = true;
+
+ // FYI: We should ignore if default of preceding keydown or keyup event is
+ // prevented since even on the other browsers, web applications
+ // cannot cancel the following composition event.
+ // Spec bug: https://github.com/w3c/uievents/issues/180
+ bool isCancelled;
+ lastFocusedWindow->DispatchKeyDownOrKeyUpEvent(mProcessingKeyEvent, true,
+ &isCancelled);
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup "
+ "event is dispatched",
+ this));
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the "
+ "focused widget was destroyed/changed by a key event",
+ this));
+ return false;
+ }
+
+ // If the dispatched keydown event caused moving focus and that also
+ // caused changing active context, we need to cancel composition here.
+ if (GetCurrentContext() != oldCurrentContext) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the key "
+ "event causes changing active IM context",
+ this));
+ if (mComposingContext == oldComposingContext) {
+ // Only when the context is still composing, we should call
+ // ResetIME() here. Otherwise, it should've already been
+ // cleaned up.
+ ResetIME();
+ }
+ return false;
+ }
+
+ return true;
+}
+
+bool
IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext)
{
MOZ_LOG(gGtkIMLog, LogLevel::Info,
("0x%p DispatchCompositionStart(aContext=0x%p)",
this, aContext));
if (IsComposing()) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
@@ -1394,62 +1462,26 @@ IMContextWrapper::DispatchCompositionSta
// XXX The composition start point might be changed by composition events
// even though we strongly hope it doesn't happen.
// Every composition event should have the start offset for the result
// because it may high cost if we query the offset every time.
mCompositionStart = mSelection.mOffset;
mDispatchedCompositionString.Truncate();
- if (mProcessingKeyEvent && !mKeyDownEventWasSent &&
- mProcessingKeyEvent->type == GDK_KEY_PRESS) {
- // A keydown event handler may change focus with the following keydown
- // event. In such case, we need to cancel this composition. So, we
- // need to store IM context now because mComposingContext may be
- // overwritten with different context if calling this method
- // recursively.
- // Note that we don't need to grab the context here because |context|
- // will be used only for checking if it's same as mComposingContext.
- GtkIMContext* context = mComposingContext;
-
- // If this composition is started by a native keydown event, we need to
- // dispatch our keydown event here (before composition start).
- // Note that make the preceding eKeyDown event marked as "processed
- // by IME" for compatibility with Chromium.
- bool isCancelled;
- mLastFocusedWindow->DispatchKeyDownOrKeyUpEvent(mProcessingKeyEvent,
- true, &isCancelled);
- MOZ_LOG(gGtkIMLog, LogLevel::Debug,
- ("0x%p DispatchCompositionStart(), preceding keydown event is "
- "dispatched",
+ // If this composition is started by a key press, we need to dispatch
+ // eKeyDown or eKeyUp event before dispatching eCompositionStart event.
+ // Note that dispatching a keyboard event which is marked as "processed
+ // by IME" is okay since Chromium also dispatches keyboard event as so.
+ if (!MaybeDispatchKeyEventAsProcessedByIME()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p DispatchCompositionStart(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
this));
- if (lastFocusedWindow->IsDestroyed() ||
- lastFocusedWindow != mLastFocusedWindow) {
- MOZ_LOG(gGtkIMLog, LogLevel::Warning,
- ("0x%p DispatchCompositionStart(), Warning, the focused "
- "widget was destroyed/changed by keydown event",
- this));
- return false;
- }
-
- // If the dispatched keydown event caused moving focus and that also
- // caused changing active context, we need to cancel composition here.
- if (GetCurrentContext() != context) {
- MOZ_LOG(gGtkIMLog, LogLevel::Warning,
- ("0x%p DispatchCompositionStart(), Warning, the preceding "
- "keydown event causes changing active IM context",
- this));
- if (mComposingContext == context) {
- // Only when the context is still composing, we should call
- // ResetIME() here. Otherwise, it should've already been
- // cleaned up.
- ResetIME();
- }
- return false;
- }
+ return false;
}
RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
nsresult rv = dispatcher->BeginNativeInputTransaction();
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
("0x%p DispatchCompositionStart(), FAILED, "
"due to BeginNativeInputTransaction() failure",
@@ -1497,16 +1529,25 @@ IMContextWrapper::DispatchCompositionCha
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
("0x%p DispatchCompositionChangeEvent(), the composition "
"wasn't started, force starting...",
this));
if (!DispatchCompositionStart(aContext)) {
return false;
}
}
+ // If this composition string change caused by a key press, we need to
+ // dispatch eKeyDown or eKeyUp before dispatching eCompositionChange event.
+ else if (!MaybeDispatchKeyEventAsProcessedByIME()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p DispatchCompositionChangeEvent(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ return false;
+ }
RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
nsresult rv = dispatcher->BeginNativeInputTransaction();
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
("0x%p DispatchCompositionChangeEvent(), FAILED, "
"due to BeginNativeInputTransaction() failure",
this));
@@ -1586,32 +1627,50 @@ IMContextWrapper::DispatchCompositionCom
if (!mLastFocusedWindow) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
("0x%p DispatchCompositionCommitEvent(), FAILED, "
"there are no focused window in this module",
this));
return false;
}
+ // TODO: We need special care to handle request to commit composition
+ // by content while we're committing composition because we have
+ // commit string information now but IME may not have composition
+ // anymore. Therefore, we may not be able to handle commit as
+ // expected. However, this is rare case because this situation
+ // never occurs with remote content. So, it's okay to fix this
+ // issue later. (Perhaps, TextEventDisptcher should do it for
+ // all platforms. E.g., creating WillCommitComposition()?)
if (!IsComposing()) {
if (!aCommitString || aCommitString->IsEmpty()) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
("0x%p DispatchCompositionCommitEvent(), FAILED, "
"there is no composition and empty commit string",
this));
return true;
}
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
("0x%p DispatchCompositionCommitEvent(), "
"the composition wasn't started, force starting...",
this));
if (!DispatchCompositionStart(aContext)) {
return false;
}
}
+ // If this commit caused by a key press, we need to dispatch eKeyDown or
+ // eKeyUp before dispatching composition events.
+ else if (!MaybeDispatchKeyEventAsProcessedByIME()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p DispatchCompositionCommitEvent(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ mCompositionState = eCompositionState_NotComposing;
+ return false;
+ }
RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
nsresult rv = dispatcher->BeginNativeInputTransaction();
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
("0x%p DispatchCompositionCommitEvent(), FAILED, "
"due to BeginNativeInputTransaction() failure",
this));
@@ -2296,16 +2355,26 @@ IMContextWrapper::DeleteText(GtkIMContex
lastFocusedWindow->Destroyed()) {
MOZ_LOG(gGtkIMLog, LogLevel::Error,
("0x%p DeleteText(), FAILED, setting selection caused "
"focus change or window destroyed",
this));
return NS_ERROR_FAILURE;
}
+ // If this deleting text caused by a key press, we need to dispatch
+ // eKeyDown or eKeyUp before dispatching eContentCommandDelete event.
+ if (!MaybeDispatchKeyEventAsProcessedByIME()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p DeleteText(), Warning, "
+ "MaybeDispatchKeyEventAsProcessedByIME() returned false",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
// Delete the selection
WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete,
mLastFocusedWindow);
mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
if (!contentCommandEvent.mSucceeded ||
lastFocusedWindow != mLastFocusedWindow ||
lastFocusedWindow->Destroyed()) {