Bug 1343451 - part 3-2: Make IMContextWrapper dispatch eKeyDown event or eKeyUp event if IME handles native key event but we have not dispatched DOM event for it yet r?m_kato draft
authorMasayuki Nakano <masayuki@d-toybox.com>
Thu, 22 Feb 2018 20:56:08 +0900
changeset 766629 46a2dcc9ccc49743d9308ef1403f7882cdb9bead
parent 766628 9db6aee9190dfe66a5c245bd2cd5a9ba42b178ac
child 766630 7f45e7f41ee946b755930f871195c29796780744
push id102374
push usermasayuki@d-toybox.com
push dateTue, 13 Mar 2018 06:34:13 +0000
reviewersm_kato
bugs1343451
milestone61.0a1
Bug 1343451 - part 3-2: Make IMContextWrapper dispatch eKeyDown event or eKeyUp event if IME handles native key event but we have not dispatched DOM event for it yet r?m_kato Currently, IMContextWrapper doesn't dispatch eKeyDown event and eKeyUp event if it's handled by IME. However, for conforming to UI Events, it should not eat given keyboard events completely. This patch makes IMContextWrapper dispatches eKeyDown event or eKeyUp event before dispatching first event of composition events or content command event. MozReview-Commit-ID: H2jHpViTH5Q
widget/gtk/IMContextWrapper.cpp
widget/gtk/IMContextWrapper.h
--- 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()) {
--- a/widget/gtk/IMContextWrapper.h
+++ b/widget/gtk/IMContextWrapper.h
@@ -60,23 +60,36 @@ public:
     void OnDestroyWindow(nsWindow* aWindow);
     // OnFocusChangeInGecko is a notification that an editor gets focus.
     void OnFocusChangeInGecko(bool aFocus);
     // OnSelectionChange is a notification that selection (caret) is changed
     // in the focused editor.
     void OnSelectionChange(nsWindow* aCaller,
                            const IMENotification& aIMENotification);
 
-    // OnKeyEvent is called when aWindow gets a native key press event or a
-    // native key release event.  If this returns TRUE, the key event was
-    // filtered by IME.  Otherwise, this returns FALSE.
-    // NOTE: When the keypress event starts composition, this returns TRUE but
-    //       this dispatches keydown event before compositionstart event.
+    /**
+     * OnKeyEvent() is called when aWindow gets a native key press event or a
+     * native key release event.  If this returns true, the key event was
+     * filtered by IME.  Otherwise, this returns false.
+     * NOTE: When the native key press event starts composition, this returns
+     *       true but dispatches an eKeyDown event or eKeyUp event before
+     *       dispatching composition events or content command event.
+     *
+     * @param aWindow                       A window on which user operate the
+     *                                      key.
+     * @param aEvent                        A native key press or release
+     *                                      event.
+     * @param aKeyboardEventWasDispatched   true if eKeyDown or eKeyUp event
+     *                                      for aEvent has already been
+     *                                      dispatched.  In this case,
+     *                                      this class doesn't dispatch
+     *                                      keyboard event anymore.
+     */
     bool OnKeyEvent(nsWindow* aWindow, GdkEventKey* aEvent,
-                      bool aKeyDownEventWasSent = false);
+                    bool aKeyboardEventWasDispatched = false);
 
     // IME related nsIWidget methods.
     nsresult EndIMEComposition(nsWindow* aCaller);
     void SetInputContext(nsWindow* aCaller,
                          const InputContext* aContext,
                          const InputContextAction* aAction);
     InputContext GetInputContext();
     void OnUpdateComposition();
@@ -258,26 +271,30 @@ protected:
             return endOffset.value();
         }
     } mSelection;
     bool EnsureToCacheSelection(nsAString* aSelectedString = nullptr);
 
     // mIsIMFocused is set to TRUE when we call gtk_im_context_focus_in(). And
     // it's set to FALSE when we call gtk_im_context_focus_out().
     bool mIsIMFocused;
-    // mFilterKeyEvent is used by OnKeyEvent().  If the commit event should
-    // be processed as simple key event, this is set to TRUE by the commit
-    // handler.
-    bool mFilterKeyEvent;
-    // mKeyDownEventWasSent is used by OnKeyEvent() and
-    // DispatchCompositionStart().  DispatchCompositionStart() dispatches
-    // a keydown event if the composition start is caused by a native
-    // keypress event.  If this is true, the keydown event has been dispatched.
-    // Then, DispatchCompositionStart() doesn't dispatch keydown event.
-    bool mKeyDownEventWasSent;
+    // mFallbackToKeyEvent is set to false when this class starts to handle
+    // a native key event (at that time, mProcessingKeyEvent is set to the
+    // native event).  If active IME just commits composition with a character
+    // which is produced by the key with current keyboard layout, this is set
+    // to true.
+    bool mFallbackToKeyEvent;
+    // mKeyboardEventWasDispatched is used by OnKeyEvent() and
+    // MaybeDispatchKeyEventAsProcessedByIME().
+    // MaybeDispatchKeyEventAsProcessedByIME() dispatches an eKeyDown or
+    // eKeyUp event event if the composition is caused by a native
+    // key press event.  If this is true, a keyboard event has been dispatched
+    // for the native event.  If so, MaybeDispatchKeyEventAsProcessedByIME()
+    // won't dispatch keyboard event anymore.
+    bool mKeyboardEventWasDispatched;
     // mIsDeletingSurrounding is true while OnDeleteSurroundingNative() is
     // trying to delete the surrounding text.
     bool mIsDeletingSurrounding;
     // mLayoutChanged is true after OnLayoutChange() is called.  This is reset
     // when eCompositionChange is being dispatched.
     bool mLayoutChanged;
     // mSetCursorPositionOnKeyEvent true when caret rect or position is updated
     // with no composition.  If true, we update candidate window position
@@ -438,22 +455,38 @@ protected:
     // Called before destroying the context to work around some platform bugs.
     void PrepareToDestroyContext(GtkIMContext* aContext);
 
     /**
      *  WARNING:
      *    Following methods dispatch gecko events.  Then, the focused widget
      *    can be destroyed, and also it can be stolen focus.  If they returns
      *    FALSE, callers cannot continue the composition.
+     *      - MaybeDispatchKeyEventAsProcessedByIME
      *      - DispatchCompositionStart
      *      - DispatchCompositionChangeEvent
      *      - DispatchCompositionCommitEvent
      */
 
     /**
+     * Dispatch an eKeyDown or eKeyUp event whose mKeyCode value is
+     * NS_VK_PROCESSKEY and mKeyNameIndex is KEY_NAME_INDEX_Process if
+     * mProcessingKeyEvent is not nullptr and mKeyboardEventWasDispatched is
+     * still false.  If this dispatches a keyboard event, this sets
+     * mKeyboardEventWasDispatched to true.
+     *
+     * @return                      If the caller can continue to handle
+     *                              composition, returns true.  Otherwise,
+     *                              false.  For example, if focus is moved
+     *                              by dispatched keyboard event, returns
+     *                              false.
+     */
+    bool MaybeDispatchKeyEventAsProcessedByIME();
+
+    /**
      * Dispatches a composition start event.
      *
      * @param aContext              A GtkIMContext which is being handled.
      * @return                      true if the focused widget is neither
      *                              destroyed nor changed.  Otherwise, false.
      */
     bool DispatchCompositionStart(GtkIMContext* aContext);