https://github.com/mozilla/gecko-dev
Raw File
Tip revision: 2090e4f3ef018a54be7f60c5832fe8691e8d5a20 authored by Mozilla Releng Treescript on 05 August 2024, 14:45:23 UTC
Update configs. IGNORE BROKEN CHANGESETS CLOSED TREE NO BUG a=release ba=release
Tip revision: 2090e4f
IMContextWrapper.cpp
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=4 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/Logging.h"
#include "nsString.h"
#include "prtime.h"
#include "prenv.h"

#include "IMContextWrapper.h"

#include "GRefPtr.h"
#include "nsGtkKeyUtils.h"
#include "nsWindow.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/Likely.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_intl.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TextEventDispatcher.h"
#include "mozilla/TextEvents.h"
#include "mozilla/ToString.h"
#include "mozilla/WritingModes.h"

// For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync`
// rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
// big file.
// Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
mozilla::LazyLogModule gIMELog("IMEHandler");

namespace mozilla {
namespace widget {

static inline const char* ToChar(bool aBool) {
  return aBool ? "true" : "false";
}

static const char* GetEventType(GdkEventKey* aKeyEvent) {
  switch (aKeyEvent->type) {
    case GDK_KEY_PRESS:
      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::Unknown) {
    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::IBus:
        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::Fcitx:
      case IMContextWrapper::IMContextID::Fcitx5:
        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 GetTextRangeStyleText final : public nsAutoCString {
 public:
  explicit GetTextRangeStyleText(const TextRangeStyle& aStyle) {
    if (!aStyle.IsDefined()) {
      AssignLiteral("{ IsDefined()=false }");
      return;
    }

    if (aStyle.IsLineStyleDefined()) {
      AppendLiteral("{ mLineStyle=");
      AppendLineStyle(aStyle.mLineStyle);
      if (aStyle.IsUnderlineColorDefined()) {
        AppendLiteral(", mUnderlineColor=");
        AppendColor(aStyle.mUnderlineColor);
      } else {
        AppendLiteral(", IsUnderlineColorDefined=false");
      }
    } else {
      AppendLiteral("{ IsLineStyleDefined()=false");
    }

    if (aStyle.IsForegroundColorDefined()) {
      AppendLiteral(", mForegroundColor=");
      AppendColor(aStyle.mForegroundColor);
    } else {
      AppendLiteral(", IsForegroundColorDefined()=false");
    }

    if (aStyle.IsBackgroundColorDefined()) {
      AppendLiteral(", mBackgroundColor=");
      AppendColor(aStyle.mBackgroundColor);
    } else {
      AppendLiteral(", IsBackgroundColorDefined()=false");
    }

    AppendLiteral(" }");
  }
  void AppendLineStyle(TextRangeStyle::LineStyle aLineStyle) {
    switch (aLineStyle) {
      case TextRangeStyle::LineStyle::None:
        AppendLiteral("LineStyle::None");
        break;
      case TextRangeStyle::LineStyle::Solid:
        AppendLiteral("LineStyle::Solid");
        break;
      case TextRangeStyle::LineStyle::Dotted:
        AppendLiteral("LineStyle::Dotted");
        break;
      case TextRangeStyle::LineStyle::Dashed:
        AppendLiteral("LineStyle::Dashed");
        break;
      case TextRangeStyle::LineStyle::Double:
        AppendLiteral("LineStyle::Double");
        break;
      case TextRangeStyle::LineStyle::Wavy:
        AppendLiteral("LineStyle::Wavy");
        break;
      default:
        AppendPrintf("Invalid(0x%02X)",
                     static_cast<TextRangeStyle::LineStyleType>(aLineStyle));
        break;
    }
  }
  void AppendColor(nscolor aColor) {
    AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }", NS_GET_R(aColor),
                 NS_GET_G(aColor), NS_GET_B(aColor), NS_GET_A(aColor));
  }
  virtual ~GetTextRangeStyleText() = default;
};

const static bool kUseSimpleContextDefault = false;

/******************************************************************************
 * SelectionStyleProvider
 *
 * IME (e.g., fcitx, ~4.2.8.3) may look up selection colors of widget, which
 * is related to the window associated with the IM context, to support any
 * colored widgets.  Our editor (like <input type="text">) is rendered as
 * native GtkTextView as far as possible by default and if editor color is
 * changed by web apps, nsTextFrame may swap background color of foreground
 * color of composition string for making composition string is always
 * visually distinct in normal text.
 *
 * So, we would like IME to set style of composition string to good colors
 * in GtkTextView.  Therefore, this class overwrites selection colors of
 * our widget with selection colors of GtkTextView so that it's possible IME
 * to refer selection colors of GtkTextView via our widget.
 ******************************************************************************/

static Maybe<nscolor> GetSystemColor(LookAndFeel::ColorID aId) {
  return LookAndFeel::GetColor(aId, LookAndFeel::ColorScheme::Light,
                               LookAndFeel::UseStandins::No);
}

class SelectionStyleProvider final {
 public:
  static SelectionStyleProvider* GetExistingInstance() { return sInstance; }

  static SelectionStyleProvider* GetInstance() {
    if (sHasShutDown) {
      return nullptr;
    }
    if (!sInstance) {
      sInstance = new SelectionStyleProvider();
    }
    return sInstance;
  }

  static void Shutdown() {
    if (sInstance) {
      g_object_unref(sInstance->mProvider);
    }
    delete sInstance;
    sInstance = nullptr;
    sHasShutDown = true;
  }

  // aGDKWindow is a GTK window which will be associated with an IM context.
  void AttachTo(GdkWindow* aGDKWindow) {
    GtkWidget* widget = nullptr;
    // gdk_window_get_user_data() typically returns pointer to widget that
    // window belongs to.  If it's widget, fcitx retrieves selection colors
    // of them.  So, we need to overwrite its style.
    gdk_window_get_user_data(aGDKWindow, (gpointer*)&widget);
    if (GTK_IS_WIDGET(widget)) {
      gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
                                     GTK_STYLE_PROVIDER(mProvider),
                                     GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    }
  }

  void OnThemeChanged() {
    // fcitx refers GtkStyle::text[GTK_STATE_SELECTED] and
    // GtkStyle::bg[GTK_STATE_SELECTED] (although pair of text and *base*
    // or *fg* and bg is correct).  gtk_style_update_from_context() will
    // set these colors using the widget's GtkStyleContext and so the
    // colors can be controlled by a ":selected" CSS rule.
    nsAutoCString style(":selected{");
    // FYI: LookAndFeel always returns selection colors of GtkTextView.
    if (auto selectionForegroundColor =
            GetSystemColor(LookAndFeel::ColorID::Highlight)) {
      double alpha =
          static_cast<double>(NS_GET_A(*selectionForegroundColor)) / 0xFF;
      style.AppendPrintf("color:rgba(%u,%u,%u,",
                         NS_GET_R(*selectionForegroundColor),
                         NS_GET_G(*selectionForegroundColor),
                         NS_GET_B(*selectionForegroundColor));
      // We can't use AppendPrintf here, because it does locale-specific
      // formatting of floating-point values.
      style.AppendFloat(alpha);
      style.AppendPrintf(");");
    }
    if (auto selectionBackgroundColor =
            GetSystemColor(LookAndFeel::ColorID::Highlighttext)) {
      double alpha =
          static_cast<double>(NS_GET_A(*selectionBackgroundColor)) / 0xFF;
      style.AppendPrintf("background-color:rgba(%u,%u,%u,",
                         NS_GET_R(*selectionBackgroundColor),
                         NS_GET_G(*selectionBackgroundColor),
                         NS_GET_B(*selectionBackgroundColor));
      style.AppendFloat(alpha);
      style.AppendPrintf(");");
    }
    style.AppendLiteral("}");
    gtk_css_provider_load_from_data(mProvider, style.get(), -1, nullptr);
  }

 private:
  static SelectionStyleProvider* sInstance;
  static bool sHasShutDown;
  GtkCssProvider* const mProvider;

  SelectionStyleProvider() : mProvider(gtk_css_provider_new()) {
    OnThemeChanged();
  }
};

SelectionStyleProvider* SelectionStyleProvider::sInstance = nullptr;
bool SelectionStyleProvider::sHasShutDown = false;

/******************************************************************************
 * IMContextWrapper
 ******************************************************************************/

IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr;
guint16 IMContextWrapper::sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
bool IMContextWrapper::sUseSimpleContext;

NS_IMPL_ISUPPORTS(IMContextWrapper, TextEventDispatcherListener,
                  nsISupportsWeakReference)

IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow)
    : mOwnerWindow(aOwnerWindow),
      mLastFocusedWindow(nullptr),
      mContext(nullptr),
      mSimpleContext(nullptr),
      mDummyContext(nullptr),
      mComposingContext(nullptr),
      mCompositionStart(UINT32_MAX),
      mProcessingKeyEvent(nullptr),
      mCompositionState(eCompositionState_NotComposing),
      mIMContextID(IMContextID::Unknown),
      mFallbackToKeyEvent(false),
      mKeyboardEventWasDispatched(false),
      mKeyboardEventWasConsumed(false),
      mIsDeletingSurrounding(false),
      mLayoutChanged(false),
      mSetCursorPositionOnKeyEvent(true),
      mPendingResettingIMContext(false),
      mRetrieveSurroundingSignalReceived(false),
      mMaybeInDeadKeySequence(false),
      mIsIMInAsyncKeyHandlingMode(false),
      mSetInputPurposeAndInputHints(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");
}

nsDependentCSubstring IMContextWrapper::GetIMName() const {
  const char* contextIDChar =
      gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext));
  if (!contextIDChar) {
    return nsDependentCSubstring();
  }

  nsDependentCSubstring im(contextIDChar, strlen(contextIDChar));

  // If the context is XIM, actual engine must be specified with
  // |XMODIFIERS=@im=foo|.
  const char* xmodifiersChar = PR_GetEnv("XMODIFIERS");
  if (!xmodifiersChar || !im.EqualsLiteral("xim")) {
    return im;
  }

  nsDependentCString xmodifiers(xmodifiersChar);
  int32_t atIMValueStart = xmodifiers.Find("@im=") + 4;
  if (atIMValueStart < 4 ||
      xmodifiers.Length() <= static_cast<size_t>(atIMValueStart)) {
    return im;
  }

  int32_t atIMValueEnd = xmodifiers.Find("@", atIMValueStart);
  if (atIMValueEnd > atIMValueStart) {
    return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
                                 atIMValueEnd - atIMValueStart);
  }

  if (atIMValueEnd == kNotFound) {
    return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
                                 strlen(xmodifiersChar) - atIMValueStart);
  }

  return im;
}

void IMContextWrapper::Init() {
  MozContainer* container = mOwnerWindow->GetMozContainer();
  MOZ_ASSERT(container, "container is null");
  GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container));

  // Overwrite selection colors of the window before associating the window
  // with IM context since IME may look up selection colors via IM context
  // to support any colored widgets.
  SelectionStyleProvider::GetInstance()->AttachTo(gdkWindow);

  // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
  //       So, we don't need to check the result.

  // Normal context.
  mContext = gtk_im_multicontext_new();
  gtk_im_context_set_client_window(mContext, gdkWindow);
  g_signal_connect(mContext, "preedit_changed",
                   G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback),
                   this);
  g_signal_connect(mContext, "retrieve_surrounding",
                   G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback),
                   this);
  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);
  nsDependentCSubstring im = GetIMName();
  if (im.EqualsLiteral("ibus")) {
    mIMContextID = IMContextID::IBus;
    mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode();
    // Although ibus has key snooper mode, it's forcibly disabled on Firefox
    // in default settings by its whitelist since we always send key events
    // to IME before handling shortcut keys.  The whitelist can be
    // customized with env, IBUS_NO_SNOOPER_APPS, but we don't need to
    // support such rare cases for reducing maintenance cost.
    mIsKeySnooped = false;
  } else if (im.EqualsLiteral("fcitx")) {
    mIMContextID = IMContextID::Fcitx;
    mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode();
    // Although Fcitx has key snooper mode similar to ibus, it's also
    // disabled on Firefox in default settings by its whitelist.  The
    // whitelist can be customized with env, IBUS_NO_SNOOPER_APPS or
    // FCITX_NO_SNOOPER_APPS, but we don't need to support such rare cases
    // for reducing maintenance cost.
    mIsKeySnooped = false;
  } else if (im.EqualsLiteral("fcitx5")) {
    mIMContextID = IMContextID::Fcitx5;
    mIsIMInAsyncKeyHandlingMode = true;  // does not have sync mode.
    mIsKeySnooped = false;               // never use key snooper.
  } else if (im.EqualsLiteral("uim")) {
    mIMContextID = IMContextID::Uim;
    mIsIMInAsyncKeyHandlingMode = false;
    // We cannot know if uim uses key snooper since it's build option of
    // uim.  Therefore, we need to retrieve the consideration from the
    // pref for making users and distributions allowed to choose their
    // preferred value.
    mIsKeySnooped =
        Preferences::GetBool("intl.ime.hack.uim.using_key_snooper", true);
  } else if (im.EqualsLiteral("scim")) {
    mIMContextID = IMContextID::Scim;
    mIsIMInAsyncKeyHandlingMode = false;
    mIsKeySnooped = false;
  } else if (im.EqualsLiteral("iiim")) {
    mIMContextID = IMContextID::IIIMF;
    mIsIMInAsyncKeyHandlingMode = false;
    mIsKeySnooped = false;
  } else if (im.EqualsLiteral("wayland")) {
    mIMContextID = IMContextID::Wayland;
    mIsIMInAsyncKeyHandlingMode = false;
    mIsKeySnooped = true;
  } else {
    mIMContextID = IMContextID::Unknown;
    mIsIMInAsyncKeyHandlingMode = false;
    mIsKeySnooped = 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);
    g_signal_connect(
        mSimpleContext, "retrieve_surrounding",
        G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback), this);
    g_signal_connect(mSimpleContext, "delete_surrounding",
                     G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback),
                     this);
    g_signal_connect(mSimpleContext, "commit",
                     G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback),
                     this);
    g_signal_connect(mSimpleContext, "preedit_start",
                     G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
                     this);
    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(gIMELog, LogLevel::Info,
          ("0x%p Init(), mOwnerWindow=%p, mContext=%p (im=\"%s\"), "
           "mIsIMInAsyncKeyHandlingMode=%s, mIsKeySnooped=%s, "
           "mSimpleContext=%p, mDummyContext=%p, "
           "gtk_im_multicontext_get_context_id()=\"%s\", "
           "PR_GetEnv(\"XMODIFIERS\")=\"%s\"",
           this, mOwnerWindow, mContext, nsAutoCString(im).get(),
           ToChar(mIsIMInAsyncKeyHandlingMode), ToChar(mIsKeySnooped),
           mSimpleContext, mDummyContext,
           gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)),
           PR_GetEnv("XMODIFIERS")));
}

/* static */
void IMContextWrapper::Shutdown() { SelectionStyleProvider::Shutdown(); }

IMContextWrapper::~IMContextWrapper() {
  MOZ_ASSERT(!mContext);
  MOZ_ASSERT(!mComposingContext);
  if (this == sLastFocusedContext) {
    sLastFocusedContext = nullptr;
  }
  MOZ_LOG(gIMELog, LogLevel::Info, ("0x%p ~IMContextWrapper()", this));
}

NS_IMETHODIMP
IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
                            const IMENotification& aNotification) {
  switch (aNotification.mMessage) {
    case REQUEST_TO_COMMIT_COMPOSITION:
    case REQUEST_TO_CANCEL_COMPOSITION: {
      nsWindow* window =
          static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
      return IsComposing() ? EndIMEComposition(window) : NS_OK;
    }
    case NOTIFY_IME_OF_FOCUS:
      OnFocusChangeInGecko(true);
      return NS_OK;
    case NOTIFY_IME_OF_BLUR:
      OnFocusChangeInGecko(false);
      return NS_OK;
    case NOTIFY_IME_OF_POSITION_CHANGE:
      OnLayoutChange();
      return NS_OK;
    case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
      OnUpdateComposition();
      return NS_OK;
    case NOTIFY_IME_OF_SELECTION_CHANGE: {
      nsWindow* window =
          static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
      OnSelectionChange(window, aNotification);
      return NS_OK;
    }
    default:
      return NS_ERROR_NOT_IMPLEMENTED;
  }
}

NS_IMETHODIMP_(void)
IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
  // XXX When input transaction is being stolen by add-on, what should we do?
}

NS_IMETHODIMP_(void)
IMContextWrapper::WillDispatchKeyboardEvent(
    TextEventDispatcher* aTextEventDispatcher,
    WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
    void* aData) {
  KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent,
                                           static_cast<GdkEventKey*>(aData));
}

TextEventDispatcher* IMContextWrapper::GetTextEventDispatcher() {
  if (NS_WARN_IF(!mLastFocusedWindow)) {
    return nullptr;
  }
  TextEventDispatcher* dispatcher =
      mLastFocusedWindow->GetTextEventDispatcher();
  // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr.
  MOZ_RELEASE_ASSERT(dispatcher);
  return dispatcher;
}

NS_IMETHODIMP_(IMENotificationRequests)
IMContextWrapper::GetIMENotificationRequests() {
  IMENotificationRequests::Notifications notifications =
      IMENotificationRequests::NOTIFY_NOTHING;
  // If it's not enabled, we don't need position change notification.
  if (IsEnabled()) {
    notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE;
  }
  return IMENotificationRequests(notifications);
}

void IMContextWrapper::OnDestroyWindow(nsWindow* aWindow) {
  MOZ_LOG(
      gIMELog, LogLevel::Info,
      ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
       "mOwnerWindow=0x%p, mLastFocusedModule=0x%p",
       this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext));

  MOZ_ASSERT(aWindow, "aWindow must not be null");

  if (mLastFocusedWindow == aWindow) {
    if (IsComposing()) {
      EndIMEComposition(aWindow);
    }
    NotifyIMEOfFocusChange(IMEFocusState::Blurred);
    mLastFocusedWindow = nullptr;
  }

  if (mOwnerWindow != aWindow) {
    return;
  }

  if (sLastFocusedContext == this) {
    sLastFocusedContext = nullptr;
  }

  /**
   * NOTE:
   *   The given window is the owner of this, so, we must disconnect from the
   *   contexts now.  But that might be referred from other nsWindows
   *   (they are children of this.  But we don't know why there are the
   *   cases).  So, we need to clear the pointers that refers to contexts
   *   and this if the other referrers are still alive. See bug 349727.
   */
  if (mContext) {
    PrepareToDestroyContext(mContext);
    gtk_im_context_set_client_window(mContext, nullptr);
    g_signal_handlers_disconnect_by_data(mContext, this);
    g_object_unref(mContext);
    mContext = nullptr;
  }

  if (mSimpleContext) {
    gtk_im_context_set_client_window(mSimpleContext, nullptr);
    g_signal_handlers_disconnect_by_data(mSimpleContext, this);
    g_object_unref(mSimpleContext);
    mSimpleContext = nullptr;
  }

  if (mDummyContext) {
    // mContext and mDummyContext have the same slaveType and signal_data
    // so no need for another workaround_gtk_im_display_closed.
    gtk_im_context_set_client_window(mDummyContext, nullptr);
    g_object_unref(mDummyContext);
    mDummyContext = nullptr;
  }

  if (NS_WARN_IF(mComposingContext)) {
    g_object_unref(mComposingContext);
    mComposingContext = nullptr;
  }

  mOwnerWindow = nullptr;
  mLastFocusedWindow = nullptr;
  mInputContext.mIMEState.mEnabled = IMEEnabled::Disabled;
  mPostingKeyEvents.Clear();

  MOZ_LOG(gIMELog, LogLevel::Debug,
          ("0x%p   OnDestroyWindow(), succeeded, Completely destroyed", this));
}

void IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext) {
  if (mIMContextID == IMContextID::IIIMF) {
    // IIIM module registers handlers for the "closed" signal on the
    // display, but the signal handler is not disconnected when the module
    // is unloaded.  To prevent the module from being unloaded, use static
    // variable to hold reference of slave context class declared by IIIM.
    // Note that this does not grab any instance, it grabs the "class".
    static gpointer sGtkIIIMContextClass = nullptr;
    if (!sGtkIIIMContextClass) {
      // We retrieved slave context class with g_type_name() and actual
      // slave context instance when our widget was GTK2.  That must be
      // _GtkIMContext::priv::slave in GTK3.  However, _GtkIMContext::priv
      // is an opacity struct named _GtkIMMulticontextPrivate, i.e., it's
      // not exposed by GTK3.  Therefore, we cannot access the instance
      // safely.  So, we need to retrieve the slave context class with
      // g_type_from_name("GtkIMContextIIIM") directly (anyway, we needed
      // to compare the class name with "GtkIMContextIIIM").
      GType IIMContextType = g_type_from_name("GtkIMContextIIIM");
      if (IIMContextType) {
        sGtkIIIMContextClass = g_type_class_ref(IIMContextType);
        MOZ_LOG(gIMELog, LogLevel::Info,
                ("0x%p PrepareToDestroyContext(), added to reference to "
                 "GtkIMContextIIIM class to prevent it from being unloaded",
                 this));
      } else {
        MOZ_LOG(gIMELog, LogLevel::Error,
                ("0x%p PrepareToDestroyContext(), FAILED to prevent the "
                 "IIIM module from being uploaded",
                 this));
      }
    }
  }
}

void IMContextWrapper::OnFocusWindow(nsWindow* aWindow) {
  if (MOZ_UNLIKELY(IsDestroyed())) {
    return;
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this,
           aWindow, mLastFocusedWindow));
  mLastFocusedWindow = aWindow;
}

void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) {
  if (MOZ_UNLIKELY(IsDestroyed())) {
    return;
  }

  MOZ_LOG(
      gIMELog, LogLevel::Info,
      ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
       "mIMEFocusState=%s",
       this, aWindow, mLastFocusedWindow, ToString(mIMEFocusState).c_str()));

  if (mLastFocusedWindow != aWindow) {
    return;
  }

  NotifyIMEOfFocusChange(IMEFocusState::Blurred);
}

KeyHandlingState IMContextWrapper::OnKeyEvent(
    nsWindow* aCaller, GdkEventKey* aEvent,
    bool aKeyboardEventWasDispatched /* = false */) {
  MOZ_ASSERT(aEvent, "aEvent must be non-null");

  if (!mInputContext.mIMEState.IsEditable() || MOZ_UNLIKELY(IsDestroyed())) {
    return KeyHandlingState::eNotHandled;
  }

  MOZ_LOG(gIMELog, LogLevel::Info, (">>>>>>>>>>>>>>>>"));
  MOZ_LOG(
      gIMELog, LogLevel::Info,
      ("0x%p OnKeyEvent(aCaller=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),
       GetEventStateName(aEvent->state, mIMContextID).get(), aEvent->time,
       aEvent->hardware_keycode, aEvent->group,
       ToChar(aKeyboardEventWasDispatched)));
  MOZ_LOG(
      gIMELog, 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(), ToString(mIMContextID).c_str(),
       ToChar(mIsIMInAsyncKeyHandlingMode)));

  if (aCaller != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnKeyEvent(), FAILED, the caller isn't focused "
             "window, mLastFocusedWindow=0x%p",
             this, mLastFocusedWindow));
    return KeyHandlingState::eNotHandled;
  }

  // Even if old IM context has composition, key event should be sent to
  // current context since the user expects so.
  GtkIMContext* currentContext = GetCurrentContext();
  if (MOZ_UNLIKELY(!currentContext)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnKeyEvent(), FAILED, there are no context", this));
    return KeyHandlingState::eNotHandled;
  }

  if (mSetCursorPositionOnKeyEvent) {
    SetCursorPosition(currentContext);
    mSetCursorPositionOnKeyEvent = false;
  }

  // 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 probablyHandledAsynchronously =
      mIsIMInAsyncKeyHandlingMode && currentContext == mContext;

  // If we're not sure whether the event is handled asynchronously, this is
  // set to true.
  bool maybeHandledAsynchronously = false;

  // If aEvent is a synthesized event for async handling, this will be set to
  // true.
  bool isHandlingAsyncEvent = false;

  // If we've decided that the event won't be synthesized asyncrhonously
  // by IME, but actually IME did it, this is set to true.
  bool isUnexpectedAsyncEvent = false;

  // 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 (probablyHandledAsynchronously) {
    switch (mIMContextID) {
      case IMContextID::IBus: {
        // 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.
        isHandlingAsyncEvent = !!(aEvent->state & IBUS_IGNORED_MASK);
        if (!isHandlingAsyncEvent) {
          // On some environments, IBUS_IGNORED_MASK flag is not set as
          // expected.  In such case, we keep pusing all events into the queue.
          // I.e., that causes eating a lot of memory until it's blurred.
          // Therefore, we need to check whether there is same timestamp event
          // in the queue.  This redundant cost should be low because in most
          // causes, key events in the queue should be 2 or 4.
          isHandlingAsyncEvent =
              mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
          if (isHandlingAsyncEvent) {
            MOZ_LOG(gIMELog, LogLevel::Info,
                    ("0x%p   OnKeyEvent(), aEvent->state does not have "
                     "IBUS_IGNORED_MASK but "
                     "same event in the queue.  So, assuming it's a "
                     "synthesized event",
                     this));
          }
        }

        // If it's a synthesized event, let's remove it from the posting
        // event queue first.  Otherwise the following blocks cannot use
        // `break`.
        if (isHandlingAsyncEvent) {
          MOZ_LOG(gIMELog, LogLevel::Info,
                  ("0x%p   OnKeyEvent(), aEvent->state has IBUS_IGNORED_MASK "
                   "or aEvent is in the "
                   "posting event queue, so, it won't be handled "
                   "asynchronously anymore. Removing "
                   "the posted events from the queue",
                   this));
          probablyHandledAsynchronously = false;
          mPostingKeyEvents.RemoveEvent(aEvent);
        }

        // ibus won't send back key press events in a dead key sequcne.
        if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
          probablyHandledAsynchronously = false;
          if (isHandlingAsyncEvent) {
            isUnexpectedAsyncEvent = true;
            break;
          }
          // Some keyboard layouts which have dead keys may send
          // "empty" key event to make us call
          // gtk_im_context_filter_keypress() to commit composed
          // character during a GDK_KEY_PRESS event dispatching.
          if (!gdk_keyval_to_unicode(aEvent->keyval) &&
              !aEvent->hardware_keycode) {
            isUnexpectedAsyncEvent = true;
            break;
          }
          break;
        }
        // ibus may handle key events synchronously if focused editor is
        // <input type="password"> or |ime-mode: disabled;|.  However, in
        // some environments, not so actually.  Therefore, we need to check
        // the result of gtk_im_context_filter_keypress() later.
        if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
          probablyHandledAsynchronously = false;
          maybeHandledAsynchronously = !isHandlingAsyncEvent;
          break;
        }
        break;
      }
      case IMContextID::Fcitx:
      case IMContextID::Fcitx5: {
        // 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.
        isHandlingAsyncEvent = !!(aEvent->state & FcitxKeyState_IgnoredMask);
        if (!isHandlingAsyncEvent) {
          // On some environments, FcitxKeyState_IgnoredMask flag *might* be not
          // set as expected. If there were such cases, we'd keep pusing all
          // events into the queue.  I.e., that would cause eating a lot of
          // memory until it'd be blurred.  Therefore, we should check whether
          // there is same timestamp event in the queue.  This redundant cost
          // should be low because in most causes, key events in the queue
          // should be 2 or 4.
          isHandlingAsyncEvent =
              mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
          if (isHandlingAsyncEvent) {
            MOZ_LOG(gIMELog, LogLevel::Info,
                    ("0x%p   OnKeyEvent(), aEvent->state does not have "
                     "FcitxKeyState_IgnoredMask "
                     "but same event in the queue.  So, assuming it's a "
                     "synthesized event",
                     this));
          }
        }

        // fcitx won't send back key press events in a dead key sequcne.
        if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
          probablyHandledAsynchronously = false;
          if (isHandlingAsyncEvent) {
            isUnexpectedAsyncEvent = true;
            break;
          }
          // Some keyboard layouts which have dead keys may send
          // "empty" key event to make us call
          // gtk_im_context_filter_keypress() to commit composed
          // character during a GDK_KEY_PRESS event dispatching.
          if (!gdk_keyval_to_unicode(aEvent->keyval) &&
              !aEvent->hardware_keycode) {
            isUnexpectedAsyncEvent = true;
            break;
          }
        }

        // fcitx handles key events asynchronously even if focused
        // editor cannot use IME actually.

        if (isHandlingAsyncEvent) {
          MOZ_LOG(gIMELog, LogLevel::Info,
                  ("0x%p   OnKeyEvent(), aEvent->state has "
                   "FcitxKeyState_IgnoredMask or aEvent is in "
                   "the posting event queue, so, it won't be handled "
                   "asynchronously anymore. "
                   "Removing the posted events from the queue",
                   this));
          probablyHandledAsynchronously = false;
          mPostingKeyEvents.RemoveEvent(aEvent);
          break;
        }
        break;
      }
      default:
        MOZ_ASSERT_UNREACHABLE(
            "IME may handle key event "
            "asyncrhonously, but not yet confirmed if it comes agian "
            "actually");
    }
  }

  if (!isUnexpectedAsyncEvent) {
    mKeyboardEventWasDispatched = aKeyboardEventWasDispatched;
    mKeyboardEventWasConsumed = false;
  } else {
    // If we didn't expect this event, we've alreday dispatched eKeyDown
    // event or eKeyUp event for that.
    mKeyboardEventWasDispatched = true;
    // And in this case, we need to assume that another key event hasn't
    // been receivied and mKeyboardEventWasConsumed keeps storing the
    // dispatched eKeyDown or eKeyUp event's state.
  }
  mFallbackToKeyEvent = false;
  mProcessingKeyEvent = aEvent;
  gboolean isFiltered = gtk_im_context_filter_keypress(currentContext, aEvent);

  // If we're not sure whether the event is handled by IME asynchronously or
  // synchronously, we need to trust the result of
  // gtk_im_context_filter_keypress().  If it consumed and but did nothing,
  // we can assume that another event will be synthesized.
  if (!isHandlingAsyncEvent && maybeHandledAsynchronously) {
    probablyHandledAsynchronously |=
        isFiltered && !mFallbackToKeyEvent && !mKeyboardEventWasDispatched;
  }

  if (aEvent->type == GDK_KEY_PRESS) {
    if (isFiltered && probablyHandledAsynchronously) {
      sWaitingSynthesizedKeyPressHardwareKeyCode = aEvent->hardware_keycode;
    } else {
      sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
    }
  }

  // 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 &&
      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 (filterThisEvent && !mKeyboardEventWasDispatched) {
    // If IME handled the key event but we've not dispatched eKeyDown nor
    // eKeyUp event yet, we need to dispatch here unless the key event is
    // now being handled by other IME process.
    if (!probablyHandledAsynchronously) {
      MaybeDispatchKeyEventAsProcessedByIME(eVoidEvent);
      // Be aware, the widget might have been gone here.
    }
    // If we need to wait reply from IM, IM may send some signals to us
    // without sending the key event again.  In such case, we need to
    // dispatch keyboard events with a copy of aEvent.  Therefore, we
    // need to use information of this key event to dispatch an KeyDown
    // or eKeyUp event later.
    else {
      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p   OnKeyEvent(), putting aEvent into the queue...", this));
      mPostingKeyEvents.PutEvent(aEvent);
    }
  }

  mProcessingKeyEvent = nullptr;

  if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) {
    // If the key event hasn't been handled by active IME nor keyboard
    // layout, we can assume that the dead key sequence has been or was
    // 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;
  }

  if (aEvent->type == GDK_KEY_RELEASE) {
    if (const GdkEventKey* pendingKeyPressEvent =
            mPostingKeyEvents.GetCorrespondingKeyPressEvent(aEvent)) {
      MOZ_LOG(gIMELog, LogLevel::Warning,
              ("0x%p   OnKeyEvent(), forgetting a pending GDK_KEY_PRESS event "
               "because GDK_KEY_RELEASE for the event is handled",
               this));
      mPostingKeyEvents.RemoveEvent(pendingKeyPressEvent);
    }
  }

  MOZ_LOG(
      gIMELog, LogLevel::Debug,
      ("0x%p   OnKeyEvent(), succeeded, filterThisEvent=%s "
       "(isFiltered=%s, mFallbackToKeyEvent=%s, "
       "probablyHandledAsynchronously=%s, maybeHandledAsynchronously=%s), "
       "mPostingKeyEvents.Length()=%zu, mCompositionState=%s, "
       "mMaybeInDeadKeySequence=%s, mKeyboardEventWasDispatched=%s, "
       "mKeyboardEventWasConsumed=%s",
       this, ToChar(filterThisEvent), ToChar(isFiltered),
       ToChar(mFallbackToKeyEvent), ToChar(probablyHandledAsynchronously),
       ToChar(maybeHandledAsynchronously), mPostingKeyEvents.Length(),
       GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence),
       ToChar(mKeyboardEventWasDispatched), ToChar(mKeyboardEventWasConsumed)));
  MOZ_LOG(gIMELog, LogLevel::Info, ("<<<<<<<<<<<<<<<<\n\n"));

  if (filterThisEvent) {
    return KeyHandlingState::eHandled;
  }
  // If another call of this method has already dispatched eKeyDown event,
  // we should return KeyHandlingState::eNotHandledButEventDispatched because
  // the caller should've stopped handling the event if preceding eKeyDown
  // event was consumed.
  if (aKeyboardEventWasDispatched) {
    return KeyHandlingState::eNotHandledButEventDispatched;
  }
  if (!mKeyboardEventWasDispatched) {
    return KeyHandlingState::eNotHandled;
  }
  return mKeyboardEventWasConsumed
             ? KeyHandlingState::eNotHandledButEventConsumed
             : KeyHandlingState::eNotHandledButEventDispatched;
}

void IMContextWrapper::OnFocusChangeInGecko(bool aFocus) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnFocusChangeInGecko(aFocus=%s),mCompositionState=%s, "
           "mIMEFocusState=%s, mSetInputPurposeAndInputHints=%s",
           this, ToChar(aFocus), GetCompositionStateName(),
           ToString(mIMEFocusState).c_str(),
           ToChar(mSetInputPurposeAndInputHints)));

  // We shouldn't carry over the removed string to another editor.
  mSelectedStringRemovedByComposition.Truncate();
  mContentSelection.reset();

  if (aFocus) {
    if (mSetInputPurposeAndInputHints) {
      mSetInputPurposeAndInputHints = false;
      SetInputPurposeAndInputHints();
    }
    NotifyIMEOfFocusChange(IMEFocusState::Focused);
  } else {
    NotifyIMEOfFocusChange(IMEFocusState::Blurred);
  }

  // When the focus changes, we need to inform IM about the new cursor
  // position. Chinese input methods generally rely on this because they
  // usually don't start composition until a character is picked.
  if (aFocus && EnsureToCacheContentSelection()) {
    SetCursorPosition(GetActiveContext());
  }
}

void IMContextWrapper::ResetIME() {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p ResetIME(), mCompositionState=%s, mIMEFocusState=%s", this,
           GetCompositionStateName(), ToString(mIMEFocusState).c_str()));

  GtkIMContext* activeContext = GetActiveContext();
  if (MOZ_UNLIKELY(!activeContext)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   ResetIME(), FAILED, there are no context", this));
    return;
  }

  RefPtr<IMContextWrapper> kungFuDeathGrip(this);
  RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);

  mPendingResettingIMContext = false;
  gtk_im_context_reset(activeContext);

  // The last focused window might have been destroyed by a DOM event handler
  // which was called by us during a call of gtk_im_context_reset().
  if (!lastFocusedWindow ||
      NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) ||
      lastFocusedWindow->Destroyed()) {
    return;
  }

  nsAutoString compositionString;
  GetCompositionString(activeContext, compositionString);

  MOZ_LOG(gIMELog, LogLevel::Debug,
          ("0x%p   ResetIME() called gtk_im_context_reset(), "
           "activeContext=0x%p, mCompositionState=%s, compositionString=%s, "
           "mIMEFocusState=%s",
           this, activeContext, GetCompositionStateName(),
           NS_ConvertUTF16toUTF8(compositionString).get(),
           ToString(mIMEFocusState).c_str()));

  // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still
  //     used in Japan!) sends only "preedit_changed" signal with empty
  //     composition string synchronously.  Therefore, if composition string
  //     is now empty string, we should assume that the IME won't send
  //     "commit" signal.
  if (IsComposing() && compositionString.IsEmpty()) {
    // WARNING: The widget might have been gone after this.
    DispatchCompositionCommitEvent(activeContext, &EmptyString());
  }
}

nsresult IMContextWrapper::EndIMEComposition(nsWindow* aCaller) {
  if (MOZ_UNLIKELY(IsDestroyed())) {
    return NS_OK;
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p EndIMEComposition(aCaller=0x%p), "
           "mCompositionState=%s",
           this, aCaller, GetCompositionStateName()));

  if (aCaller != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   EndIMEComposition(), FAILED, the caller isn't "
             "focused window, mLastFocusedWindow=0x%p",
             this, mLastFocusedWindow));
    return NS_OK;
  }

  if (!IsComposing()) {
    return NS_OK;
  }

  // Currently, GTK has API neither to commit nor to cancel composition
  // forcibly.  Therefore, TextComposition will recompute commit string for
  // the request even if native IME will cause unexpected commit string.
  // So, we don't need to emulate commit or cancel composition with
  // proper composition events.
  // XXX ResetIME() might not enough for finishing compositoin on some
  //     environments.  We should emulate focus change too because some IMEs
  //     may commit or cancel composition at blur.
  ResetIME();

  return NS_OK;
}

void IMContextWrapper::OnLayoutChange() {
  if (MOZ_UNLIKELY(IsDestroyed())) {
    return;
  }

  if (IsComposing()) {
    SetCursorPosition(GetActiveContext());
  } else {
    // If not composing, candidate window position is updated before key
    // down
    mSetCursorPositionOnKeyEvent = true;
  }
  mLayoutChanged = true;
}

void IMContextWrapper::OnUpdateComposition() {
  if (MOZ_UNLIKELY(IsDestroyed())) {
    return;
  }

  if (!IsComposing()) {
    // Composition has been committed.  So we need update selection for
    // caret later
    mContentSelection.reset();
    EnsureToCacheContentSelection();
    mSetCursorPositionOnKeyEvent = true;
  }

  // If we've already set candidate window position, we don't need to update
  // the position with update composition notification.
  if (!mLayoutChanged) {
    SetCursorPosition(GetActiveContext());
  }
}

void IMContextWrapper::SetInputContext(nsWindow* aCaller,
                                       const InputContext* aContext,
                                       const InputContextAction* aAction) {
  if (MOZ_UNLIKELY(IsDestroyed())) {
    return;
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ "
           "mEnabled=%s }, mHTMLInputType=%s })",
           this, aCaller, ToString(aContext->mIMEState.mEnabled).c_str(),
           NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));

  if (aCaller != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   SetInputContext(), FAILED, "
             "the caller isn't focused window, mLastFocusedWindow=0x%p",
             this, mLastFocusedWindow));
    return;
  }

  if (!mContext) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   SetInputContext(), FAILED, "
             "there are no context",
             this));
    return;
  }

  if (sLastFocusedContext != this) {
    mInputContext = *aContext;
    MOZ_LOG(gIMELog, LogLevel::Debug,
            ("0x%p   SetInputContext(), succeeded, "
             "but we're not active",
             this));
    return;
  }

  const bool changingEnabledState =
      aContext->IsInputAttributeChanged(mInputContext);

  // Release current IME focus if IME is enabled.
  if (changingEnabledState && mInputContext.mIMEState.IsEditable()) {
    if (IsComposing()) {
      EndIMEComposition(mLastFocusedWindow);
    }
    if (mIMEFocusState == IMEFocusState::Focused) {
      NotifyIMEOfFocusChange(IMEFocusState::BlurredWithoutFocusChange);
    }
  }

  mInputContext = *aContext;
  mSetInputPurposeAndInputHints = false;

  if (!changingEnabledState || !mInputContext.mIMEState.IsEditable()) {
    return;
  }

  // If the input context was temporarily disabled without a focus change,
  // it must be ready to query content even if the focused content is in
  // a remote process.  In this case, we should set IME focus right now.
  if (mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
    SetInputPurposeAndInputHints();
    NotifyIMEOfFocusChange(IMEFocusState::Focused);
    return;
  }

  // Otherwise, we cannot set input-purpose and input-hints right now because
  // setting them may require to set focus immediately for IME own's UI.
  // However, at this moment, `ContentCacheInParent` does not have content
  // cache, it'll be available after `NOTIFY_IME_OF_FOCUS` notification.
  // Therefore, we set them at receiving the notification.
  mSetInputPurposeAndInputHints = true;
}

void IMContextWrapper::SetInputPurposeAndInputHints() {
  GtkIMContext* currentContext = GetCurrentContext();
  if (!currentContext) {
    return;
  }

  GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM;
  const nsString& inputType = mInputContext.mHTMLInputType;
  // Password case has difficult issue.  Desktop IMEs disable composition if
  // input-purpose is password.  For disabling IME on |ime-mode: disabled;|, we
  // need to check mEnabled value instead of inputType value.  This hack also
  // enables composition on <input type="password" style="ime-mode: enabled;">.
  // This is right behavior of ime-mode on desktop.
  //
  // On the other hand, IME for tablet devices may provide a specific software
  // keyboard for password field.  If so, the behavior might look strange on
  // both:
  //   <input type="text" style="ime-mode: disabled;">
  //   <input type="password" style="ime-mode: enabled;">
  //
  // Temporarily, we should focus on desktop environment for now.  I.e., let's
  // ignore tablet devices for now.  When somebody reports actual trouble on
  // tablet devices, we should try to look for a way to solve actual problem.
  if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
    purpose = GTK_INPUT_PURPOSE_PASSWORD;
  } else if (inputType.EqualsLiteral("email")) {
    purpose = GTK_INPUT_PURPOSE_EMAIL;
  } else if (inputType.EqualsLiteral("url")) {
    purpose = GTK_INPUT_PURPOSE_URL;
  } else if (inputType.EqualsLiteral("tel")) {
    purpose = GTK_INPUT_PURPOSE_PHONE;
  } else if (inputType.EqualsLiteral("number")) {
    purpose = GTK_INPUT_PURPOSE_NUMBER;
  } else if (mInputContext.mHTMLInputMode.EqualsLiteral("decimal")) {
    purpose = GTK_INPUT_PURPOSE_NUMBER;
  } else if (mInputContext.mHTMLInputMode.EqualsLiteral("email")) {
    purpose = GTK_INPUT_PURPOSE_EMAIL;
  } else if (mInputContext.mHTMLInputMode.EqualsLiteral("numeric")) {
    purpose = GTK_INPUT_PURPOSE_DIGITS;
  } else if (mInputContext.mHTMLInputMode.EqualsLiteral("tel")) {
    purpose = GTK_INPUT_PURPOSE_PHONE;
  } else if (mInputContext.mHTMLInputMode.EqualsLiteral("url")) {
    purpose = GTK_INPUT_PURPOSE_URL;
  }
  // Search by type and inputmode isn't supported on GTK.

  g_object_set(currentContext, "input-purpose", purpose, nullptr);

  // Although GtkInputHints is enum type, value is bit field.
  gint hints = GTK_INPUT_HINT_NONE;
  if (mInputContext.mHTMLInputMode.EqualsLiteral("none")) {
    hints |= GTK_INPUT_HINT_INHIBIT_OSK;
  }

  if (mInputContext.mAutocapitalize.EqualsLiteral("characters")) {
    hints |= GTK_INPUT_HINT_UPPERCASE_CHARS;
  } else if (mInputContext.mAutocapitalize.EqualsLiteral("sentences")) {
    hints |= GTK_INPUT_HINT_UPPERCASE_SENTENCES;
  } else if (mInputContext.mAutocapitalize.EqualsLiteral("words")) {
    hints |= GTK_INPUT_HINT_UPPERCASE_WORDS;
  }

  g_object_set(currentContext, "input-hints", hints, nullptr);
}

InputContext IMContextWrapper::GetInputContext() {
  mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
  return mInputContext;
}

GtkIMContext* IMContextWrapper::GetCurrentContext() const {
  if (IsEnabled()) {
    return mContext;
  }
  if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
    return mSimpleContext;
  }
  return mDummyContext;
}

bool IMContextWrapper::IsValidContext(GtkIMContext* aContext) const {
  if (!aContext) {
    return false;
  }
  return aContext == mContext || aContext == mSimpleContext ||
         aContext == mDummyContext;
}

bool IMContextWrapper::IsEnabled() const {
  return mInputContext.mIMEState.mEnabled == IMEEnabled::Enabled ||
         (!sUseSimpleContext &&
          mInputContext.mIMEState.mEnabled == IMEEnabled::Password);
}

void IMContextWrapper::NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState) {
  MOZ_ASSERT_IF(aIMEFocusState == IMEFocusState::BlurredWithoutFocusChange,
                mIMEFocusState != IMEFocusState::Blurred);
  if (mIMEFocusState == aIMEFocusState) {
    return;
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p NotifyIMEOfFocusChange(aIMEFocusState=%s), mIMEFocusState=%s, "
           "sLastFocusedContext=0x%p",
           this, ToString(aIMEFocusState).c_str(),
           ToString(mIMEFocusState).c_str(), sLastFocusedContext));
  MOZ_ASSERT(!mSetInputPurposeAndInputHints);

  // If we've already made IME blurred at setting the input context disabled
  // and it's now completely blurred by a focus move, we need only to update
  // mIMEFocusState and when the input context gets enabled, we cannot set
  // IME focus immediately.
  if (aIMEFocusState == IMEFocusState::Blurred &&
      mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
    mIMEFocusState = IMEFocusState::Blurred;
    return;
  }

  auto Blur = [&](IMEFocusState aInternalState) {
    GtkIMContext* currentContext = GetCurrentContext();
    if (MOZ_UNLIKELY(!currentContext)) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   NotifyIMEOfFocusChange()::Blur(), FAILED, "
               "there is no context",
               this));
      return;
    }
    gtk_im_context_focus_out(currentContext);
    mIMEFocusState = aInternalState;
  };

  if (aIMEFocusState != IMEFocusState::Focused) {
    return Blur(aIMEFocusState);
  }

  GtkIMContext* currentContext = GetCurrentContext();
  if (MOZ_UNLIKELY(!currentContext)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   NotifyIMEOfFocusChange(), FAILED, "
             "there is no context",
             this));
    return;
  }

  if (sLastFocusedContext && sLastFocusedContext != this) {
    sLastFocusedContext->NotifyIMEOfFocusChange(IMEFocusState::Blurred);
  }

  sLastFocusedContext = this;

  // Forget all posted key events when focus is moved since they shouldn't
  // be fired in different editor.
  sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
  mPostingKeyEvents.Clear();

  gtk_im_context_focus_in(currentContext);
  mIMEFocusState = aIMEFocusState;
  mSetCursorPositionOnKeyEvent = true;

  if (!IsEnabled()) {
    // We should release IME focus for uim and scim.
    // These IMs are using snooper that is released at losing focus.
    Blur(IMEFocusState::BlurredWithoutFocusChange);
  }
}

void IMContextWrapper::OnSelectionChange(
    nsWindow* aCaller, const IMENotification& aIMENotification) {
  const bool isSelectionRangeChanged =
      mContentSelection.isNothing() ||
      !aIMENotification.mSelectionChangeData.EqualsRange(
          mContentSelection.ref());
  mContentSelection =
      Some(ContentSelection(aIMENotification.mSelectionChangeData));
  const bool retrievedSurroundingSignalReceived =
      mRetrieveSurroundingSignalReceived;
  mRetrieveSurroundingSignalReceived = false;

  if (MOZ_UNLIKELY(IsDestroyed())) {
    return;
  }

  const IMENotification::SelectionChangeDataBase& selectionChangeData =
      aIMENotification.mSelectionChangeData;

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
           "mSelectionChangeData=%s }), "
           "mCompositionState=%s, mIsDeletingSurrounding=%s, "
           "mRetrieveSurroundingSignalReceived=%s, isSelectionRangeChanged=%s",
           this, aCaller, ToString(selectionChangeData).c_str(),
           GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
           ToChar(retrievedSurroundingSignalReceived),
           ToChar(isSelectionRangeChanged)));

  if (aCaller != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnSelectionChange(), FAILED, "
             "the caller isn't focused window, mLastFocusedWindow=0x%p",
             this, mLastFocusedWindow));
    return;
  }

  if (!IsComposing()) {
    // Now we have no composition (mostly situation on calling this method)
    // If we have it, it will set by
    // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED.
    mSetCursorPositionOnKeyEvent = true;
  }

  // The focused editor might have placeholder text with normal text node.
  // In such case, the text node must be removed from a compositionstart
  // event handler.  So, we're dispatching eCompositionStart,
  // we should ignore selection change notification.
  if (mCompositionState == eCompositionState_CompositionStartDispatched) {
    if (NS_WARN_IF(mContentSelection.isNothing())) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   OnSelectionChange(), FAILED, "
               "new offset is too large, cannot keep composing",
               this));
    } else if (mContentSelection->HasRange()) {
      // Modify the selection start offset with new offset.
      mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
      // XXX We should modify mSelectedStringRemovedByComposition?
      // But how?
      MOZ_LOG(gIMELog, LogLevel::Debug,
              ("0x%p   OnSelectionChange(), ignored, mCompositionStart "
               "is updated to %u, the selection change doesn't cause "
               "resetting IM context",
               this, mCompositionStart));
      // And don't reset the IM context.
      return;
    } else {
      MOZ_LOG(
          gIMELog, LogLevel::Debug,
          ("0x%p   OnSelectionChange(), ignored, because of no selection range",
           this));
      return;
    }
    // Otherwise, reset the IM context due to impossible to keep composing.
  }

  // If the selection change is caused by deleting surrounding text,
  // we shouldn't need to notify IME of selection change.
  if (mIsDeletingSurrounding) {
    return;
  }

  bool occurredBeforeComposition =
      IsComposing() && !selectionChangeData.mOccurredDuringComposition &&
      !selectionChangeData.mCausedByComposition;
  if (occurredBeforeComposition) {
    mPendingResettingIMContext = true;
  }

  // When the selection change is caused by dispatching composition event,
  // selection set event and/or occurred before starting current composition,
  // we shouldn't notify IME of that and commit existing composition.
  // Don't do this even if selection is not changed actually.  For example,
  // fcitx has direct input mode which does not insert composing string, but
  // inserts commited text for each key sequence (i.e., there is "invisible"
  // composition string).  In the world after bug 1712269, we don't use a
  // set of composition events for this kind of IME.  Therefore,
  // SelectionChangeData.mCausedByComposition is not expected value for here
  // if this call is caused by a preceding commit.  And if the preceding commit
  // is triggered by a key type for next word, resetting IME state makes fcitx
  // discard the pending input for the next word.  Thus, we need to check
  // whether the selection range is actually changed here.
  if (!selectionChangeData.mCausedByComposition &&
      !selectionChangeData.mCausedBySelectionEvent && isSelectionRangeChanged &&
      !occurredBeforeComposition) {
    // Hack for ibus-pinyin.  ibus-pinyin will synthesize a set of
    // composition which commits with empty string after calling
    // gtk_im_context_reset().  Therefore, selecting text causes
    // unexpectedly removing it.  For preventing it but not breaking the
    // other IMEs which use surrounding text, we should call it only when
    // surrounding text has been retrieved after last selection range was
    // set.  If it's not retrieved, that means that current IME doesn't
    // have any content cache, so, it must not need the notification of
    // selection change.
    if (IsComposing() || retrievedSurroundingSignalReceived) {
      ResetIME();
    }
  }
}

/* static */
void IMContextWrapper::OnThemeChanged() {
  if (auto* provider = SelectionStyleProvider::GetExistingInstance()) {
    provider->OnThemeChanged();
  }
}

/* static */
void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
                                                  IMContextWrapper* aModule) {
  aModule->OnStartCompositionNative(aContext);
}

void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) {
  // IME may synthesize composition asynchronously after filtering a
  // GDK_KEY_PRESS event.  In that case, we should handle composition with
  // emulating the usual case, i.e., this is called in the stack of
  // OnKeyEvent().
  Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
  if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
    GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
    if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
        KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
            KEY_NAME_INDEX_USE_STRING) {
      maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
      mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
    }
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnStartCompositionNative(aContext=0x%p), "
           "current context=0x%p, mComposingContext=0x%p",
           this, aContext, GetCurrentContext(), mComposingContext));

  // See bug 472635, we should do nothing if IM context doesn't match.
  if (GetCurrentContext() != aContext) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnStartCompositionNative(), FAILED, "
             "given context doesn't match",
             this));
    return;
  }

  if (mComposingContext && aContext != mComposingContext) {
    // XXX For now, we should ignore this odd case, just logging.
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p   OnStartCompositionNative(), Warning, "
             "there is already a composing context but starting new "
             "composition with different context",
             this));
  }

  // IME may start composition without "preedit_start" signal.  Therefore,
  // mComposingContext will be initialized in DispatchCompositionStart().

  if (!DispatchCompositionStart(aContext)) {
    return;
  }
  mCompositionTargetRange.mOffset = mCompositionStart;
  mCompositionTargetRange.mLength = 0;
}

/* static */
void IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext,
                                                IMContextWrapper* aModule) {
  aModule->OnEndCompositionNative(aContext);
}

void IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnEndCompositionNative(aContext=0x%p), mComposingContext=0x%p",
           this, aContext, mComposingContext));

  // See bug 472635, we should do nothing if IM context doesn't match.
  // Note that if this is called after focus move, the context may different
  // from any our owning context.
  if (!IsValidContext(aContext)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p    OnEndCompositionNative(), FAILED, "
             "given context doesn't match with any context",
             this));
    return;
  }

  // If we've not started composition with aContext, we should ignore it.
  if (aContext != mComposingContext) {
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p    OnEndCompositionNative(), Warning, "
             "given context doesn't match with mComposingContext",
             this));
    return;
  }

  g_object_unref(mComposingContext);
  mComposingContext = nullptr;

  // If we already handled the commit event, we should do nothing here.
  if (IsComposing()) {
    if (!DispatchCompositionCommitEvent(aContext)) {
      // If the widget is destroyed, we should do nothing anymore.
      return;
    }
  }

  if (mPendingResettingIMContext) {
    ResetIME();
  }
}

/* static */
void IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext,
                                                   IMContextWrapper* aModule) {
  RefPtr module = aModule;
  module->OnChangeCompositionNative(aContext);

  if (module->IsDestroyed()) {
    // A strong reference is already held during "preedit-changed" emission,
    // but _ibus_context_destroy_cb() in ibus 1.5.28 and
    // _fcitx_im_context_close_im_cb() in fcitx 4.2.9.9 want their
    // GtkIMContexts to live a little longer.  See bug 1824634.
    NS_DispatchToMainThread(
        NS_NewRunnableFunction(__func__, [context = RefPtr{aContext}]() {}));
  }
}

void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) {
  // IME may synthesize composition asynchronously after filtering a
  // GDK_KEY_PRESS event.  In that case, we should handle composition with
  // emulating the usual case, i.e., this is called in the stack of
  // OnKeyEvent().
  Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
  if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
    GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
    if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
        KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
            KEY_NAME_INDEX_USE_STRING) {
      maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
      mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
    }
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnChangeCompositionNative(aContext=0x%p), "
           "mComposingContext=0x%p",
           this, aContext, mComposingContext));

  // See bug 472635, we should do nothing if IM context doesn't match.
  // Note that if this is called after focus move, the context may different
  // from any our owning context.
  if (!IsValidContext(aContext)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnChangeCompositionNative(), FAILED, "
             "given context doesn't match with any context",
             this));
    return;
  }

  if (mComposingContext && aContext != mComposingContext) {
    // XXX For now, we should ignore this odd case, just logging.
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p   OnChangeCompositionNative(), Warning, "
             "given context doesn't match with composing context",
             this));
  }

  nsAutoString compositionString;
  GetCompositionString(aContext, compositionString);
  if (!IsComposing() && compositionString.IsEmpty()) {
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p   OnChangeCompositionNative(), Warning, does nothing "
             "because has not started composition and composing string is "
             "empty",
             this));
    mDispatchedCompositionString.Truncate();
    return;  // Don't start the composition with empty string.
  }

  // Be aware, widget can be gone
  DispatchCompositionChangeEvent(aContext, compositionString);
}

/* static */
gboolean IMContextWrapper::OnRetrieveSurroundingCallback(
    GtkIMContext* aContext, IMContextWrapper* aModule) {
  return aModule->OnRetrieveSurroundingNative(aContext);
}

gboolean IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), "
           "current context=0x%p",
           this, aContext, GetCurrentContext()));

  // See bug 472635, we should do nothing if IM context doesn't match.
  if (GetCurrentContext() != aContext) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnRetrieveSurroundingNative(), FAILED, "
             "given context doesn't match",
             this));
    return FALSE;
  }

  nsAutoString uniStr;
  uint32_t cursorPos;
  if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) {
    return FALSE;
  }

  // Despite taking a pointer and a length, IBus wants the string to be
  // zero-terminated and doesn't like U+0000 within the string.
  uniStr.ReplaceChar(char16_t(0), char16_t(0xFFFD));

  NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
  uint32_t cursorPosInUTF8 = utf8Str.Length();
  AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
  gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
                                 cursorPosInUTF8);
  mRetrieveSurroundingSignalReceived = true;
  return TRUE;
}

/* static */
gboolean IMContextWrapper::OnDeleteSurroundingCallback(
    GtkIMContext* aContext, gint aOffset, gint aNChars,
    IMContextWrapper* aModule) {
  return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
}

gboolean IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext,
                                                     gint aOffset,
                                                     gint aNChars) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, "
           "aNChar=%d), current context=0x%p",
           this, aContext, aOffset, aNChars, GetCurrentContext()));

  // See bug 472635, we should do nothing if IM context doesn't match.
  if (GetCurrentContext() != aContext) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnDeleteSurroundingNative(), FAILED, "
             "given context doesn't match",
             this));
    return FALSE;
  }

  AutoRestore<bool> saveDeletingSurrounding(mIsDeletingSurrounding);
  mIsDeletingSurrounding = true;
  if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) {
    return TRUE;
  }

  // failed
  MOZ_LOG(gIMELog, LogLevel::Error,
          ("0x%p   OnDeleteSurroundingNative(), FAILED, "
           "cannot delete text",
           this));
  return FALSE;
}

/* static */
void IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext,
                                                   const gchar* aString,
                                                   IMContextWrapper* aModule) {
  aModule->OnCommitCompositionNative(aContext, aString);
}

void IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext,
                                                 const gchar* aUTF8Char) {
  const gchar emptyStr = 0;
  const gchar* commitString = aUTF8Char ? aUTF8Char : &emptyStr;
  NS_ConvertUTF8toUTF16 utf16CommitString(commitString);

  // IME may synthesize composition asynchronously after filtering a
  // GDK_KEY_PRESS event.  In that case, we should handle composition with
  // emulating the usual case, i.e., this is called in the stack of
  // OnKeyEvent().
  Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
  if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
    GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
    if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
        KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
            KEY_NAME_INDEX_USE_STRING) {
      maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
      mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
    }
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p OnCommitCompositionNative(aContext=0x%p), "
           "current context=0x%p, active context=0x%p, commitString=\"%s\", "
           "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s",
           this, aContext, GetCurrentContext(), GetActiveContext(),
           commitString, mProcessingKeyEvent, ToChar(IsComposingOn(aContext))));

  // See bug 472635, we should do nothing if IM context doesn't match.
  if (!IsValidContext(aContext)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   OnCommitCompositionNative(), FAILED, "
             "given context doesn't match",
             this));
    return;
  }

  // If we are not in composition and committing with empty string,
  // we need to do nothing because if we continued to handle this
  // signal, we would dispatch compositionstart, text, compositionend
  // events with empty string.  Of course, they are unnecessary events
  // for Web applications and our editor.
  if (!IsComposingOn(aContext) && utf16CommitString.IsEmpty()) {
    MOZ_LOG(gIMELog, 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,
  // 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 &&
      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 committing string is exactly same as a character which is
    // produced by the key, eKeyDown and eKeyPress event should be
    // dispatched by the caller of OnKeyEvent() normally.  Note that
    // mMaybeInDeadKeySequence will be set to false by OnKeyEvent()
    // since we set mFallbackToKeyEvent to true here.
    if (!strcmp(commitString, keyval_utf8)) {
      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p   OnCommitCompositionNative(), "
               "we'll send normal key event",
               this));
      mFallbackToKeyEvent = true;
      return;
    }

    // If we're in a dead key sequence, commit string is a character in
    // the BMP and mProcessingKeyEvent produces some characters but it's
    // not same as committing string, we should dispatch an eKeyPress
    // event from here.
    WidgetKeyboardEvent keyDownEvent(true, eKeyDown, mLastFocusedWindow);
    KeymapWrapper::InitKeyEvent(keyDownEvent, mProcessingKeyEvent, false);
    if (mMaybeInDeadKeySequence && utf16CommitString.Length() == 1 &&
        keyDownEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
      mKeyboardEventWasDispatched = true;
      // Anyway, we're not in dead key sequence anymore.
      mMaybeInDeadKeySequence = false;

      RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
      nsresult rv = dispatcher->BeginNativeInputTransaction();
      if (NS_WARN_IF(NS_FAILED(rv))) {
        MOZ_LOG(gIMELog, LogLevel::Error,
                ("0x%p   OnCommitCompositionNative(), FAILED, "
                 "due to BeginNativeInputTransaction() failure",
                 this));
        return;
      }

      // First, dispatch eKeyDown event.
      keyDownEvent.mKeyValue = utf16CommitString;
      nsEventStatus status = nsEventStatus_eIgnore;
      bool dispatched = dispatcher->DispatchKeyboardEvent(
          eKeyDown, keyDownEvent, status, mProcessingKeyEvent);
      if (!dispatched || status == nsEventStatus_eConsumeNoDefault) {
        mKeyboardEventWasConsumed = true;
        MOZ_LOG(gIMELog, LogLevel::Info,
                ("0x%p   OnCommitCompositionNative(), "
                 "doesn't dispatch eKeyPress event because the preceding "
                 "eKeyDown event was not dispatched or was consumed",
                 this));
        return;
      }
      if (mLastFocusedWindow != keyDownEvent.mWidget ||
          mLastFocusedWindow->Destroyed()) {
        MOZ_LOG(gIMELog, LogLevel::Warning,
                ("0x%p   OnCommitCompositionNative(), Warning, "
                 "stop dispatching eKeyPress event because the preceding "
                 "eKeyDown event caused changing focused widget or "
                 "destroyed",
                 this));
        return;
      }
      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p   OnCommitCompositionNative(), "
               "dispatched eKeyDown event for the committed character",
               this));

      // Next, dispatch eKeyPress event.
      dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status,
                                              mProcessingKeyEvent);
      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p   OnCommitCompositionNative(), "
               "dispatched eKeyPress event for the committed character",
               this));
      return;
    }
  }

  NS_ConvertUTF8toUTF16 str(commitString);
  // Be aware, widget can be gone
  DispatchCompositionCommitEvent(aContext, &str);
}

void IMContextWrapper::GetCompositionString(GtkIMContext* aContext,
                                            nsAString& aCompositionString) {
  gchar* preedit_string;
  gint cursor_pos;
  PangoAttrList* feedback_list;
  gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
                                    &cursor_pos);
  if (preedit_string && *preedit_string) {
    CopyUTF8toUTF16(MakeStringSpan(preedit_string), aCompositionString);
  } else {
    aCompositionString.Truncate();
  }

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p GetCompositionString(aContext=0x%p), "
           "aCompositionString=\"%s\"",
           this, aContext, preedit_string));

  pango_attr_list_unref(feedback_list);
  g_free(preedit_string);
}

bool IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME(
    EventMessage aFollowingEvent) {
  if (!mLastFocusedWindow) {
    return false;
  }

  if (!mIsKeySnooped &&
      ((!mProcessingKeyEvent && mPostingKeyEvents.IsEmpty()) ||
       (mProcessingKeyEvent && mKeyboardEventWasDispatched))) {
    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);

  if (mProcessingKeyEvent || !mPostingKeyEvents.IsEmpty()) {
    if (mProcessingKeyEvent) {
      mKeyboardEventWasDispatched = true;
    }
    // If we're not handling a key event synchronously, the signal may be
    // sent by IME without sending key event to us.  In such case, we
    // should dispatch keyboard event for the last key event which was
    // posted to other IME process.
    GdkEventKey* sourceEvent = mProcessingKeyEvent
                                   ? mProcessingKeyEvent
                                   : mPostingKeyEvents.GetFirstEvent();

    MOZ_LOG(
        gIMELog, LogLevel::Info,
        ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
         "aFollowingEvent=%s), dispatch %s %s "
         "event: { type=%s, keyval=%s, unicode=0x%X, state=%s, "
         "time=%u, hardware_keycode=%u, group=%u }",
         this, ToChar(aFollowingEvent),
         ToChar(sourceEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp),
         mProcessingKeyEvent ? "processing" : "posted",
         GetEventType(sourceEvent), gdk_keyval_name(sourceEvent->keyval),
         gdk_keyval_to_unicode(sourceEvent->keyval),
         GetEventStateName(sourceEvent->state, mIMContextID).get(),
         sourceEvent->time, sourceEvent->hardware_keycode, sourceEvent->group));

    // Let's dispatch eKeyDown event or eKeyUp event now.  Note that only
    // when we're not in a dead key composition, we should mark the
    // eKeyDown and eKeyUp event as "processed by IME" since we should
    // expose raw keyCode and key value to web apps the key event is a
    // part of a dead key sequence.
    // 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
    KeymapWrapper::DispatchKeyDownOrKeyUpEvent(lastFocusedWindow, sourceEvent,
                                               !mMaybeInDeadKeySequence,
                                               &mKeyboardEventWasConsumed);
    MOZ_LOG(gIMELog, LogLevel::Info,
            ("0x%p   MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup "
             "event is dispatched",
             this));

    if (!mProcessingKeyEvent) {
      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p   MaybeDispatchKeyEventAsProcessedByIME(), removing first "
               "event from the queue",
               this));
      mPostingKeyEvents.RemoveEvent(sourceEvent);
    }
  } else {
    MOZ_ASSERT(mIsKeySnooped);
    // Currently, we support key snooper mode of uim and wayland only.
    MOZ_ASSERT(mIMContextID == IMContextID::Uim ||
               mIMContextID == IMContextID::Wayland);
    // uim sends "preedit_start" signal and "preedit_changed" separately
    // at starting composition, "commit" and "preedit_end" separately at
    // committing composition.

    // Currently, we should dispatch only fake eKeyDown event because
    // we cannot decide which is the last signal of each key operation
    // and Chromium also dispatches only "keydown" event in this case.
    bool dispatchFakeKeyDown = false;
    switch (aFollowingEvent) {
      case eCompositionStart:
      case eCompositionCommit:
      case eCompositionCommitAsIs:
      case eContentCommandInsertText:
        dispatchFakeKeyDown = true;
        break;
      // XXX Unfortunately, I don't have a good idea to prevent to
      //     dispatch redundant eKeyDown event for eCompositionStart
      //     immediately after "delete_surrounding" signal.  However,
      //     not dispatching eKeyDown event is worse than dispatching
      //     redundant eKeyDown events.
      case eContentCommandDelete:
        dispatchFakeKeyDown = true;
        break;
      // We need to prevent to dispatch redundant eKeyDown event for
      // eCompositionChange immediately after eCompositionStart.  So,
      // We should not dispatch eKeyDown event if dispatched composition
      // string is still empty string.
      case eCompositionChange:
        dispatchFakeKeyDown = !mDispatchedCompositionString.IsEmpty();
        break;
      default:
        MOZ_ASSERT_UNREACHABLE("Do you forget to handle the case?");
        break;
    }

    if (dispatchFakeKeyDown) {
      WidgetKeyboardEvent fakeKeyDownEvent(true, eKeyDown, lastFocusedWindow);
      fakeKeyDownEvent.mKeyCode = NS_VK_PROCESSKEY;
      fakeKeyDownEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
      // It's impossible to get physical key information in this case but
      // this should be okay since web apps shouldn't do anything with
      // physical key information during composition.
      fakeKeyDownEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN;

      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
               "aFollowingEvent=%s), dispatch fake eKeyDown event",
               this, ToChar(aFollowingEvent)));

      KeymapWrapper::DispatchKeyDownOrKeyUpEvent(
          lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed);
      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p   MaybeDispatchKeyEventAsProcessedByIME(), "
               "fake keydown event is dispatched",
               this));
    }
  }

  if (lastFocusedWindow->IsDestroyed() ||
      lastFocusedWindow != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, 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(gIMELog, 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(gIMELog, LogLevel::Info,
          ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext));

  if (IsComposing()) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionStart(), FAILED, "
             "we're already in composition",
             this));
    return true;
  }

  if (!mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionStart(), FAILED, "
             "there are no focused window in this module",
             this));
    return false;
  }

  if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionStart(), FAILED, "
             "cannot query the selection offset",
             this));
    return false;
  }

  if (NS_WARN_IF(!mContentSelection->HasRange())) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionStart(), FAILED, "
             "due to no selection",
             this));
    return false;
  }

  mComposingContext = static_cast<GtkIMContext*>(g_object_ref(aContext));
  MOZ_ASSERT(mComposingContext);

  // Keep the last focused window alive
  RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);

  // 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 = mContentSelection->OffsetAndDataRef().StartOffset();
  mDispatchedCompositionString.Truncate();

  // 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(eCompositionStart)) {
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p   DispatchCompositionStart(), Warning, "
             "MaybeDispatchKeyEventAsProcessedByIME() returned false",
             this));
    return false;
  }

  RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
  nsresult rv = dispatcher->BeginNativeInputTransaction();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionStart(), FAILED, "
             "due to BeginNativeInputTransaction() failure",
             this));
    return false;
  }

  static bool sHasSetTelemetry = false;
  if (!sHasSetTelemetry) {
    sHasSetTelemetry = true;
    NS_ConvertUTF8toUTF16 im(GetIMName());
    // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
    if (im.Length() > 72) {
      if (NS_IS_SURROGATE_PAIR(im[72 - 2], im[72 - 1])) {
        im.Truncate(72 - 2);
      } else {
        im.Truncate(72 - 1);
      }
      // U+2026 is "..."
      im.Append(char16_t(0x2026));
    }
    Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_LINUX, im,
                         true);
  }

  MOZ_LOG(gIMELog, LogLevel::Debug,
          ("0x%p   DispatchCompositionStart(), dispatching "
           "compositionstart... (mCompositionStart=%u)",
           this, mCompositionStart));
  mCompositionState = eCompositionState_CompositionStartDispatched;
  nsEventStatus status;
  dispatcher->StartComposition(status);
  if (lastFocusedWindow->IsDestroyed() ||
      lastFocusedWindow != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionStart(), FAILED, the focused "
             "widget was destroyed/changed by compositionstart event",
             this));
    return false;
  }

  return true;
}

bool IMContextWrapper::DispatchCompositionChangeEvent(
    GtkIMContext* aContext, const nsAString& aCompositionString) {
  MOZ_LOG(
      gIMELog, LogLevel::Info,
      ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext));

  if (!mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionChangeEvent(), FAILED, "
             "there are no focused window in this module",
             this));
    return false;
  }

  if (!IsComposing()) {
    MOZ_LOG(gIMELog, 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(eCompositionChange)) {
    MOZ_LOG(gIMELog, 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(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionChangeEvent(), FAILED, "
             "due to BeginNativeInputTransaction() failure",
             this));
    return false;
  }

  // Store the selected string which will be removed by following
  // compositionchange event.
  if (mCompositionState == eCompositionState_CompositionStartDispatched) {
    if (NS_WARN_IF(!EnsureToCacheContentSelection(
            &mSelectedStringRemovedByComposition))) {
      // XXX How should we behave in this case??
    } else if (mContentSelection->HasRange()) {
      // XXX We should assume, for now, any web applications don't change
      //     selection at handling this compositionchange event.
      mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
    } else {
      // If there is no selection range, we should keep previously storing
      // mCompositionStart.
    }
  }

  RefPtr<TextRangeArray> rangeArray =
      CreateTextRangeArray(aContext, aCompositionString);

  rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionChangeEvent(), FAILED, "
             "due to SetPendingComposition() failure",
             this));
    return false;
  }

  mCompositionState = eCompositionState_CompositionChangeEventDispatched;

  // We cannot call SetCursorPosition for e10s-aware.
  // DispatchEvent is async on e10s, so composition rect isn't updated now
  // on tab parent.
  mDispatchedCompositionString = aCompositionString;
  mLayoutChanged = false;
  mCompositionTargetRange.mOffset =
      mCompositionStart + rangeArray->TargetClauseOffset();
  mCompositionTargetRange.mLength = rangeArray->TargetClauseLength();

  RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
  nsEventStatus status;
  rv = dispatcher->FlushPendingComposition(status);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionChangeEvent(), FAILED, "
             "due to FlushPendingComposition() failure",
             this));
    return false;
  }

  if (lastFocusedWindow->IsDestroyed() ||
      lastFocusedWindow != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionChangeEvent(), FAILED, the "
             "focused widget was destroyed/changed by "
             "compositionchange event",
             this));
    return false;
  }
  return true;
}

bool IMContextWrapper::DispatchCompositionCommitEvent(
    GtkIMContext* aContext, const nsAString* aCommitString) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, "
           "aCommitString=0x%p, (\"%s\"))",
           this, aContext, aCommitString,
           aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));

  if (!mLastFocusedWindow) {
    MOZ_LOG(gIMELog, 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()?)
  RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
  RefPtr<TextEventDispatcher> dispatcher;
  if (!IsComposing() &&
      !StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
    if (!aCommitString || aCommitString->IsEmpty()) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   DispatchCompositionCommitEvent(), FAILED, "
               "did nothing due to inserting empty string without composition",
               this));
      return true;
    }
    if (MOZ_UNLIKELY(!EnsureToCacheContentSelection())) {
      MOZ_LOG(gIMELog, LogLevel::Warning,
              ("0x%p   DispatchCompositionCommitEvent(), Warning, "
               "Failed to cache selection before dispatching "
               "eContentCommandInsertText event",
               this));
    }
    if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandInsertText)) {
      MOZ_LOG(gIMELog, LogLevel::Warning,
              ("0x%p   DispatchCompositionCommitEvent(), Warning, "
               "MaybeDispatchKeyEventAsProcessedByIME() returned false",
               this));
      return false;
    }
    // Emulate selection until receiving actual selection range.  This is
    // important for OnSelectionChange.  If selection is not changed by web
    // apps, i.e., selection range is same as what selection expects, we
    // shouldn't reset IME because the trigger of causing this commit may be an
    // input for next composition and we shouldn't cancel it.
    if (mContentSelection.isSome()) {
      mContentSelection->Collapse(
          (mContentSelection->HasRange()
               ? mContentSelection->OffsetAndDataRef().StartOffset()
               : mCompositionStart) +
          aCommitString->Length());
      MOZ_LOG(gIMELog, LogLevel::Info,
              ("0x%p   DispatchCompositionCommitEvent(), mContentSelection=%s",
               this, ToString(mContentSelection).c_str()));
    }
    MOZ_ASSERT(!dispatcher);
  } else {
    if (!IsComposing()) {
      if (!aCommitString || aCommitString->IsEmpty()) {
        MOZ_LOG(gIMELog, LogLevel::Error,
                ("0x%p   DispatchCompositionCommitEvent(), FAILED, "
                 "there is no composition and empty commit string",
                 this));
        return true;
      }
      MOZ_LOG(gIMELog, 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(
                 aCommitString ? eCompositionCommit : eCompositionCommitAsIs)) {
      MOZ_LOG(gIMELog, LogLevel::Warning,
              ("0x%p   DispatchCompositionCommitEvent(), Warning, "
               "MaybeDispatchKeyEventAsProcessedByIME() returned false",
               this));
      mCompositionState = eCompositionState_NotComposing;
      return false;
    }

    dispatcher = GetTextEventDispatcher();
    MOZ_ASSERT(dispatcher);
    nsresult rv = dispatcher->BeginNativeInputTransaction();
    if (NS_WARN_IF(NS_FAILED(rv))) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   DispatchCompositionCommitEvent(), FAILED, "
               "due to BeginNativeInputTransaction() failure",
               this));
      return false;
    }

    // Emulate selection until receiving actual selection range.
    const uint32_t offsetToPutCaret =
        mCompositionStart + (aCommitString
                                 ? aCommitString->Length()
                                 : mDispatchedCompositionString.Length());
    if (mContentSelection.isSome()) {
      mContentSelection->Collapse(offsetToPutCaret);
    } else {
      // TODO: We should guarantee that there should be at least fake selection
      //       for IME at here.  Then, we can keep the last writing mode.
      mContentSelection.emplace(offsetToPutCaret, WritingMode());
    }
  }

  mCompositionState = eCompositionState_NotComposing;
  // Reset dead key sequence too because GTK doesn't support dead key chain
  // (i.e., a key press doesn't cause both producing some characters and
  // restarting new dead key sequence at one time).  So, committing
  // composition means end of a dead key sequence.
  mMaybeInDeadKeySequence = false;
  mCompositionStart = UINT32_MAX;
  mCompositionTargetRange.Clear();
  mDispatchedCompositionString.Truncate();
  mSelectedStringRemovedByComposition.Truncate();

  if (!dispatcher) {
    MOZ_ASSERT(aCommitString);
    MOZ_ASSERT(!aCommitString->IsEmpty());
    nsEventStatus status = nsEventStatus_eIgnore;
    WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText,
                                              lastFocusedWindow);
    insertTextEvent.mString.emplace(*aCommitString);
    lastFocusedWindow->DispatchEvent(&insertTextEvent, status);

    if (!insertTextEvent.mSucceeded) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   DispatchCompositionChangeEvent(), FAILED, inserting "
               "text failed",
               this));
      return false;
    }
  } else {
    nsEventStatus status = nsEventStatus_eIgnore;
    nsresult rv = dispatcher->CommitComposition(status, aCommitString);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   DispatchCompositionChangeEvent(), FAILED, "
               "due to CommitComposition() failure",
               this));
      return false;
    }
  }

  if (lastFocusedWindow->IsDestroyed() ||
      lastFocusedWindow != mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DispatchCompositionCommitEvent(), FAILED, "
             "the focused widget was destroyed/changed by "
             "compositioncommit event",
             this));
    return false;
  }

  return true;
}

already_AddRefed<TextRangeArray> IMContextWrapper::CreateTextRangeArray(
    GtkIMContext* aContext, const nsAString& aCompositionString) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p CreateTextRangeArray(aContext=0x%p, "
           "aCompositionString=\"%s\" (Length()=%zu))",
           this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(),
           aCompositionString.Length()));

  RefPtr<TextRangeArray> textRangeArray = new TextRangeArray();

  gchar* preedit_string;
  gint cursor_pos_in_chars;
  PangoAttrList* feedback_list;
  gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
                                    &cursor_pos_in_chars);
  if (!preedit_string || !*preedit_string) {
    if (!aCompositionString.IsEmpty()) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   CreateTextRangeArray(), FAILED, due to "
               "preedit_string is null",
               this));
    }
    pango_attr_list_unref(feedback_list);
    g_free(preedit_string);
    return textRangeArray.forget();
  }

  // Convert caret offset from offset in characters to offset in UTF-16
  // string.  If we couldn't proper offset in UTF-16 string, we should
  // assume that the caret is at the end of the composition string.
  uint32_t caretOffsetInUTF16 = aCompositionString.Length();
  if (NS_WARN_IF(cursor_pos_in_chars < 0)) {
    // Note that this case is undocumented.  We should assume that the
    // caret is at the end of the composition string.
  } else if (cursor_pos_in_chars == 0) {
    caretOffsetInUTF16 = 0;
  } else {
    gchar* charAfterCaret =
        g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars);
    if (NS_WARN_IF(!charAfterCaret)) {
      MOZ_LOG(gIMELog, LogLevel::Warning,
              ("0x%p   CreateTextRangeArray(), failed to get UTF-8 "
               "string before the caret (cursor_pos_in_chars=%d)",
               this, cursor_pos_in_chars));
    } else {
      glong caretOffset = 0;
      gunichar2* utf16StrBeforeCaret =
          g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string,
                          nullptr, &caretOffset, nullptr);
      if (NS_WARN_IF(!utf16StrBeforeCaret) || NS_WARN_IF(caretOffset < 0)) {
        MOZ_LOG(gIMELog, LogLevel::Warning,
                ("0x%p   CreateTextRangeArray(), WARNING, failed to "
                 "convert to UTF-16 string before the caret "
                 "(cursor_pos_in_chars=%d, caretOffset=%ld)",
                 this, cursor_pos_in_chars, caretOffset));
      } else {
        caretOffsetInUTF16 = static_cast<uint32_t>(caretOffset);
        uint32_t compositionStringLength = aCompositionString.Length();
        if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) {
          MOZ_LOG(gIMELog, LogLevel::Warning,
                  ("0x%p   CreateTextRangeArray(), WARNING, "
                   "caretOffsetInUTF16=%u is larger than "
                   "compositionStringLength=%u",
                   this, caretOffsetInUTF16, compositionStringLength));
          caretOffsetInUTF16 = compositionStringLength;
        }
      }
      if (utf16StrBeforeCaret) {
        g_free(utf16StrBeforeCaret);
      }
    }
  }

  PangoAttrIterator* iter;
  iter = pango_attr_list_get_iterator(feedback_list);
  if (!iter) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   CreateTextRangeArray(), FAILED, iterator couldn't "
             "be allocated",
             this));
    pango_attr_list_unref(feedback_list);
    g_free(preedit_string);
    return textRangeArray.forget();
  }

  uint32_t minOffsetOfClauses = aCompositionString.Length();
  uint32_t maxOffsetOfClauses = 0;
  do {
    TextRange range;
    if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) {
      continue;
    }
    MOZ_ASSERT(range.Length());
    minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset);
    maxOffsetOfClauses = std::max(maxOffsetOfClauses, range.mEndOffset);
    textRangeArray->AppendElement(range);
  } while (pango_attr_iterator_next(iter));

  // If the IME doesn't define clause from the start of the composition,
  // we should insert dummy clause information since TextRangeArray assumes
  // that there must be a clause whose start is 0 when there is one or
  // more clauses.
  if (minOffsetOfClauses) {
    TextRange dummyClause;
    dummyClause.mStartOffset = 0;
    dummyClause.mEndOffset = minOffsetOfClauses;
    dummyClause.mRangeType = TextRangeType::eRawClause;
    textRangeArray->InsertElementAt(0, dummyClause);
    maxOffsetOfClauses = std::max(maxOffsetOfClauses, dummyClause.mEndOffset);
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p   CreateTextRangeArray(), inserting a dummy clause "
             "at the beginning of the composition string mStartOffset=%u, "
             "mEndOffset=%u, mRangeType=%s",
             this, dummyClause.mStartOffset, dummyClause.mEndOffset,
             ToChar(dummyClause.mRangeType)));
  }

  // If the IME doesn't define clause at end of the composition, we should
  // insert dummy clause information since TextRangeArray assumes that there
  // must be a clase whose end is the length of the composition string when
  // there is one or more clauses.
  if (!textRangeArray->IsEmpty() &&
      maxOffsetOfClauses < aCompositionString.Length()) {
    TextRange dummyClause;
    dummyClause.mStartOffset = maxOffsetOfClauses;
    dummyClause.mEndOffset = aCompositionString.Length();
    dummyClause.mRangeType = TextRangeType::eRawClause;
    textRangeArray->AppendElement(dummyClause);
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p   CreateTextRangeArray(), inserting a dummy clause "
             "at the end of the composition string mStartOffset=%u, "
             "mEndOffset=%u, mRangeType=%s",
             this, dummyClause.mStartOffset, dummyClause.mEndOffset,
             ToChar(dummyClause.mRangeType)));
  }

  TextRange range;
  range.mStartOffset = range.mEndOffset = caretOffsetInUTF16;
  range.mRangeType = TextRangeType::eCaret;
  textRangeArray->AppendElement(range);
  MOZ_LOG(
      gIMELog, LogLevel::Debug,
      ("0x%p   CreateTextRangeArray(), mStartOffset=%u, "
       "mEndOffset=%u, mRangeType=%s",
       this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));

  pango_attr_iterator_destroy(iter);
  pango_attr_list_unref(feedback_list);
  g_free(preedit_string);

  return textRangeArray.forget();
}

/* static */
nscolor IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor) {
  PangoColor& pangoColor = aPangoAttrColor->color;
  uint8_t r = pangoColor.red / 0x100;
  uint8_t g = pangoColor.green / 0x100;
  uint8_t b = pangoColor.blue / 0x100;
  return NS_RGB(r, g, b);
}

bool IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter,
                                    const gchar* aUTF8CompositionString,
                                    uint32_t aUTF16CaretOffset,
                                    TextRange& aTextRange) const {
  // Set the range offsets in UTF-16 string.
  gint utf8ClauseStart, utf8ClauseEnd;
  pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd);
  if (utf8ClauseStart == utf8ClauseEnd) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   SetTextRange(), FAILED, due to collapsed range", this));
    return false;
  }

  if (!utf8ClauseStart) {
    aTextRange.mStartOffset = 0;
  } else {
    glong utf16PreviousClausesLength;
    gunichar2* utf16PreviousClausesString =
        g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr,
                        &utf16PreviousClausesLength, nullptr);

    if (NS_WARN_IF(!utf16PreviousClausesString)) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   SetTextRange(), FAILED, due to g_utf8_to_utf16() "
               "failure (retrieving previous string of current clause)",
               this));
      return false;
    }

    aTextRange.mStartOffset = utf16PreviousClausesLength;
    g_free(utf16PreviousClausesString);
  }

  glong utf16CurrentClauseLength;
  gunichar2* utf16CurrentClauseString = g_utf8_to_utf16(
      aUTF8CompositionString + utf8ClauseStart, utf8ClauseEnd - utf8ClauseStart,
      nullptr, &utf16CurrentClauseLength, nullptr);

  if (NS_WARN_IF(!utf16CurrentClauseString)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   SetTextRange(), FAILED, due to g_utf8_to_utf16() "
             "failure (retrieving current clause)",
             this));
    return false;
  }

  // iBus Chewing IME tells us that there is an empty clause at the end of
  // the composition string but we should ignore it since our code doesn't
  // assume that there is an empty clause.
  if (!utf16CurrentClauseLength) {
    MOZ_LOG(gIMELog, LogLevel::Warning,
            ("0x%p   SetTextRange(), FAILED, due to current clause length "
             "is 0",
             this));
    return false;
  }

  aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength;
  g_free(utf16CurrentClauseString);
  utf16CurrentClauseString = nullptr;

  // Set styles
  TextRangeStyle& style = aTextRange.mRangeStyle;

  // Underline
  PangoAttrInt* attrUnderline = reinterpret_cast<PangoAttrInt*>(
      pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE));
  if (attrUnderline) {
    switch (attrUnderline->value) {
      case PANGO_UNDERLINE_NONE:
        style.mLineStyle = TextRangeStyle::LineStyle::None;
        break;
      case PANGO_UNDERLINE_DOUBLE:
        style.mLineStyle = TextRangeStyle::LineStyle::Double;
        break;
      case PANGO_UNDERLINE_ERROR:
        style.mLineStyle = TextRangeStyle::LineStyle::Wavy;
        break;
      case PANGO_UNDERLINE_SINGLE:
      case PANGO_UNDERLINE_LOW:
        style.mLineStyle = TextRangeStyle::LineStyle::Solid;
        break;
      default:
        MOZ_LOG(gIMELog, LogLevel::Warning,
                ("0x%p   SetTextRange(), retrieved unknown underline "
                 "style: %d",
                 this, attrUnderline->value));
        style.mLineStyle = TextRangeStyle::LineStyle::Solid;
        break;
    }
    style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;

    // Underline color
    PangoAttrColor* attrUnderlineColor = reinterpret_cast<PangoAttrColor*>(
        pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE_COLOR));
    if (attrUnderlineColor) {
      style.mUnderlineColor = ToNscolor(attrUnderlineColor);
      style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR;
    }
  } else {
    style.mLineStyle = TextRangeStyle::LineStyle::None;
    style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
  }

  // Don't set colors if they are not specified.  They should be computed by
  // textframe if only one of the colors are specified.

  // Foreground color (text color)
  PangoAttrColor* attrForeground = reinterpret_cast<PangoAttrColor*>(
      pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND));
  if (attrForeground) {
    style.mForegroundColor = ToNscolor(attrForeground);
    style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR;
  }

  // Background color
  PangoAttrColor* attrBackground = reinterpret_cast<PangoAttrColor*>(
      pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND));
  if (attrBackground) {
    style.mBackgroundColor = ToNscolor(attrBackground);
    style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR;
  }

  /**
   * We need to judge the meaning of the clause for a11y.  Before we support
   * IME specific composition string style, we used following rules:
   *
   *   1: If attrUnderline and attrForground are specified, we assumed the
   *      clause is TextRangeType::eSelectedClause.
   *   2: If only attrUnderline is specified, we assumed the clause is
   *      TextRangeType::eConvertedClause.
   *   3: If only attrForground is specified, we assumed the clause is
   *      TextRangeType::eSelectedRawClause.
   *   4: If neither attrUnderline nor attrForeground is specified, we assumed
   *      the clause is TextRangeType::eRawClause.
   *
   * However, this rules are odd since there can be two or more selected
   * clauses.  Additionally, our old rules caused that IME developers/users
   * cannot specify composition string style as they want.
   *
   * So, we shouldn't guess the meaning from its visual style.
   */

  // If the range covers whole of composition string and the caret is at
  // the end of the composition string, the range is probably not converted.
  if (!utf8ClauseStart &&
      utf8ClauseEnd == static_cast<gint>(strlen(aUTF8CompositionString)) &&
      aTextRange.mEndOffset == aUTF16CaretOffset) {
    aTextRange.mRangeType = TextRangeType::eRawClause;
  }
  // Typically, the caret is set at the start of the selected clause.
  // So, if the caret is in the clause, we can assume that the clause is
  // selected.
  else if (aTextRange.mStartOffset <= aUTF16CaretOffset &&
           aTextRange.mEndOffset > aUTF16CaretOffset) {
    aTextRange.mRangeType = TextRangeType::eSelectedClause;
  }
  // Otherwise, we should assume that the clause is converted but not
  // selected.
  else {
    aTextRange.mRangeType = TextRangeType::eConvertedClause;
  }

  MOZ_LOG(gIMELog, LogLevel::Debug,
          ("0x%p   SetTextRange(), succeeded, aTextRange= { "
           "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }",
           this, aTextRange.mStartOffset, aTextRange.mEndOffset,
           ToChar(aTextRange.mRangeType),
           GetTextRangeStyleText(aTextRange.mRangeStyle).get()));

  return true;
}

void IMContextWrapper::SetCursorPosition(GtkIMContext* aContext) {
  MOZ_LOG(
      gIMELog, LogLevel::Info,
      ("0x%p SetCursorPosition(aContext=0x%p), "
       "mCompositionTargetRange={ mOffset=%u, mLength=%u }, "
       "mContentSelection=%s",
       this, aContext, mCompositionTargetRange.mOffset,
       mCompositionTargetRange.mLength, ToString(mContentSelection).c_str()));

  bool useCaret = false;
  if (!mCompositionTargetRange.IsValid()) {
    if (mContentSelection.isNothing()) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   SetCursorPosition(), FAILED, "
               "mCompositionTargetRange and mContentSelection are invalid",
               this));
      return;
    }
    if (!mContentSelection->HasRange()) {
      MOZ_LOG(gIMELog, LogLevel::Warning,
              ("0x%p   SetCursorPosition(), FAILED, "
               "mCompositionTargetRange is invalid and there is no selection",
               this));
      return;
    }
    useCaret = true;
  }

  if (!mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   SetCursorPosition(), FAILED, due to no focused "
             "window",
             this));
    return;
  }

  if (MOZ_UNLIKELY(!aContext)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   SetCursorPosition(), FAILED, due to no context", this));
    return;
  }

  WidgetQueryContentEvent queryCaretOrTextRectEvent(
      true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow);
  if (useCaret) {
    queryCaretOrTextRectEvent.InitForQueryCaretRect(
        mContentSelection->OffsetAndDataRef().StartOffset());
  } else {
    if (mContentSelection->WritingModeRef().IsVertical()) {
      // For preventing the candidate window to overlap the target
      // clause, we should set fake (typically, very tall) caret rect.
      uint32_t length =
          mCompositionTargetRange.mLength ? mCompositionTargetRange.mLength : 1;
      queryCaretOrTextRectEvent.InitForQueryTextRect(
          mCompositionTargetRange.mOffset, length);
    } else {
      queryCaretOrTextRectEvent.InitForQueryTextRect(
          mCompositionTargetRange.mOffset, 1);
    }
  }
  nsEventStatus status;
  mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status);
  if (queryCaretOrTextRectEvent.Failed()) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   SetCursorPosition(), FAILED, %s was failed", this,
             useCaret ? "eQueryCaretRect" : "eQueryTextRect"));
    return;
  }

  nsWindow* rootWindow =
      static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());

  // Get the position of the rootWindow in screen.
  LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset();

  // Get the position of IM context owner window in screen.
  LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset();

  // Compute the caret position in the IM owner window.
  LayoutDeviceIntRect rect =
      queryCaretOrTextRectEvent.mReply->mRect + root - owner;
  rect.width = 0;
  GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect);

  gtk_im_context_set_cursor_location(aContext, &area);
}

nsresult IMContextWrapper::GetCurrentParagraph(nsAString& aText,
                                               uint32_t& aCursorPos) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p GetCurrentParagraph(), mCompositionState=%s", this,
           GetCompositionStateName()));

  if (!mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   GetCurrentParagraph(), FAILED, there are no "
             "focused window in this module",
             this));
    return NS_ERROR_NULL_POINTER;
  }

  nsEventStatus status;

  uint32_t selOffset = mCompositionStart;
  uint32_t selLength = mSelectedStringRemovedByComposition.Length();

  // If focused editor doesn't have composition string, we should use
  // current selection.
  if (!EditorHasCompositionString()) {
    // Query cursor position & selection
    if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   GetCurrentParagraph(), FAILED, due to no "
               "valid selection information",
               this));
      return NS_ERROR_FAILURE;
    }

    if (mContentSelection.isSome() && mContentSelection->HasRange()) {
      selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
      selLength = mContentSelection->OffsetAndDataRef().Length();
    } else {
      // If there is no range, let's get all text instead...
      selOffset = 0u;
      selLength = INT32_MAX;  // TODO: Change to UINT32_MAX, but see below
    }
  }

  MOZ_LOG(gIMELog, LogLevel::Debug,
          ("0x%p   GetCurrentParagraph(), selOffset=%u, selLength=%u", this,
           selOffset, selLength));

  // XXX nsString::Find and nsString::RFind take int32_t for offset, so,
  //     we cannot support this request when the current offset is larger
  //     than INT32_MAX.
  if (selOffset > INT32_MAX || selLength > INT32_MAX ||
      selOffset + selLength > INT32_MAX) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   GetCurrentParagraph(), FAILED, The selection is "
             "out of range",
             this));
    return NS_ERROR_FAILURE;
  }

  // Get all text contents of the focused editor
  WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
                                                mLastFocusedWindow);
  queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
  mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
  if (NS_WARN_IF(queryTextContentEvent.Failed())) {
    return NS_ERROR_FAILURE;
  }

  if (selOffset + selLength > queryTextContentEvent.mReply->DataLength()) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   GetCurrentParagraph(), FAILED, The selection is "
             "invalid, queryTextContentEvent={ mReply=%s }",
             this, ToString(queryTextContentEvent.mReply).c_str()));
    return NS_ERROR_FAILURE;
  }

  // Remove composing string and restore the selected string because
  // GtkEntry doesn't remove selected string until committing, however,
  // our editor does it.  We should emulate the behavior for IME.
  nsAutoString textContent(queryTextContentEvent.mReply->DataRef());
  if (EditorHasCompositionString() &&
      mDispatchedCompositionString != mSelectedStringRemovedByComposition) {
    textContent.Replace(mCompositionStart,
                        mDispatchedCompositionString.Length(),
                        mSelectedStringRemovedByComposition);
  }

  // Get only the focused paragraph, by looking for newlines
  int32_t parStart = 0;
  if (selOffset > 0) {
    parStart = Substring(textContent, 0, selOffset - 1).RFind(u"\n") + 1;
  }
  int32_t parEnd = textContent.Find(u"\n", selOffset + selLength);
  if (parEnd < 0) {
    parEnd = textContent.Length();
  }
  aText = nsDependentSubstring(textContent, parStart, parEnd - parStart);
  aCursorPos = selOffset - uint32_t(parStart);

  MOZ_LOG(
      gIMELog, LogLevel::Debug,
      ("0x%p   GetCurrentParagraph(), succeeded, aText=%s, "
       "aText.Length()=%zu, aCursorPos=%u",
       this, NS_ConvertUTF16toUTF8(aText).get(), aText.Length(), aCursorPos));

  return NS_OK;
}

nsresult IMContextWrapper::DeleteText(GtkIMContext* aContext, int32_t aOffset,
                                      uint32_t aNChars) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), "
           "mCompositionState=%s",
           this, aContext, aOffset, aNChars, GetCompositionStateName()));

  if (!mLastFocusedWindow) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DeleteText(), FAILED, there are no focused window "
             "in this module",
             this));
    return NS_ERROR_NULL_POINTER;
  }

  if (!aNChars) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DeleteText(), FAILED, aNChars must not be zero", this));
    return NS_ERROR_INVALID_ARG;
  }

  RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
  nsEventStatus status;

  // First, we should cancel current composition because editor cannot
  // handle changing selection and deleting text.
  uint32_t selOffset;
  bool wasComposing = IsComposing();
  bool editorHadCompositionString = EditorHasCompositionString();
  if (wasComposing) {
    selOffset = mCompositionStart;
    if (!DispatchCompositionCommitEvent(aContext,
                                        &mSelectedStringRemovedByComposition)) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   DeleteText(), FAILED, quitting from DeletText", this));
      return NS_ERROR_FAILURE;
    }
  } else {
    if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
      MOZ_LOG(gIMELog, LogLevel::Error,
              ("0x%p   DeleteText(), FAILED, due to no valid selection "
               "information",
               this));
      return NS_ERROR_FAILURE;
    }
    if (!mContentSelection->HasRange()) {
      MOZ_LOG(gIMELog, LogLevel::Debug,
              ("0x%p   DeleteText(), does nothing, due to no selection range",
               this));
      return NS_OK;
    }
    selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
  }

  // Get all text contents of the focused editor
  WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
                                                mLastFocusedWindow);
  queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
  mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
  if (NS_WARN_IF(queryTextContentEvent.Failed())) {
    return NS_ERROR_FAILURE;
  }
  if (queryTextContentEvent.mReply->IsDataEmpty()) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DeleteText(), FAILED, there is no contents", this));
    return NS_ERROR_FAILURE;
  }

  NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(
      queryTextContentEvent.mReply->DataRef(), 0, selOffset));
  glong offsetInUTF8Characters =
      g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset;
  if (offsetInUTF8Characters < 0) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DeleteText(), FAILED, aOffset is too small for "
             "current cursor pos (computed offset: %ld)",
             this, offsetInUTF8Characters));
    return NS_ERROR_FAILURE;
  }

  AppendUTF16toUTF8(
      nsDependentSubstring(queryTextContentEvent.mReply->DataRef(), selOffset),
      utf8Str);
  glong countOfCharactersInUTF8 =
      g_utf8_strlen(utf8Str.get(), utf8Str.Length());
  glong endInUTF8Characters = offsetInUTF8Characters + aNChars;
  if (countOfCharactersInUTF8 < endInUTF8Characters) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DeleteText(), FAILED, aNChars is too large for "
             "current contents (content length: %ld, computed end offset: %ld)",
             this, countOfCharactersInUTF8, endInUTF8Characters));
    return NS_ERROR_FAILURE;
  }

  gchar* charAtOffset =
      g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters);
  gchar* charAtEnd =
      g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters);

  // Set selection to delete
  WidgetSelectionEvent selectionEvent(true, eSetSelection, mLastFocusedWindow);

  nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0,
                                            charAtOffset - utf8Str.get());
  selectionEvent.mOffset = NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length();

  nsDependentCSubstring utf8DeletingStr(utf8Str, utf8StrBeforeOffset.Length(),
                                        charAtEnd - charAtOffset);
  selectionEvent.mLength = NS_ConvertUTF8toUTF16(utf8DeletingStr).Length();

  selectionEvent.mReversed = false;
  selectionEvent.mExpandToClusterBoundary = false;
  lastFocusedWindow->DispatchEvent(&selectionEvent, status);

  if (!selectionEvent.mSucceeded || lastFocusedWindow != mLastFocusedWindow ||
      lastFocusedWindow->Destroyed()) {
    MOZ_LOG(gIMELog, 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(eContentCommandDelete)) {
    MOZ_LOG(gIMELog, 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()) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p   DeleteText(), FAILED, deleting the selection caused "
             "focus change or window destroyed",
             this));
    return NS_ERROR_FAILURE;
  }

  if (!wasComposing) {
    return NS_OK;
  }

  // Restore the composition at new caret position.
  if (!DispatchCompositionStart(aContext)) {
    MOZ_LOG(
        gIMELog, LogLevel::Error,
        ("0x%p   DeleteText(), FAILED, resterting composition start", this));
    return NS_ERROR_FAILURE;
  }

  if (!editorHadCompositionString) {
    return NS_OK;
  }

  nsAutoString compositionString;
  GetCompositionString(aContext, compositionString);
  if (!DispatchCompositionChangeEvent(aContext, compositionString)) {
    MOZ_LOG(
        gIMELog, LogLevel::Error,
        ("0x%p   DeleteText(), FAILED, restoring composition string", this));
    return NS_ERROR_FAILURE;
  }

  return NS_OK;
}

bool IMContextWrapper::EnsureToCacheContentSelection(
    nsAString* aSelectedString) {
  if (aSelectedString) {
    aSelectedString->Truncate();
  }

  if (mContentSelection.isSome()) {
    if (mContentSelection->HasRange() && aSelectedString) {
      aSelectedString->Assign(mContentSelection->OffsetAndDataRef().DataRef());
    }
    return true;
  }

  RefPtr<nsWindow> dispatcherWindow =
      mLastFocusedWindow ? mLastFocusedWindow : mOwnerWindow;
  if (NS_WARN_IF(!dispatcherWindow)) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
             "no focused window",
             this));
    return false;
  }

  nsEventStatus status;
  WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
                                                 dispatcherWindow);
  dispatcherWindow->DispatchEvent(&querySelectedTextEvent, status);
  if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
    MOZ_LOG(gIMELog, LogLevel::Error,
            ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
             "failure of query selection event",
             this));
    return false;
  }

  mContentSelection = Some(ContentSelection(querySelectedTextEvent));
  if (mContentSelection->HasRange()) {
    if (!mContentSelection->OffsetAndDataRef().IsDataEmpty() &&
        aSelectedString) {
      aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef());
    }
  }

  MOZ_LOG(
      gIMELog, LogLevel::Debug,
      ("0x%p EnsureToCacheContentSelection(), Succeeded, mContentSelection=%s",
       this, ToString(mContentSelection).c_str()));
  return true;
}

}  // namespace widget
}  // namespace mozilla
back to top