Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "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
const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE");
// See _get_boolean_env() in client/gtk2/ibusimcontext.c
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
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
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() {
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 release 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_object_unref(mContext);
mContext = nullptr;
}
if (mSimpleContext) {
gtk_im_context_set_client_window(mSimpleContext, nullptr);
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) {