/* -*- 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(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 ) 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 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(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(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(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(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(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(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 // 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; } 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 kungFuDeathGrip(this); RefPtr 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 . // 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: // // // // 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) { 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) { aModule->OnChangeCompositionNative(aContext); } void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) { 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 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); 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 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 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(g_object_ref(aContext)); MOZ_ASSERT(mComposingContext); // Keep the last focused window alive RefPtr 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 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 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 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 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 lastFocusedWindow(mLastFocusedWindow); RefPtr 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 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 = 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(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( 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( 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( 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( 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(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(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 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 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