no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / widget / gtk / IMContextWrapper.cpp
blobfc87acbf8633c67ea95f31e64d586358e240f7b4
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=4 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/Logging.h"
8 #include "nsString.h"
9 #include "prtime.h"
10 #include "prenv.h"
12 #include "IMContextWrapper.h"
14 #include "GRefPtr.h"
15 #include "nsGtkKeyUtils.h"
16 #include "nsWindow.h"
17 #include "mozilla/AutoRestore.h"
18 #include "mozilla/Likely.h"
19 #include "mozilla/LookAndFeel.h"
20 #include "mozilla/MiscEvents.h"
21 #include "mozilla/Preferences.h"
22 #include "mozilla/StaticPrefs_intl.h"
23 #include "mozilla/Telemetry.h"
24 #include "mozilla/TextEventDispatcher.h"
25 #include "mozilla/TextEvents.h"
26 #include "mozilla/ToString.h"
27 #include "mozilla/WritingModes.h"
29 // For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync`
30 // rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
31 // big file.
32 // Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
33 mozilla::LazyLogModule gIMELog("IMEHandler");
35 namespace mozilla {
36 namespace widget {
38 static inline const char* ToChar(bool aBool) {
39 return aBool ? "true" : "false";
42 static const char* GetEventType(GdkEventKey* aKeyEvent) {
43 switch (aKeyEvent->type) {
44 case GDK_KEY_PRESS:
45 return "GDK_KEY_PRESS";
46 case GDK_KEY_RELEASE:
47 return "GDK_KEY_RELEASE";
48 default:
49 return "Unknown";
53 class GetEventStateName : public nsAutoCString {
54 public:
55 explicit GetEventStateName(guint aState,
56 IMContextWrapper::IMContextID aIMContextID =
57 IMContextWrapper::IMContextID::Unknown) {
58 if (aState & GDK_SHIFT_MASK) {
59 AppendModifier("shift");
61 if (aState & GDK_CONTROL_MASK) {
62 AppendModifier("control");
64 if (aState & GDK_MOD1_MASK) {
65 AppendModifier("mod1");
67 if (aState & GDK_MOD2_MASK) {
68 AppendModifier("mod2");
70 if (aState & GDK_MOD3_MASK) {
71 AppendModifier("mod3");
73 if (aState & GDK_MOD4_MASK) {
74 AppendModifier("mod4");
76 if (aState & GDK_MOD4_MASK) {
77 AppendModifier("mod5");
79 if (aState & GDK_MOD4_MASK) {
80 AppendModifier("mod5");
82 switch (aIMContextID) {
83 case IMContextWrapper::IMContextID::IBus:
84 static const guint IBUS_HANDLED_MASK = 1 << 24;
85 static const guint IBUS_IGNORED_MASK = 1 << 25;
86 if (aState & IBUS_HANDLED_MASK) {
87 AppendModifier("IBUS_HANDLED_MASK");
89 if (aState & IBUS_IGNORED_MASK) {
90 AppendModifier("IBUS_IGNORED_MASK");
92 break;
93 case IMContextWrapper::IMContextID::Fcitx:
94 case IMContextWrapper::IMContextID::Fcitx5:
95 static const guint FcitxKeyState_HandledMask = 1 << 24;
96 static const guint FcitxKeyState_IgnoredMask = 1 << 25;
97 if (aState & FcitxKeyState_HandledMask) {
98 AppendModifier("FcitxKeyState_HandledMask");
100 if (aState & FcitxKeyState_IgnoredMask) {
101 AppendModifier("FcitxKeyState_IgnoredMask");
103 break;
104 default:
105 break;
109 private:
110 void AppendModifier(const char* aModifierName) {
111 if (!IsEmpty()) {
112 AppendLiteral(" + ");
114 Append(aModifierName);
118 class GetTextRangeStyleText final : public nsAutoCString {
119 public:
120 explicit GetTextRangeStyleText(const TextRangeStyle& aStyle) {
121 if (!aStyle.IsDefined()) {
122 AssignLiteral("{ IsDefined()=false }");
123 return;
126 if (aStyle.IsLineStyleDefined()) {
127 AppendLiteral("{ mLineStyle=");
128 AppendLineStyle(aStyle.mLineStyle);
129 if (aStyle.IsUnderlineColorDefined()) {
130 AppendLiteral(", mUnderlineColor=");
131 AppendColor(aStyle.mUnderlineColor);
132 } else {
133 AppendLiteral(", IsUnderlineColorDefined=false");
135 } else {
136 AppendLiteral("{ IsLineStyleDefined()=false");
139 if (aStyle.IsForegroundColorDefined()) {
140 AppendLiteral(", mForegroundColor=");
141 AppendColor(aStyle.mForegroundColor);
142 } else {
143 AppendLiteral(", IsForegroundColorDefined()=false");
146 if (aStyle.IsBackgroundColorDefined()) {
147 AppendLiteral(", mBackgroundColor=");
148 AppendColor(aStyle.mBackgroundColor);
149 } else {
150 AppendLiteral(", IsBackgroundColorDefined()=false");
153 AppendLiteral(" }");
155 void AppendLineStyle(TextRangeStyle::LineStyle aLineStyle) {
156 switch (aLineStyle) {
157 case TextRangeStyle::LineStyle::None:
158 AppendLiteral("LineStyle::None");
159 break;
160 case TextRangeStyle::LineStyle::Solid:
161 AppendLiteral("LineStyle::Solid");
162 break;
163 case TextRangeStyle::LineStyle::Dotted:
164 AppendLiteral("LineStyle::Dotted");
165 break;
166 case TextRangeStyle::LineStyle::Dashed:
167 AppendLiteral("LineStyle::Dashed");
168 break;
169 case TextRangeStyle::LineStyle::Double:
170 AppendLiteral("LineStyle::Double");
171 break;
172 case TextRangeStyle::LineStyle::Wavy:
173 AppendLiteral("LineStyle::Wavy");
174 break;
175 default:
176 AppendPrintf("Invalid(0x%02X)",
177 static_cast<TextRangeStyle::LineStyleType>(aLineStyle));
178 break;
181 void AppendColor(nscolor aColor) {
182 AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }", NS_GET_R(aColor),
183 NS_GET_G(aColor), NS_GET_B(aColor), NS_GET_A(aColor));
185 virtual ~GetTextRangeStyleText() = default;
188 const static bool kUseSimpleContextDefault = false;
190 /******************************************************************************
191 * SelectionStyleProvider
193 * IME (e.g., fcitx, ~4.2.8.3) may look up selection colors of widget, which
194 * is related to the window associated with the IM context, to support any
195 * colored widgets. Our editor (like <input type="text">) is rendered as
196 * native GtkTextView as far as possible by default and if editor color is
197 * changed by web apps, nsTextFrame may swap background color of foreground
198 * color of composition string for making composition string is always
199 * visually distinct in normal text.
201 * So, we would like IME to set style of composition string to good colors
202 * in GtkTextView. Therefore, this class overwrites selection colors of
203 * our widget with selection colors of GtkTextView so that it's possible IME
204 * to refer selection colors of GtkTextView via our widget.
205 ******************************************************************************/
207 static Maybe<nscolor> GetSystemColor(LookAndFeel::ColorID aId) {
208 return LookAndFeel::GetColor(aId, LookAndFeel::ColorScheme::Light,
209 LookAndFeel::UseStandins::No);
212 class SelectionStyleProvider final {
213 public:
214 static SelectionStyleProvider* GetExistingInstance() { return sInstance; }
216 static SelectionStyleProvider* GetInstance() {
217 if (sHasShutDown) {
218 return nullptr;
220 if (!sInstance) {
221 sInstance = new SelectionStyleProvider();
223 return sInstance;
226 static void Shutdown() {
227 if (sInstance) {
228 g_object_unref(sInstance->mProvider);
230 delete sInstance;
231 sInstance = nullptr;
232 sHasShutDown = true;
235 // mContainer associated with an IM context.
236 void AttachTo(MozContainer* aContainer) {
237 gtk_style_context_add_provider(
238 gtk_widget_get_style_context(GTK_WIDGET(aContainer)),
239 GTK_STYLE_PROVIDER(mProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
242 void OnThemeChanged() {
243 // fcitx refers GtkStyle::text[GTK_STATE_SELECTED] and
244 // GtkStyle::bg[GTK_STATE_SELECTED] (although pair of text and *base*
245 // or *fg* and bg is correct). gtk_style_update_from_context() will
246 // set these colors using the widget's GtkStyleContext and so the
247 // colors can be controlled by a ":selected" CSS rule.
248 nsAutoCString style(":selected{");
249 // FYI: LookAndFeel always returns selection colors of GtkTextView.
250 if (auto selectionForegroundColor =
251 GetSystemColor(LookAndFeel::ColorID::Highlight)) {
252 double alpha =
253 static_cast<double>(NS_GET_A(*selectionForegroundColor)) / 0xFF;
254 style.AppendPrintf("color:rgba(%u,%u,%u,",
255 NS_GET_R(*selectionForegroundColor),
256 NS_GET_G(*selectionForegroundColor),
257 NS_GET_B(*selectionForegroundColor));
258 // We can't use AppendPrintf here, because it does locale-specific
259 // formatting of floating-point values.
260 style.AppendFloat(alpha);
261 style.AppendPrintf(");");
263 if (auto selectionBackgroundColor =
264 GetSystemColor(LookAndFeel::ColorID::Highlighttext)) {
265 double alpha =
266 static_cast<double>(NS_GET_A(*selectionBackgroundColor)) / 0xFF;
267 style.AppendPrintf("background-color:rgba(%u,%u,%u,",
268 NS_GET_R(*selectionBackgroundColor),
269 NS_GET_G(*selectionBackgroundColor),
270 NS_GET_B(*selectionBackgroundColor));
271 style.AppendFloat(alpha);
272 style.AppendPrintf(");");
274 style.AppendLiteral("}");
275 gtk_css_provider_load_from_data(mProvider, style.get(), -1, nullptr);
278 private:
279 static SelectionStyleProvider* sInstance;
280 static bool sHasShutDown;
281 GtkCssProvider* const mProvider;
283 SelectionStyleProvider() : mProvider(gtk_css_provider_new()) {
284 OnThemeChanged();
288 SelectionStyleProvider* SelectionStyleProvider::sInstance = nullptr;
289 bool SelectionStyleProvider::sHasShutDown = false;
291 /******************************************************************************
292 * IMContextWrapper
293 ******************************************************************************/
295 IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr;
296 guint16 IMContextWrapper::sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
297 bool IMContextWrapper::sUseSimpleContext;
299 NS_IMPL_ISUPPORTS(IMContextWrapper, TextEventDispatcherListener,
300 nsISupportsWeakReference)
302 IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow)
303 : mOwnerWindow(aOwnerWindow),
304 mLastFocusedWindow(nullptr),
305 mContext(nullptr),
306 mSimpleContext(nullptr),
307 mDummyContext(nullptr),
308 mComposingContext(nullptr),
309 mCompositionStart(UINT32_MAX),
310 mProcessingKeyEvent(nullptr),
311 mCompositionState(eCompositionState_NotComposing),
312 mIMContextID(IMContextID::Unknown),
313 mFallbackToKeyEvent(false),
314 mKeyboardEventWasDispatched(false),
315 mKeyboardEventWasConsumed(false),
316 mIsDeletingSurrounding(false),
317 mLayoutChanged(false),
318 mSetCursorPositionOnKeyEvent(true),
319 mPendingResettingIMContext(false),
320 mRetrieveSurroundingSignalReceived(false),
321 mMaybeInDeadKeySequence(false),
322 mIsIMInAsyncKeyHandlingMode(false),
323 mSetInputPurposeAndInputHints(false) {
324 static bool sFirstInstance = true;
325 if (sFirstInstance) {
326 sFirstInstance = false;
327 sUseSimpleContext =
328 Preferences::GetBool("intl.ime.use_simple_context_on_password_field",
329 kUseSimpleContextDefault);
331 Init();
334 static bool IsIBusInSyncMode() {
335 // See ibus_im_context_class_init() in client/gtk2/ibusimcontext.c
336 // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L610
337 const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE");
339 // See _get_boolean_env() in client/gtk2/ibusimcontext.c
340 // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L520-L537
341 if (!env) {
342 return false;
344 nsDependentCString envStr(env);
345 if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
346 envStr.EqualsLiteral("false") || envStr.EqualsLiteral("False") ||
347 envStr.EqualsLiteral("FALSE")) {
348 return false;
350 return true;
353 static bool GetFcitxBoolEnv(const char* aEnv) {
354 // See fcitx_utils_get_boolean_env in src/lib/fcitx-utils/utils.c
355 // https://github.com/fcitx/fcitx/blob/0c87840dc7d9460c2cb5feaeefec299d0d3d62ec/src/lib/fcitx-utils/utils.c#L721-L736
356 const char* env = PR_GetEnv(aEnv);
357 if (!env) {
358 return false;
360 nsDependentCString envStr(env);
361 if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
362 envStr.EqualsLiteral("false")) {
363 return false;
365 return true;
368 static bool IsFcitxInSyncMode() {
369 // See fcitx_im_context_class_init() in src/frontend/gtk2/fcitximcontext.c
370 // https://github.com/fcitx/fcitx/blob/78b98d9230dc9630e99d52e3172bdf440ffd08c4/src/frontend/gtk2/fcitximcontext.c#L395-L398
371 return GetFcitxBoolEnv("IBUS_ENABLE_SYNC_MODE") ||
372 GetFcitxBoolEnv("FCITX_ENABLE_SYNC_MODE");
375 nsDependentCSubstring IMContextWrapper::GetIMName() const {
376 const char* contextIDChar =
377 gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext));
378 if (!contextIDChar) {
379 return nsDependentCSubstring();
382 nsDependentCSubstring im(contextIDChar, strlen(contextIDChar));
384 // If the context is XIM, actual engine must be specified with
385 // |XMODIFIERS=@im=foo|.
386 const char* xmodifiersChar = PR_GetEnv("XMODIFIERS");
387 if (!xmodifiersChar || !im.EqualsLiteral("xim")) {
388 return im;
391 nsDependentCString xmodifiers(xmodifiersChar);
392 int32_t atIMValueStart = xmodifiers.Find("@im=") + 4;
393 if (atIMValueStart < 4 ||
394 xmodifiers.Length() <= static_cast<size_t>(atIMValueStart)) {
395 return im;
398 int32_t atIMValueEnd = xmodifiers.Find("@", atIMValueStart);
399 if (atIMValueEnd > atIMValueStart) {
400 return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
401 atIMValueEnd - atIMValueStart);
404 if (atIMValueEnd == kNotFound) {
405 return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
406 strlen(xmodifiersChar) - atIMValueStart);
409 return im;
412 void IMContextWrapper::Init() {
413 // Overwrite selection colors of the window before associating the window
414 // with IM context since IME may look up selection colors via IM context
415 // to support any colored widgets.
416 SelectionStyleProvider::GetInstance()->AttachTo(
417 mOwnerWindow->GetMozContainer());
419 // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
420 // So, we don't need to check the result.
422 // Normal context.
423 mContext = gtk_im_multicontext_new();
424 g_signal_connect(mContext, "preedit_changed",
425 G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback),
426 this);
427 g_signal_connect(mContext, "retrieve_surrounding",
428 G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback),
429 this);
430 g_signal_connect(mContext, "delete_surrounding",
431 G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback),
432 this);
433 g_signal_connect(mContext, "commit",
434 G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback),
435 this);
436 g_signal_connect(mContext, "preedit_start",
437 G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
438 this);
439 g_signal_connect(mContext, "preedit_end",
440 G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
441 this);
442 nsDependentCSubstring im = GetIMName();
443 if (im.EqualsLiteral("ibus")) {
444 mIMContextID = IMContextID::IBus;
445 mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode();
446 // Although ibus has key snooper mode, it's forcibly disabled on Firefox
447 // in default settings by its whitelist since we always send key events
448 // to IME before handling shortcut keys. The whitelist can be
449 // customized with env, IBUS_NO_SNOOPER_APPS, but we don't need to
450 // support such rare cases for reducing maintenance cost.
451 mIsKeySnooped = false;
452 } else if (im.EqualsLiteral("fcitx")) {
453 mIMContextID = IMContextID::Fcitx;
454 mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode();
455 // Although Fcitx has key snooper mode similar to ibus, it's also
456 // disabled on Firefox in default settings by its whitelist. The
457 // whitelist can be customized with env, IBUS_NO_SNOOPER_APPS or
458 // FCITX_NO_SNOOPER_APPS, but we don't need to support such rare cases
459 // for reducing maintenance cost.
460 mIsKeySnooped = false;
461 } else if (im.EqualsLiteral("fcitx5")) {
462 mIMContextID = IMContextID::Fcitx5;
463 mIsIMInAsyncKeyHandlingMode = true; // does not have sync mode.
464 mIsKeySnooped = false; // never use key snooper.
465 } else if (im.EqualsLiteral("uim")) {
466 mIMContextID = IMContextID::Uim;
467 mIsIMInAsyncKeyHandlingMode = false;
468 // We cannot know if uim uses key snooper since it's build option of
469 // uim. Therefore, we need to retrieve the consideration from the
470 // pref for making users and distributions allowed to choose their
471 // preferred value.
472 mIsKeySnooped =
473 Preferences::GetBool("intl.ime.hack.uim.using_key_snooper", true);
474 } else if (im.EqualsLiteral("scim")) {
475 mIMContextID = IMContextID::Scim;
476 mIsIMInAsyncKeyHandlingMode = false;
477 mIsKeySnooped = false;
478 } else if (im.EqualsLiteral("iiim")) {
479 mIMContextID = IMContextID::IIIMF;
480 mIsIMInAsyncKeyHandlingMode = false;
481 mIsKeySnooped = false;
482 } else if (im.EqualsLiteral("wayland")) {
483 mIMContextID = IMContextID::Wayland;
484 mIsIMInAsyncKeyHandlingMode = false;
485 mIsKeySnooped = true;
486 } else {
487 mIMContextID = IMContextID::Unknown;
488 mIsIMInAsyncKeyHandlingMode = false;
489 mIsKeySnooped = false;
492 // Simple context
493 if (sUseSimpleContext) {
494 mSimpleContext = gtk_im_context_simple_new();
495 g_signal_connect(mSimpleContext, "preedit_changed",
496 G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback),
497 this);
498 g_signal_connect(
499 mSimpleContext, "retrieve_surrounding",
500 G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback), this);
501 g_signal_connect(mSimpleContext, "delete_surrounding",
502 G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback),
503 this);
504 g_signal_connect(mSimpleContext, "commit",
505 G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback),
506 this);
507 g_signal_connect(mSimpleContext, "preedit_start",
508 G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
509 this);
510 g_signal_connect(mSimpleContext, "preedit_end",
511 G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
512 this);
515 // Dummy context
516 mDummyContext = gtk_im_multicontext_new();
518 MOZ_LOG(gIMELog, LogLevel::Info,
519 ("0x%p Init(), mOwnerWindow=%p, mContext=%p (im=\"%s\"), "
520 "mIsIMInAsyncKeyHandlingMode=%s, mIsKeySnooped=%s, "
521 "mSimpleContext=%p, mDummyContext=%p, "
522 "gtk_im_multicontext_get_context_id()=\"%s\", "
523 "PR_GetEnv(\"XMODIFIERS\")=\"%s\"",
524 this, mOwnerWindow, mContext, nsAutoCString(im).get(),
525 ToChar(mIsIMInAsyncKeyHandlingMode), ToChar(mIsKeySnooped),
526 mSimpleContext, mDummyContext,
527 gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)),
528 PR_GetEnv("XMODIFIERS")));
531 /* static */
532 void IMContextWrapper::Shutdown() { SelectionStyleProvider::Shutdown(); }
534 IMContextWrapper::~IMContextWrapper() {
535 MOZ_ASSERT(!mContext);
536 MOZ_ASSERT(!mComposingContext);
537 if (this == sLastFocusedContext) {
538 sLastFocusedContext = nullptr;
540 MOZ_LOG(gIMELog, LogLevel::Info, ("0x%p ~IMContextWrapper()", this));
543 void IMContextWrapper::SetGdkWindow(GdkWindow* aGdkWindow) {
544 MOZ_LOG(gIMELog, LogLevel::Info,
545 ("0x%p GdkWindowChanged(%p)", this, aGdkWindow));
546 MOZ_ASSERT(!aGdkWindow || mOwnerWindow->GetGdkWindow() == aGdkWindow);
547 gtk_im_context_set_client_window(mContext, aGdkWindow);
548 if (mSimpleContext) {
549 gtk_im_context_set_client_window(mSimpleContext, aGdkWindow);
551 gtk_im_context_set_client_window(mDummyContext, aGdkWindow);
554 NS_IMETHODIMP
555 IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
556 const IMENotification& aNotification) {
557 switch (aNotification.mMessage) {
558 case REQUEST_TO_COMMIT_COMPOSITION:
559 case REQUEST_TO_CANCEL_COMPOSITION: {
560 nsWindow* window =
561 static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
562 return IsComposing() ? EndIMEComposition(window) : NS_OK;
564 case NOTIFY_IME_OF_FOCUS:
565 OnFocusChangeInGecko(true);
566 return NS_OK;
567 case NOTIFY_IME_OF_BLUR:
568 OnFocusChangeInGecko(false);
569 return NS_OK;
570 case NOTIFY_IME_OF_POSITION_CHANGE:
571 OnLayoutChange();
572 return NS_OK;
573 case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
574 OnUpdateComposition();
575 return NS_OK;
576 case NOTIFY_IME_OF_SELECTION_CHANGE: {
577 nsWindow* window =
578 static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
579 OnSelectionChange(window, aNotification);
580 return NS_OK;
582 default:
583 return NS_ERROR_NOT_IMPLEMENTED;
587 NS_IMETHODIMP_(void)
588 IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
589 // XXX When input transaction is being stolen by add-on, what should we do?
592 NS_IMETHODIMP_(void)
593 IMContextWrapper::WillDispatchKeyboardEvent(
594 TextEventDispatcher* aTextEventDispatcher,
595 WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
596 void* aData) {
597 KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent,
598 static_cast<GdkEventKey*>(aData));
601 TextEventDispatcher* IMContextWrapper::GetTextEventDispatcher() {
602 if (NS_WARN_IF(!mLastFocusedWindow)) {
603 return nullptr;
605 TextEventDispatcher* dispatcher =
606 mLastFocusedWindow->GetTextEventDispatcher();
607 // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr.
608 MOZ_RELEASE_ASSERT(dispatcher);
609 return dispatcher;
612 NS_IMETHODIMP_(IMENotificationRequests)
613 IMContextWrapper::GetIMENotificationRequests() {
614 IMENotificationRequests::Notifications notifications =
615 IMENotificationRequests::NOTIFY_NOTHING;
616 // If it's not enabled, we don't need position change notification.
617 if (IsEnabled()) {
618 notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE;
620 return IMENotificationRequests(notifications);
623 void IMContextWrapper::OnDestroyWindow(nsWindow* aWindow) {
624 MOZ_LOG(
625 gIMELog, LogLevel::Info,
626 ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
627 "mOwnerWindow=0x%p, mLastFocusedModule=0x%p",
628 this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext));
630 MOZ_ASSERT(aWindow, "aWindow must not be null");
632 if (mLastFocusedWindow == aWindow) {
633 if (IsComposing()) {
634 EndIMEComposition(aWindow);
636 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
637 mLastFocusedWindow = nullptr;
640 if (mOwnerWindow != aWindow) {
641 return;
644 if (sLastFocusedContext == this) {
645 sLastFocusedContext = nullptr;
649 * NOTE:
650 * The given window is the owner of this, so, we must disconnect from the
651 * contexts now. But that might be referred from other nsWindows
652 * (they are children of this. But we don't know why there are the
653 * cases). So, we need to clear the pointers that refers to contexts
654 * and this if the other referrers are still alive. See bug 349727.
656 if (mContext) {
657 PrepareToDestroyContext(mContext);
658 gtk_im_context_set_client_window(mContext, nullptr);
659 g_signal_handlers_disconnect_by_data(mContext, this);
660 g_object_unref(mContext);
661 mContext = nullptr;
664 if (mSimpleContext) {
665 gtk_im_context_set_client_window(mSimpleContext, nullptr);
666 g_signal_handlers_disconnect_by_data(mSimpleContext, this);
667 g_object_unref(mSimpleContext);
668 mSimpleContext = nullptr;
671 if (mDummyContext) {
672 // mContext and mDummyContext have the same slaveType and signal_data
673 // so no need for another workaround_gtk_im_display_closed.
674 gtk_im_context_set_client_window(mDummyContext, nullptr);
675 g_object_unref(mDummyContext);
676 mDummyContext = nullptr;
679 if (NS_WARN_IF(mComposingContext)) {
680 g_object_unref(mComposingContext);
681 mComposingContext = nullptr;
684 mOwnerWindow = nullptr;
685 mLastFocusedWindow = nullptr;
686 mInputContext.mIMEState.mEnabled = IMEEnabled::Disabled;
687 mPostingKeyEvents.Clear();
689 MOZ_LOG(gIMELog, LogLevel::Debug,
690 ("0x%p OnDestroyWindow(), succeeded, Completely destroyed", this));
693 void IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext) {
694 if (mIMContextID == IMContextID::IIIMF) {
695 // IIIM module registers handlers for the "closed" signal on the
696 // display, but the signal handler is not disconnected when the module
697 // is unloaded. To prevent the module from being unloaded, use static
698 // variable to hold reference of slave context class declared by IIIM.
699 // Note that this does not grab any instance, it grabs the "class".
700 static gpointer sGtkIIIMContextClass = nullptr;
701 if (!sGtkIIIMContextClass) {
702 // We retrieved slave context class with g_type_name() and actual
703 // slave context instance when our widget was GTK2. That must be
704 // _GtkIMContext::priv::slave in GTK3. However, _GtkIMContext::priv
705 // is an opacity struct named _GtkIMMulticontextPrivate, i.e., it's
706 // not exposed by GTK3. Therefore, we cannot access the instance
707 // safely. So, we need to retrieve the slave context class with
708 // g_type_from_name("GtkIMContextIIIM") directly (anyway, we needed
709 // to compare the class name with "GtkIMContextIIIM").
710 GType IIMContextType = g_type_from_name("GtkIMContextIIIM");
711 if (IIMContextType) {
712 sGtkIIIMContextClass = g_type_class_ref(IIMContextType);
713 MOZ_LOG(gIMELog, LogLevel::Info,
714 ("0x%p PrepareToDestroyContext(), added to reference to "
715 "GtkIMContextIIIM class to prevent it from being unloaded",
716 this));
717 } else {
718 MOZ_LOG(gIMELog, LogLevel::Error,
719 ("0x%p PrepareToDestroyContext(), FAILED to prevent the "
720 "IIIM module from being uploaded",
721 this));
727 void IMContextWrapper::OnFocusWindow(nsWindow* aWindow) {
728 if (MOZ_UNLIKELY(IsDestroyed())) {
729 return;
732 MOZ_LOG(gIMELog, LogLevel::Info,
733 ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this,
734 aWindow, mLastFocusedWindow));
735 mLastFocusedWindow = aWindow;
738 void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) {
739 if (MOZ_UNLIKELY(IsDestroyed())) {
740 return;
743 MOZ_LOG(
744 gIMELog, LogLevel::Info,
745 ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
746 "mIMEFocusState=%s",
747 this, aWindow, mLastFocusedWindow, ToString(mIMEFocusState).c_str()));
749 if (mLastFocusedWindow != aWindow) {
750 return;
753 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
756 KeyHandlingState IMContextWrapper::OnKeyEvent(
757 nsWindow* aCaller, GdkEventKey* aEvent,
758 bool aKeyboardEventWasDispatched /* = false */) {
759 MOZ_ASSERT(aEvent, "aEvent must be non-null");
761 if (!mInputContext.mIMEState.IsEditable() || MOZ_UNLIKELY(IsDestroyed())) {
762 return KeyHandlingState::eNotHandled;
765 MOZ_LOG(gIMELog, LogLevel::Info, (">>>>>>>>>>>>>>>>"));
766 MOZ_LOG(
767 gIMELog, LogLevel::Info,
768 ("0x%p OnKeyEvent(aCaller=0x%p, "
769 "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X, state=%s, "
770 "time=%u, hardware_keycode=%u, group=%u }, "
771 "aKeyboardEventWasDispatched=%s)",
772 this, aCaller, aEvent, GetEventType(aEvent),
773 gdk_keyval_name(aEvent->keyval), gdk_keyval_to_unicode(aEvent->keyval),
774 GetEventStateName(aEvent->state, mIMContextID).get(), aEvent->time,
775 aEvent->hardware_keycode, aEvent->group,
776 ToChar(aKeyboardEventWasDispatched)));
777 MOZ_LOG(
778 gIMELog, LogLevel::Info,
779 ("0x%p OnKeyEvent(), mMaybeInDeadKeySequence=%s, "
780 "mCompositionState=%s, current context=%p, active context=%p, "
781 "mIMContextID=%s, mIsIMInAsyncKeyHandlingMode=%s",
782 this, ToChar(mMaybeInDeadKeySequence), GetCompositionStateName(),
783 GetCurrentContext(), GetActiveContext(), ToString(mIMContextID).c_str(),
784 ToChar(mIsIMInAsyncKeyHandlingMode)));
786 if (aCaller != mLastFocusedWindow) {
787 MOZ_LOG(gIMELog, LogLevel::Error,
788 ("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
789 "window, mLastFocusedWindow=0x%p",
790 this, mLastFocusedWindow));
791 return KeyHandlingState::eNotHandled;
794 // Even if old IM context has composition, key event should be sent to
795 // current context since the user expects so.
796 GtkIMContext* currentContext = GetCurrentContext();
797 if (MOZ_UNLIKELY(!currentContext)) {
798 MOZ_LOG(gIMELog, LogLevel::Error,
799 ("0x%p OnKeyEvent(), FAILED, there are no context", this));
800 return KeyHandlingState::eNotHandled;
803 if (mSetCursorPositionOnKeyEvent) {
804 SetCursorPosition(currentContext);
805 mSetCursorPositionOnKeyEvent = false;
808 // Let's support dead key event even if active keyboard layout also
809 // supports complicated composition like CJK IME.
810 bool isDeadKey =
811 KeymapWrapper::ComputeDOMKeyNameIndex(aEvent) == KEY_NAME_INDEX_Dead;
812 mMaybeInDeadKeySequence |= isDeadKey;
814 // If current context is mSimpleContext, both ibus and fcitx handles key
815 // events synchronously. So, only when current context is mContext which
816 // is GtkIMMulticontext, the key event may be handled by IME asynchronously.
817 bool probablyHandledAsynchronously =
818 mIsIMInAsyncKeyHandlingMode && currentContext == mContext;
820 // If we're not sure whether the event is handled asynchronously, this is
821 // set to true.
822 bool maybeHandledAsynchronously = false;
824 // If aEvent is a synthesized event for async handling, this will be set to
825 // true.
826 bool isHandlingAsyncEvent = false;
828 // If we've decided that the event won't be synthesized asyncrhonously
829 // by IME, but actually IME did it, this is set to true.
830 bool isUnexpectedAsyncEvent = false;
832 // If IM is ibus or fcitx and it handles key events asynchronously,
833 // they mark aEvent->state as "handled by me" when they post key event
834 // to another process. Unfortunately, we need to check this hacky
835 // flag because it's difficult to store all pending key events by
836 // an array or a hashtable.
837 if (probablyHandledAsynchronously) {
838 switch (mIMContextID) {
839 case IMContextID::IBus: {
840 // See src/ibustypes.h
841 static const guint IBUS_IGNORED_MASK = 1 << 25;
842 // If IBUS_IGNORED_MASK was set to aEvent->state, the event
843 // has already been handled by another process and it wasn't
844 // used by IME.
845 isHandlingAsyncEvent = !!(aEvent->state & IBUS_IGNORED_MASK);
846 if (!isHandlingAsyncEvent) {
847 // On some environments, IBUS_IGNORED_MASK flag is not set as
848 // expected. In such case, we keep pusing all events into the queue.
849 // I.e., that causes eating a lot of memory until it's blurred.
850 // Therefore, we need to check whether there is same timestamp event
851 // in the queue. This redundant cost should be low because in most
852 // causes, key events in the queue should be 2 or 4.
853 isHandlingAsyncEvent =
854 mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
855 if (isHandlingAsyncEvent) {
856 MOZ_LOG(gIMELog, LogLevel::Info,
857 ("0x%p OnKeyEvent(), aEvent->state does not have "
858 "IBUS_IGNORED_MASK but "
859 "same event in the queue. So, assuming it's a "
860 "synthesized event",
861 this));
865 // If it's a synthesized event, let's remove it from the posting
866 // event queue first. Otherwise the following blocks cannot use
867 // `break`.
868 if (isHandlingAsyncEvent) {
869 MOZ_LOG(gIMELog, LogLevel::Info,
870 ("0x%p OnKeyEvent(), aEvent->state has IBUS_IGNORED_MASK "
871 "or aEvent is in the "
872 "posting event queue, so, it won't be handled "
873 "asynchronously anymore. Removing "
874 "the posted events from the queue",
875 this));
876 probablyHandledAsynchronously = false;
877 mPostingKeyEvents.RemoveEvent(aEvent);
880 // ibus won't send back key press events in a dead key sequcne.
881 if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
882 probablyHandledAsynchronously = false;
883 if (isHandlingAsyncEvent) {
884 isUnexpectedAsyncEvent = true;
885 break;
887 // Some keyboard layouts which have dead keys may send
888 // "empty" key event to make us call
889 // gtk_im_context_filter_keypress() to commit composed
890 // character during a GDK_KEY_PRESS event dispatching.
891 if (!gdk_keyval_to_unicode(aEvent->keyval) &&
892 !aEvent->hardware_keycode) {
893 isUnexpectedAsyncEvent = true;
894 break;
896 break;
898 // ibus may handle key events synchronously if focused editor is
899 // <input type="password"> or |ime-mode: disabled;|. However, in
900 // some environments, not so actually. Therefore, we need to check
901 // the result of gtk_im_context_filter_keypress() later.
902 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
903 probablyHandledAsynchronously = false;
904 maybeHandledAsynchronously = !isHandlingAsyncEvent;
905 break;
907 break;
909 case IMContextID::Fcitx:
910 case IMContextID::Fcitx5: {
911 // See src/lib/fcitx-utils/keysym.h
912 static const guint FcitxKeyState_IgnoredMask = 1 << 25;
913 // If FcitxKeyState_IgnoredMask was set to aEvent->state,
914 // the event has already been handled by another process and
915 // it wasn't used by IME.
916 isHandlingAsyncEvent = !!(aEvent->state & FcitxKeyState_IgnoredMask);
917 if (!isHandlingAsyncEvent) {
918 // On some environments, FcitxKeyState_IgnoredMask flag *might* be not
919 // set as expected. If there were such cases, we'd keep pusing all
920 // events into the queue. I.e., that would cause eating a lot of
921 // memory until it'd be blurred. Therefore, we should check whether
922 // there is same timestamp event in the queue. This redundant cost
923 // should be low because in most causes, key events in the queue
924 // should be 2 or 4.
925 isHandlingAsyncEvent =
926 mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
927 if (isHandlingAsyncEvent) {
928 MOZ_LOG(gIMELog, LogLevel::Info,
929 ("0x%p OnKeyEvent(), aEvent->state does not have "
930 "FcitxKeyState_IgnoredMask "
931 "but same event in the queue. So, assuming it's a "
932 "synthesized event",
933 this));
937 // fcitx won't send back key press events in a dead key sequcne.
938 if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
939 probablyHandledAsynchronously = false;
940 if (isHandlingAsyncEvent) {
941 isUnexpectedAsyncEvent = true;
942 break;
944 // Some keyboard layouts which have dead keys may send
945 // "empty" key event to make us call
946 // gtk_im_context_filter_keypress() to commit composed
947 // character during a GDK_KEY_PRESS event dispatching.
948 if (!gdk_keyval_to_unicode(aEvent->keyval) &&
949 !aEvent->hardware_keycode) {
950 isUnexpectedAsyncEvent = true;
951 break;
955 // fcitx handles key events asynchronously even if focused
956 // editor cannot use IME actually.
958 if (isHandlingAsyncEvent) {
959 MOZ_LOG(gIMELog, LogLevel::Info,
960 ("0x%p OnKeyEvent(), aEvent->state has "
961 "FcitxKeyState_IgnoredMask or aEvent is in "
962 "the posting event queue, so, it won't be handled "
963 "asynchronously anymore. "
964 "Removing the posted events from the queue",
965 this));
966 probablyHandledAsynchronously = false;
967 mPostingKeyEvents.RemoveEvent(aEvent);
968 break;
970 break;
972 default:
973 MOZ_ASSERT_UNREACHABLE(
974 "IME may handle key event "
975 "asyncrhonously, but not yet confirmed if it comes agian "
976 "actually");
980 if (!isUnexpectedAsyncEvent) {
981 mKeyboardEventWasDispatched = aKeyboardEventWasDispatched;
982 mKeyboardEventWasConsumed = false;
983 } else {
984 // If we didn't expect this event, we've alreday dispatched eKeyDown
985 // event or eKeyUp event for that.
986 mKeyboardEventWasDispatched = true;
987 // And in this case, we need to assume that another key event hasn't
988 // been receivied and mKeyboardEventWasConsumed keeps storing the
989 // dispatched eKeyDown or eKeyUp event's state.
991 mFallbackToKeyEvent = false;
992 mProcessingKeyEvent = aEvent;
993 gboolean isFiltered = gtk_im_context_filter_keypress(currentContext, aEvent);
995 // If we're not sure whether the event is handled by IME asynchronously or
996 // synchronously, we need to trust the result of
997 // gtk_im_context_filter_keypress(). If it consumed and but did nothing,
998 // we can assume that another event will be synthesized.
999 if (!isHandlingAsyncEvent && maybeHandledAsynchronously) {
1000 probablyHandledAsynchronously |=
1001 isFiltered && !mFallbackToKeyEvent && !mKeyboardEventWasDispatched;
1004 if (aEvent->type == GDK_KEY_PRESS) {
1005 if (isFiltered && probablyHandledAsynchronously) {
1006 sWaitingSynthesizedKeyPressHardwareKeyCode = aEvent->hardware_keycode;
1007 } else {
1008 sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
1012 // The caller of this shouldn't handle aEvent anymore if we've dispatched
1013 // composition events or modified content with other events.
1014 bool filterThisEvent = isFiltered && !mFallbackToKeyEvent;
1016 if (IsComposingOnCurrentContext() && !isFiltered &&
1017 aEvent->type == GDK_KEY_PRESS && mDispatchedCompositionString.IsEmpty()) {
1018 // A Hangul input engine for SCIM doesn't emit preedit_end
1019 // signal even when composition string becomes empty. On the
1020 // other hand, we should allow to make composition with empty
1021 // string for other languages because there *might* be such
1022 // IM. For compromising this issue, we should dispatch
1023 // compositionend event, however, we don't need to reset IM
1024 // actually.
1025 // NOTE: Don't dispatch key events as "processed by IME" since
1026 // we need to dispatch keyboard events as IME wasn't handled it.
1027 mProcessingKeyEvent = nullptr;
1028 DispatchCompositionCommitEvent(currentContext, &EmptyString());
1029 mProcessingKeyEvent = aEvent;
1030 // In this case, even though we handle the keyboard event here,
1031 // but we should dispatch keydown event as
1032 filterThisEvent = false;
1035 if (filterThisEvent && !mKeyboardEventWasDispatched) {
1036 // If IME handled the key event but we've not dispatched eKeyDown nor
1037 // eKeyUp event yet, we need to dispatch here unless the key event is
1038 // now being handled by other IME process.
1039 if (!probablyHandledAsynchronously) {
1040 MaybeDispatchKeyEventAsProcessedByIME(eVoidEvent);
1041 // Be aware, the widget might have been gone here.
1043 // If we need to wait reply from IM, IM may send some signals to us
1044 // without sending the key event again. In such case, we need to
1045 // dispatch keyboard events with a copy of aEvent. Therefore, we
1046 // need to use information of this key event to dispatch an KeyDown
1047 // or eKeyUp event later.
1048 else {
1049 MOZ_LOG(gIMELog, LogLevel::Info,
1050 ("0x%p OnKeyEvent(), putting aEvent into the queue...", this));
1051 mPostingKeyEvents.PutEvent(aEvent);
1055 mProcessingKeyEvent = nullptr;
1057 if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) {
1058 // If the key event hasn't been handled by active IME nor keyboard
1059 // layout, we can assume that the dead key sequence has been or was
1060 // ended. Note that we should not reset it when the key event is
1061 // GDK_KEY_RELEASE since it may not be filtered by active keyboard
1062 // layout even in composition.
1063 mMaybeInDeadKeySequence = false;
1066 if (aEvent->type == GDK_KEY_RELEASE) {
1067 if (const GdkEventKey* pendingKeyPressEvent =
1068 mPostingKeyEvents.GetCorrespondingKeyPressEvent(aEvent)) {
1069 MOZ_LOG(gIMELog, LogLevel::Warning,
1070 ("0x%p OnKeyEvent(), forgetting a pending GDK_KEY_PRESS event "
1071 "because GDK_KEY_RELEASE for the event is handled",
1072 this));
1073 mPostingKeyEvents.RemoveEvent(pendingKeyPressEvent);
1077 MOZ_LOG(
1078 gIMELog, LogLevel::Debug,
1079 ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s "
1080 "(isFiltered=%s, mFallbackToKeyEvent=%s, "
1081 "probablyHandledAsynchronously=%s, maybeHandledAsynchronously=%s), "
1082 "mPostingKeyEvents.Length()=%zu, mCompositionState=%s, "
1083 "mMaybeInDeadKeySequence=%s, mKeyboardEventWasDispatched=%s, "
1084 "mKeyboardEventWasConsumed=%s",
1085 this, ToChar(filterThisEvent), ToChar(isFiltered),
1086 ToChar(mFallbackToKeyEvent), ToChar(probablyHandledAsynchronously),
1087 ToChar(maybeHandledAsynchronously), mPostingKeyEvents.Length(),
1088 GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence),
1089 ToChar(mKeyboardEventWasDispatched), ToChar(mKeyboardEventWasConsumed)));
1090 MOZ_LOG(gIMELog, LogLevel::Info, ("<<<<<<<<<<<<<<<<\n\n"));
1092 if (filterThisEvent) {
1093 return KeyHandlingState::eHandled;
1095 // If another call of this method has already dispatched eKeyDown event,
1096 // we should return KeyHandlingState::eNotHandledButEventDispatched because
1097 // the caller should've stopped handling the event if preceding eKeyDown
1098 // event was consumed.
1099 if (aKeyboardEventWasDispatched) {
1100 return KeyHandlingState::eNotHandledButEventDispatched;
1102 if (!mKeyboardEventWasDispatched) {
1103 return KeyHandlingState::eNotHandled;
1105 return mKeyboardEventWasConsumed
1106 ? KeyHandlingState::eNotHandledButEventConsumed
1107 : KeyHandlingState::eNotHandledButEventDispatched;
1110 void IMContextWrapper::OnFocusChangeInGecko(bool aFocus) {
1111 MOZ_LOG(gIMELog, LogLevel::Info,
1112 ("0x%p OnFocusChangeInGecko(aFocus=%s),mCompositionState=%s, "
1113 "mIMEFocusState=%s, mSetInputPurposeAndInputHints=%s",
1114 this, ToChar(aFocus), GetCompositionStateName(),
1115 ToString(mIMEFocusState).c_str(),
1116 ToChar(mSetInputPurposeAndInputHints)));
1118 // We shouldn't carry over the removed string to another editor.
1119 mSelectedStringRemovedByComposition.Truncate();
1120 mContentSelection.reset();
1122 if (aFocus) {
1123 if (mSetInputPurposeAndInputHints) {
1124 mSetInputPurposeAndInputHints = false;
1125 SetInputPurposeAndInputHints();
1127 NotifyIMEOfFocusChange(IMEFocusState::Focused);
1128 } else {
1129 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
1132 // When the focus changes, we need to inform IM about the new cursor
1133 // position. Chinese input methods generally rely on this because they
1134 // usually don't start composition until a character is picked.
1135 if (aFocus && EnsureToCacheContentSelection()) {
1136 SetCursorPosition(GetActiveContext());
1140 void IMContextWrapper::ResetIME() {
1141 MOZ_LOG(gIMELog, LogLevel::Info,
1142 ("0x%p ResetIME(), mCompositionState=%s, mIMEFocusState=%s", this,
1143 GetCompositionStateName(), ToString(mIMEFocusState).c_str()));
1145 GtkIMContext* activeContext = GetActiveContext();
1146 if (MOZ_UNLIKELY(!activeContext)) {
1147 MOZ_LOG(gIMELog, LogLevel::Error,
1148 ("0x%p ResetIME(), FAILED, there are no context", this));
1149 return;
1152 RefPtr<IMContextWrapper> kungFuDeathGrip(this);
1153 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
1155 mPendingResettingIMContext = false;
1156 gtk_im_context_reset(activeContext);
1158 // The last focused window might have been destroyed by a DOM event handler
1159 // which was called by us during a call of gtk_im_context_reset().
1160 if (!lastFocusedWindow ||
1161 NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) ||
1162 lastFocusedWindow->Destroyed()) {
1163 return;
1166 nsAutoString compositionString;
1167 GetCompositionString(activeContext, compositionString);
1169 MOZ_LOG(gIMELog, LogLevel::Debug,
1170 ("0x%p ResetIME() called gtk_im_context_reset(), "
1171 "activeContext=0x%p, mCompositionState=%s, compositionString=%s, "
1172 "mIMEFocusState=%s",
1173 this, activeContext, GetCompositionStateName(),
1174 NS_ConvertUTF16toUTF8(compositionString).get(),
1175 ToString(mIMEFocusState).c_str()));
1177 // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still
1178 // used in Japan!) sends only "preedit_changed" signal with empty
1179 // composition string synchronously. Therefore, if composition string
1180 // is now empty string, we should assume that the IME won't send
1181 // "commit" signal.
1182 if (IsComposing() && compositionString.IsEmpty()) {
1183 // WARNING: The widget might have been gone after this.
1184 DispatchCompositionCommitEvent(activeContext, &EmptyString());
1188 nsresult IMContextWrapper::EndIMEComposition(nsWindow* aCaller) {
1189 if (MOZ_UNLIKELY(IsDestroyed())) {
1190 return NS_OK;
1193 MOZ_LOG(gIMELog, LogLevel::Info,
1194 ("0x%p EndIMEComposition(aCaller=0x%p), "
1195 "mCompositionState=%s",
1196 this, aCaller, GetCompositionStateName()));
1198 if (aCaller != mLastFocusedWindow) {
1199 MOZ_LOG(gIMELog, LogLevel::Error,
1200 ("0x%p EndIMEComposition(), FAILED, the caller isn't "
1201 "focused window, mLastFocusedWindow=0x%p",
1202 this, mLastFocusedWindow));
1203 return NS_OK;
1206 if (!IsComposing()) {
1207 return NS_OK;
1210 // Currently, GTK has API neither to commit nor to cancel composition
1211 // forcibly. Therefore, TextComposition will recompute commit string for
1212 // the request even if native IME will cause unexpected commit string.
1213 // So, we don't need to emulate commit or cancel composition with
1214 // proper composition events.
1215 // XXX ResetIME() might not enough for finishing compositoin on some
1216 // environments. We should emulate focus change too because some IMEs
1217 // may commit or cancel composition at blur.
1218 ResetIME();
1220 return NS_OK;
1223 void IMContextWrapper::OnLayoutChange() {
1224 if (MOZ_UNLIKELY(IsDestroyed())) {
1225 return;
1228 if (IsComposing()) {
1229 SetCursorPosition(GetActiveContext());
1230 } else {
1231 // If not composing, candidate window position is updated before key
1232 // down
1233 mSetCursorPositionOnKeyEvent = true;
1235 mLayoutChanged = true;
1238 void IMContextWrapper::OnUpdateComposition() {
1239 if (MOZ_UNLIKELY(IsDestroyed())) {
1240 return;
1243 if (!IsComposing()) {
1244 // Composition has been committed. So we need update selection for
1245 // caret later
1246 mContentSelection.reset();
1247 EnsureToCacheContentSelection();
1248 mSetCursorPositionOnKeyEvent = true;
1251 // If we've already set candidate window position, we don't need to update
1252 // the position with update composition notification.
1253 if (!mLayoutChanged) {
1254 SetCursorPosition(GetActiveContext());
1258 void IMContextWrapper::SetInputContext(nsWindow* aCaller,
1259 const InputContext* aContext,
1260 const InputContextAction* aAction) {
1261 if (MOZ_UNLIKELY(IsDestroyed())) {
1262 return;
1265 MOZ_LOG(gIMELog, LogLevel::Info,
1266 ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ "
1267 "mEnabled=%s }, mHTMLInputType=%s })",
1268 this, aCaller, ToString(aContext->mIMEState.mEnabled).c_str(),
1269 NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));
1271 if (aCaller != mLastFocusedWindow) {
1272 MOZ_LOG(gIMELog, LogLevel::Error,
1273 ("0x%p SetInputContext(), FAILED, "
1274 "the caller isn't focused window, mLastFocusedWindow=0x%p",
1275 this, mLastFocusedWindow));
1276 return;
1279 if (!mContext) {
1280 MOZ_LOG(gIMELog, LogLevel::Error,
1281 ("0x%p SetInputContext(), FAILED, "
1282 "there are no context",
1283 this));
1284 return;
1287 if (sLastFocusedContext != this) {
1288 mInputContext = *aContext;
1289 MOZ_LOG(gIMELog, LogLevel::Debug,
1290 ("0x%p SetInputContext(), succeeded, "
1291 "but we're not active",
1292 this));
1293 return;
1296 const bool changingEnabledState =
1297 aContext->IsInputAttributeChanged(mInputContext);
1299 // Release current IME focus if IME is enabled.
1300 if (changingEnabledState && mInputContext.mIMEState.IsEditable()) {
1301 if (IsComposing()) {
1302 EndIMEComposition(mLastFocusedWindow);
1304 if (mIMEFocusState == IMEFocusState::Focused) {
1305 NotifyIMEOfFocusChange(IMEFocusState::BlurredWithoutFocusChange);
1309 mInputContext = *aContext;
1310 mSetInputPurposeAndInputHints = false;
1312 if (!changingEnabledState || !mInputContext.mIMEState.IsEditable()) {
1313 return;
1316 // If the input context was temporarily disabled without a focus change,
1317 // it must be ready to query content even if the focused content is in
1318 // a remote process. In this case, we should set IME focus right now.
1319 if (mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
1320 SetInputPurposeAndInputHints();
1321 NotifyIMEOfFocusChange(IMEFocusState::Focused);
1322 return;
1325 // Otherwise, we cannot set input-purpose and input-hints right now because
1326 // setting them may require to set focus immediately for IME own's UI.
1327 // However, at this moment, `ContentCacheInParent` does not have content
1328 // cache, it'll be available after `NOTIFY_IME_OF_FOCUS` notification.
1329 // Therefore, we set them at receiving the notification.
1330 mSetInputPurposeAndInputHints = true;
1333 void IMContextWrapper::SetInputPurposeAndInputHints() {
1334 GtkIMContext* currentContext = GetCurrentContext();
1335 if (!currentContext) {
1336 return;
1339 GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM;
1340 const nsString& inputType = mInputContext.mHTMLInputType;
1341 // Password case has difficult issue. Desktop IMEs disable composition if
1342 // input-purpose is password. For disabling IME on |ime-mode: disabled;|, we
1343 // need to check mEnabled value instead of inputType value. This hack also
1344 // enables composition on <input type="password" style="ime-mode: enabled;">.
1345 // This is right behavior of ime-mode on desktop.
1347 // On the other hand, IME for tablet devices may provide a specific software
1348 // keyboard for password field. If so, the behavior might look strange on
1349 // both:
1350 // <input type="text" style="ime-mode: disabled;">
1351 // <input type="password" style="ime-mode: enabled;">
1353 // Temporarily, we should focus on desktop environment for now. I.e., let's
1354 // ignore tablet devices for now. When somebody reports actual trouble on
1355 // tablet devices, we should try to look for a way to solve actual problem.
1356 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
1357 purpose = GTK_INPUT_PURPOSE_PASSWORD;
1358 } else if (inputType.EqualsLiteral("email")) {
1359 purpose = GTK_INPUT_PURPOSE_EMAIL;
1360 } else if (inputType.EqualsLiteral("url")) {
1361 purpose = GTK_INPUT_PURPOSE_URL;
1362 } else if (inputType.EqualsLiteral("tel")) {
1363 purpose = GTK_INPUT_PURPOSE_PHONE;
1364 } else if (inputType.EqualsLiteral("number")) {
1365 purpose = GTK_INPUT_PURPOSE_NUMBER;
1366 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("decimal")) {
1367 purpose = GTK_INPUT_PURPOSE_NUMBER;
1368 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("email")) {
1369 purpose = GTK_INPUT_PURPOSE_EMAIL;
1370 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("numeric")) {
1371 purpose = GTK_INPUT_PURPOSE_DIGITS;
1372 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("tel")) {
1373 purpose = GTK_INPUT_PURPOSE_PHONE;
1374 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("url")) {
1375 purpose = GTK_INPUT_PURPOSE_URL;
1377 // Search by type and inputmode isn't supported on GTK.
1379 g_object_set(currentContext, "input-purpose", purpose, nullptr);
1381 // Although GtkInputHints is enum type, value is bit field.
1382 gint hints = GTK_INPUT_HINT_NONE;
1383 if (mInputContext.mHTMLInputMode.EqualsLiteral("none")) {
1384 hints |= GTK_INPUT_HINT_INHIBIT_OSK;
1387 if (mInputContext.mAutocapitalize.EqualsLiteral("characters")) {
1388 hints |= GTK_INPUT_HINT_UPPERCASE_CHARS;
1389 } else if (mInputContext.mAutocapitalize.EqualsLiteral("sentences")) {
1390 hints |= GTK_INPUT_HINT_UPPERCASE_SENTENCES;
1391 } else if (mInputContext.mAutocapitalize.EqualsLiteral("words")) {
1392 hints |= GTK_INPUT_HINT_UPPERCASE_WORDS;
1395 g_object_set(currentContext, "input-hints", hints, nullptr);
1398 InputContext IMContextWrapper::GetInputContext() {
1399 mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
1400 return mInputContext;
1403 GtkIMContext* IMContextWrapper::GetCurrentContext() const {
1404 if (IsEnabled()) {
1405 return mContext;
1407 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
1408 return mSimpleContext;
1410 return mDummyContext;
1413 bool IMContextWrapper::IsValidContext(GtkIMContext* aContext) const {
1414 if (!aContext) {
1415 return false;
1417 return aContext == mContext || aContext == mSimpleContext ||
1418 aContext == mDummyContext;
1421 bool IMContextWrapper::IsEnabled() const {
1422 return mInputContext.mIMEState.mEnabled == IMEEnabled::Enabled ||
1423 (!sUseSimpleContext &&
1424 mInputContext.mIMEState.mEnabled == IMEEnabled::Password);
1427 void IMContextWrapper::NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState) {
1428 MOZ_ASSERT_IF(aIMEFocusState == IMEFocusState::BlurredWithoutFocusChange,
1429 mIMEFocusState != IMEFocusState::Blurred);
1430 if (mIMEFocusState == aIMEFocusState) {
1431 return;
1434 MOZ_LOG(gIMELog, LogLevel::Info,
1435 ("0x%p NotifyIMEOfFocusChange(aIMEFocusState=%s), mIMEFocusState=%s, "
1436 "sLastFocusedContext=0x%p",
1437 this, ToString(aIMEFocusState).c_str(),
1438 ToString(mIMEFocusState).c_str(), sLastFocusedContext));
1439 MOZ_ASSERT(!mSetInputPurposeAndInputHints);
1441 // If we've already made IME blurred at setting the input context disabled
1442 // and it's now completely blurred by a focus move, we need only to update
1443 // mIMEFocusState and when the input context gets enabled, we cannot set
1444 // IME focus immediately.
1445 if (aIMEFocusState == IMEFocusState::Blurred &&
1446 mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
1447 mIMEFocusState = IMEFocusState::Blurred;
1448 return;
1451 auto Blur = [&](IMEFocusState aInternalState) {
1452 GtkIMContext* currentContext = GetCurrentContext();
1453 if (MOZ_UNLIKELY(!currentContext)) {
1454 MOZ_LOG(gIMELog, LogLevel::Error,
1455 ("0x%p NotifyIMEOfFocusChange()::Blur(), FAILED, "
1456 "there is no context",
1457 this));
1458 return;
1460 gtk_im_context_focus_out(currentContext);
1461 mIMEFocusState = aInternalState;
1464 if (aIMEFocusState != IMEFocusState::Focused) {
1465 return Blur(aIMEFocusState);
1468 GtkIMContext* currentContext = GetCurrentContext();
1469 if (MOZ_UNLIKELY(!currentContext)) {
1470 MOZ_LOG(gIMELog, LogLevel::Error,
1471 ("0x%p NotifyIMEOfFocusChange(), FAILED, "
1472 "there is no context",
1473 this));
1474 return;
1477 if (sLastFocusedContext && sLastFocusedContext != this) {
1478 sLastFocusedContext->NotifyIMEOfFocusChange(IMEFocusState::Blurred);
1481 sLastFocusedContext = this;
1483 // Forget all posted key events when focus is moved since they shouldn't
1484 // be fired in different editor.
1485 sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
1486 mPostingKeyEvents.Clear();
1488 gtk_im_context_focus_in(currentContext);
1489 mIMEFocusState = aIMEFocusState;
1490 mSetCursorPositionOnKeyEvent = true;
1492 if (!IsEnabled()) {
1493 // We should release IME focus for uim and scim.
1494 // These IMs are using snooper that is released at losing focus.
1495 Blur(IMEFocusState::BlurredWithoutFocusChange);
1499 void IMContextWrapper::OnSelectionChange(
1500 nsWindow* aCaller, const IMENotification& aIMENotification) {
1501 const bool isSelectionRangeChanged =
1502 mContentSelection.isNothing() ||
1503 !aIMENotification.mSelectionChangeData.EqualsRange(
1504 mContentSelection.ref());
1505 mContentSelection =
1506 Some(ContentSelection(aIMENotification.mSelectionChangeData));
1507 const bool retrievedSurroundingSignalReceived =
1508 mRetrieveSurroundingSignalReceived;
1509 mRetrieveSurroundingSignalReceived = false;
1511 if (MOZ_UNLIKELY(IsDestroyed())) {
1512 return;
1515 const IMENotification::SelectionChangeDataBase& selectionChangeData =
1516 aIMENotification.mSelectionChangeData;
1518 MOZ_LOG(gIMELog, LogLevel::Info,
1519 ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
1520 "mSelectionChangeData=%s }), "
1521 "mCompositionState=%s, mIsDeletingSurrounding=%s, "
1522 "mRetrieveSurroundingSignalReceived=%s, isSelectionRangeChanged=%s",
1523 this, aCaller, ToString(selectionChangeData).c_str(),
1524 GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
1525 ToChar(retrievedSurroundingSignalReceived),
1526 ToChar(isSelectionRangeChanged)));
1528 if (aCaller != mLastFocusedWindow) {
1529 MOZ_LOG(gIMELog, LogLevel::Error,
1530 ("0x%p OnSelectionChange(), FAILED, "
1531 "the caller isn't focused window, mLastFocusedWindow=0x%p",
1532 this, mLastFocusedWindow));
1533 return;
1536 if (!IsComposing()) {
1537 // Now we have no composition (mostly situation on calling this method)
1538 // If we have it, it will set by
1539 // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED.
1540 mSetCursorPositionOnKeyEvent = true;
1543 // The focused editor might have placeholder text with normal text node.
1544 // In such case, the text node must be removed from a compositionstart
1545 // event handler. So, we're dispatching eCompositionStart,
1546 // we should ignore selection change notification.
1547 if (mCompositionState == eCompositionState_CompositionStartDispatched) {
1548 if (NS_WARN_IF(mContentSelection.isNothing())) {
1549 MOZ_LOG(gIMELog, LogLevel::Error,
1550 ("0x%p OnSelectionChange(), FAILED, "
1551 "new offset is too large, cannot keep composing",
1552 this));
1553 } else if (mContentSelection->HasRange()) {
1554 // Modify the selection start offset with new offset.
1555 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
1556 // XXX We should modify mSelectedStringRemovedByComposition?
1557 // But how?
1558 MOZ_LOG(gIMELog, LogLevel::Debug,
1559 ("0x%p OnSelectionChange(), ignored, mCompositionStart "
1560 "is updated to %u, the selection change doesn't cause "
1561 "resetting IM context",
1562 this, mCompositionStart));
1563 // And don't reset the IM context.
1564 return;
1565 } else {
1566 MOZ_LOG(
1567 gIMELog, LogLevel::Debug,
1568 ("0x%p OnSelectionChange(), ignored, because of no selection range",
1569 this));
1570 return;
1572 // Otherwise, reset the IM context due to impossible to keep composing.
1575 // If the selection change is caused by deleting surrounding text,
1576 // we shouldn't need to notify IME of selection change.
1577 if (mIsDeletingSurrounding) {
1578 return;
1581 bool occurredBeforeComposition =
1582 IsComposing() && !selectionChangeData.mOccurredDuringComposition &&
1583 !selectionChangeData.mCausedByComposition;
1584 if (occurredBeforeComposition) {
1585 mPendingResettingIMContext = true;
1588 // When the selection change is caused by dispatching composition event,
1589 // selection set event and/or occurred before starting current composition,
1590 // we shouldn't notify IME of that and commit existing composition.
1591 // Don't do this even if selection is not changed actually. For example,
1592 // fcitx has direct input mode which does not insert composing string, but
1593 // inserts commited text for each key sequence (i.e., there is "invisible"
1594 // composition string). In the world after bug 1712269, we don't use a
1595 // set of composition events for this kind of IME. Therefore,
1596 // SelectionChangeData.mCausedByComposition is not expected value for here
1597 // if this call is caused by a preceding commit. And if the preceding commit
1598 // is triggered by a key type for next word, resetting IME state makes fcitx
1599 // discard the pending input for the next word. Thus, we need to check
1600 // whether the selection range is actually changed here.
1601 if (!selectionChangeData.mCausedByComposition &&
1602 !selectionChangeData.mCausedBySelectionEvent && isSelectionRangeChanged &&
1603 !occurredBeforeComposition) {
1604 // Hack for ibus-pinyin. ibus-pinyin will synthesize a set of
1605 // composition which commits with empty string after calling
1606 // gtk_im_context_reset(). Therefore, selecting text causes
1607 // unexpectedly removing it. For preventing it but not breaking the
1608 // other IMEs which use surrounding text, we should call it only when
1609 // surrounding text has been retrieved after last selection range was
1610 // set. If it's not retrieved, that means that current IME doesn't
1611 // have any content cache, so, it must not need the notification of
1612 // selection change.
1613 if (IsComposing() || retrievedSurroundingSignalReceived) {
1614 ResetIME();
1619 /* static */
1620 void IMContextWrapper::OnThemeChanged() {
1621 if (auto* provider = SelectionStyleProvider::GetExistingInstance()) {
1622 provider->OnThemeChanged();
1626 /* static */
1627 void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
1628 IMContextWrapper* aModule) {
1629 aModule->OnStartCompositionNative(aContext);
1632 void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) {
1633 // IME may synthesize composition asynchronously after filtering a
1634 // GDK_KEY_PRESS event. In that case, we should handle composition with
1635 // emulating the usual case, i.e., this is called in the stack of
1636 // OnKeyEvent().
1637 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1638 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1639 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1640 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1641 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1642 KEY_NAME_INDEX_USE_STRING) {
1643 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1644 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1648 MOZ_LOG(gIMELog, LogLevel::Info,
1649 ("0x%p OnStartCompositionNative(aContext=0x%p), "
1650 "current context=0x%p, mComposingContext=0x%p",
1651 this, aContext, GetCurrentContext(), mComposingContext));
1653 // See bug 472635, we should do nothing if IM context doesn't match.
1654 if (GetCurrentContext() != aContext) {
1655 MOZ_LOG(gIMELog, LogLevel::Error,
1656 ("0x%p OnStartCompositionNative(), FAILED, "
1657 "given context doesn't match",
1658 this));
1659 return;
1662 if (mComposingContext && aContext != mComposingContext) {
1663 // XXX For now, we should ignore this odd case, just logging.
1664 MOZ_LOG(gIMELog, LogLevel::Warning,
1665 ("0x%p OnStartCompositionNative(), Warning, "
1666 "there is already a composing context but starting new "
1667 "composition with different context",
1668 this));
1671 // IME may start composition without "preedit_start" signal. Therefore,
1672 // mComposingContext will be initialized in DispatchCompositionStart().
1674 if (!DispatchCompositionStart(aContext)) {
1675 return;
1677 mCompositionTargetRange.mOffset = mCompositionStart;
1678 mCompositionTargetRange.mLength = 0;
1681 /* static */
1682 void IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext,
1683 IMContextWrapper* aModule) {
1684 aModule->OnEndCompositionNative(aContext);
1687 void IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext) {
1688 MOZ_LOG(gIMELog, LogLevel::Info,
1689 ("0x%p OnEndCompositionNative(aContext=0x%p), mComposingContext=0x%p",
1690 this, aContext, mComposingContext));
1692 // See bug 472635, we should do nothing if IM context doesn't match.
1693 // Note that if this is called after focus move, the context may different
1694 // from any our owning context.
1695 if (!IsValidContext(aContext)) {
1696 MOZ_LOG(gIMELog, LogLevel::Error,
1697 ("0x%p OnEndCompositionNative(), FAILED, "
1698 "given context doesn't match with any context",
1699 this));
1700 return;
1703 // If we've not started composition with aContext, we should ignore it.
1704 if (aContext != mComposingContext) {
1705 MOZ_LOG(gIMELog, LogLevel::Warning,
1706 ("0x%p OnEndCompositionNative(), Warning, "
1707 "given context doesn't match with mComposingContext",
1708 this));
1709 return;
1712 g_object_unref(mComposingContext);
1713 mComposingContext = nullptr;
1715 // If we already handled the commit event, we should do nothing here.
1716 if (IsComposing()) {
1717 if (!DispatchCompositionCommitEvent(aContext)) {
1718 // If the widget is destroyed, we should do nothing anymore.
1719 return;
1723 if (mPendingResettingIMContext) {
1724 ResetIME();
1728 /* static */
1729 void IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext,
1730 IMContextWrapper* aModule) {
1731 RefPtr module = aModule;
1732 module->OnChangeCompositionNative(aContext);
1734 if (module->IsDestroyed()) {
1735 // A strong reference is already held during "preedit-changed" emission,
1736 // but _ibus_context_destroy_cb() in ibus 1.5.28 and
1737 // _fcitx_im_context_close_im_cb() in fcitx 4.2.9.9 want their
1738 // GtkIMContexts to live a little longer. See bug 1824634.
1739 NS_DispatchToMainThread(
1740 NS_NewRunnableFunction(__func__, [context = RefPtr{aContext}]() {}));
1744 void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) {
1745 // IME may synthesize composition asynchronously after filtering a
1746 // GDK_KEY_PRESS event. In that case, we should handle composition with
1747 // emulating the usual case, i.e., this is called in the stack of
1748 // OnKeyEvent().
1749 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1750 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1751 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1752 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1753 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1754 KEY_NAME_INDEX_USE_STRING) {
1755 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1756 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1760 MOZ_LOG(gIMELog, LogLevel::Info,
1761 ("0x%p OnChangeCompositionNative(aContext=0x%p), "
1762 "mComposingContext=0x%p",
1763 this, aContext, mComposingContext));
1765 // See bug 472635, we should do nothing if IM context doesn't match.
1766 // Note that if this is called after focus move, the context may different
1767 // from any our owning context.
1768 if (!IsValidContext(aContext)) {
1769 MOZ_LOG(gIMELog, LogLevel::Error,
1770 ("0x%p OnChangeCompositionNative(), FAILED, "
1771 "given context doesn't match with any context",
1772 this));
1773 return;
1776 if (mComposingContext && aContext != mComposingContext) {
1777 // XXX For now, we should ignore this odd case, just logging.
1778 MOZ_LOG(gIMELog, LogLevel::Warning,
1779 ("0x%p OnChangeCompositionNative(), Warning, "
1780 "given context doesn't match with composing context",
1781 this));
1784 nsAutoString compositionString;
1785 GetCompositionString(aContext, compositionString);
1786 if (!IsComposing() && compositionString.IsEmpty()) {
1787 MOZ_LOG(gIMELog, LogLevel::Warning,
1788 ("0x%p OnChangeCompositionNative(), Warning, does nothing "
1789 "because has not started composition and composing string is "
1790 "empty",
1791 this));
1792 mDispatchedCompositionString.Truncate();
1793 return; // Don't start the composition with empty string.
1796 // Be aware, widget can be gone
1797 DispatchCompositionChangeEvent(aContext, compositionString);
1800 /* static */
1801 gboolean IMContextWrapper::OnRetrieveSurroundingCallback(
1802 GtkIMContext* aContext, IMContextWrapper* aModule) {
1803 return aModule->OnRetrieveSurroundingNative(aContext);
1806 gboolean IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext) {
1807 MOZ_LOG(gIMELog, LogLevel::Info,
1808 ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), "
1809 "current context=0x%p",
1810 this, aContext, GetCurrentContext()));
1812 // See bug 472635, we should do nothing if IM context doesn't match.
1813 if (GetCurrentContext() != aContext) {
1814 MOZ_LOG(gIMELog, LogLevel::Error,
1815 ("0x%p OnRetrieveSurroundingNative(), FAILED, "
1816 "given context doesn't match",
1817 this));
1818 return FALSE;
1821 nsAutoString uniStr;
1822 uint32_t cursorPos;
1823 if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) {
1824 return FALSE;
1827 // Despite taking a pointer and a length, IBus wants the string to be
1828 // zero-terminated and doesn't like U+0000 within the string.
1829 uniStr.ReplaceChar(char16_t(0), char16_t(0xFFFD));
1831 NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
1832 uint32_t cursorPosInUTF8 = utf8Str.Length();
1833 AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
1834 gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
1835 cursorPosInUTF8);
1836 mRetrieveSurroundingSignalReceived = true;
1837 return TRUE;
1840 /* static */
1841 gboolean IMContextWrapper::OnDeleteSurroundingCallback(
1842 GtkIMContext* aContext, gint aOffset, gint aNChars,
1843 IMContextWrapper* aModule) {
1844 return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
1847 gboolean IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext,
1848 gint aOffset,
1849 gint aNChars) {
1850 MOZ_LOG(gIMELog, LogLevel::Info,
1851 ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, "
1852 "aNChar=%d), current context=0x%p",
1853 this, aContext, aOffset, aNChars, GetCurrentContext()));
1855 // See bug 472635, we should do nothing if IM context doesn't match.
1856 if (GetCurrentContext() != aContext) {
1857 MOZ_LOG(gIMELog, LogLevel::Error,
1858 ("0x%p OnDeleteSurroundingNative(), FAILED, "
1859 "given context doesn't match",
1860 this));
1861 return FALSE;
1864 AutoRestore<bool> saveDeletingSurrounding(mIsDeletingSurrounding);
1865 mIsDeletingSurrounding = true;
1866 if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) {
1867 return TRUE;
1870 // failed
1871 MOZ_LOG(gIMELog, LogLevel::Error,
1872 ("0x%p OnDeleteSurroundingNative(), FAILED, "
1873 "cannot delete text",
1874 this));
1875 return FALSE;
1878 /* static */
1879 void IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext,
1880 const gchar* aString,
1881 IMContextWrapper* aModule) {
1882 aModule->OnCommitCompositionNative(aContext, aString);
1885 void IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext,
1886 const gchar* aUTF8Char) {
1887 const gchar emptyStr = 0;
1888 const gchar* commitString = aUTF8Char ? aUTF8Char : &emptyStr;
1889 NS_ConvertUTF8toUTF16 utf16CommitString(commitString);
1891 // IME may synthesize composition asynchronously after filtering a
1892 // GDK_KEY_PRESS event. In that case, we should handle composition with
1893 // emulating the usual case, i.e., this is called in the stack of
1894 // OnKeyEvent().
1895 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1896 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1897 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1898 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1899 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1900 KEY_NAME_INDEX_USE_STRING) {
1901 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1902 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1906 MOZ_LOG(gIMELog, LogLevel::Info,
1907 ("0x%p OnCommitCompositionNative(aContext=0x%p), "
1908 "current context=0x%p, active context=0x%p, commitString=\"%s\", "
1909 "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s",
1910 this, aContext, GetCurrentContext(), GetActiveContext(),
1911 commitString, mProcessingKeyEvent, ToChar(IsComposingOn(aContext))));
1913 // See bug 472635, we should do nothing if IM context doesn't match.
1914 if (!IsValidContext(aContext)) {
1915 MOZ_LOG(gIMELog, LogLevel::Error,
1916 ("0x%p OnCommitCompositionNative(), FAILED, "
1917 "given context doesn't match",
1918 this));
1919 return;
1922 // If we are not in composition and committing with empty string,
1923 // we need to do nothing because if we continued to handle this
1924 // signal, we would dispatch compositionstart, text, compositionend
1925 // events with empty string. Of course, they are unnecessary events
1926 // for Web applications and our editor.
1927 if (!IsComposingOn(aContext) && utf16CommitString.IsEmpty()) {
1928 MOZ_LOG(gIMELog, LogLevel::Warning,
1929 ("0x%p OnCommitCompositionNative(), Warning, does nothing "
1930 "because has not started composition and commit string is empty",
1931 this));
1932 return;
1935 // If IME doesn't change their keyevent that generated this commit,
1936 // we should treat that IME didn't handle the key event because
1937 // web applications want to receive "keydown" and "keypress" event
1938 // in such case.
1939 // NOTE: While a key event is being handled, this might be caused on
1940 // current context. Otherwise, this may be caused on active context.
1941 if (!IsComposingOn(aContext) && mProcessingKeyEvent &&
1942 mProcessingKeyEvent->type == GDK_KEY_PRESS &&
1943 aContext == GetCurrentContext()) {
1944 char keyval_utf8[8]; /* should have at least 6 bytes of space */
1945 gint keyval_utf8_len;
1946 guint32 keyval_unicode;
1948 keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval);
1949 keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8);
1950 keyval_utf8[keyval_utf8_len] = '\0';
1952 // If committing string is exactly same as a character which is
1953 // produced by the key, eKeyDown and eKeyPress event should be
1954 // dispatched by the caller of OnKeyEvent() normally. Note that
1955 // mMaybeInDeadKeySequence will be set to false by OnKeyEvent()
1956 // since we set mFallbackToKeyEvent to true here.
1957 if (!strcmp(commitString, keyval_utf8)) {
1958 MOZ_LOG(gIMELog, LogLevel::Info,
1959 ("0x%p OnCommitCompositionNative(), "
1960 "we'll send normal key event",
1961 this));
1962 mFallbackToKeyEvent = true;
1963 return;
1966 // If we're in a dead key sequence, commit string is a character in
1967 // the BMP and mProcessingKeyEvent produces some characters but it's
1968 // not same as committing string, we should dispatch an eKeyPress
1969 // event from here.
1970 WidgetKeyboardEvent keyDownEvent(true, eKeyDown, mLastFocusedWindow);
1971 KeymapWrapper::InitKeyEvent(keyDownEvent, mProcessingKeyEvent, false);
1972 if (mMaybeInDeadKeySequence && utf16CommitString.Length() == 1 &&
1973 keyDownEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
1974 mKeyboardEventWasDispatched = true;
1975 // Anyway, we're not in dead key sequence anymore.
1976 mMaybeInDeadKeySequence = false;
1978 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
1979 nsresult rv = dispatcher->BeginNativeInputTransaction();
1980 if (NS_WARN_IF(NS_FAILED(rv))) {
1981 MOZ_LOG(gIMELog, LogLevel::Error,
1982 ("0x%p OnCommitCompositionNative(), FAILED, "
1983 "due to BeginNativeInputTransaction() failure",
1984 this));
1985 return;
1988 // First, dispatch eKeyDown event.
1989 keyDownEvent.mKeyValue = utf16CommitString;
1990 nsEventStatus status = nsEventStatus_eIgnore;
1991 bool dispatched = dispatcher->DispatchKeyboardEvent(
1992 eKeyDown, keyDownEvent, status, mProcessingKeyEvent);
1993 if (!dispatched || status == nsEventStatus_eConsumeNoDefault) {
1994 mKeyboardEventWasConsumed = true;
1995 MOZ_LOG(gIMELog, LogLevel::Info,
1996 ("0x%p OnCommitCompositionNative(), "
1997 "doesn't dispatch eKeyPress event because the preceding "
1998 "eKeyDown event was not dispatched or was consumed",
1999 this));
2000 return;
2002 if (mLastFocusedWindow != keyDownEvent.mWidget ||
2003 mLastFocusedWindow->Destroyed()) {
2004 MOZ_LOG(gIMELog, LogLevel::Warning,
2005 ("0x%p OnCommitCompositionNative(), Warning, "
2006 "stop dispatching eKeyPress event because the preceding "
2007 "eKeyDown event caused changing focused widget or "
2008 "destroyed",
2009 this));
2010 return;
2012 MOZ_LOG(gIMELog, LogLevel::Info,
2013 ("0x%p OnCommitCompositionNative(), "
2014 "dispatched eKeyDown event for the committed character",
2015 this));
2017 // Next, dispatch eKeyPress event.
2018 dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status,
2019 mProcessingKeyEvent);
2020 MOZ_LOG(gIMELog, LogLevel::Info,
2021 ("0x%p OnCommitCompositionNative(), "
2022 "dispatched eKeyPress event for the committed character",
2023 this));
2024 return;
2028 NS_ConvertUTF8toUTF16 str(commitString);
2029 // Be aware, widget can be gone
2030 DispatchCompositionCommitEvent(aContext, &str);
2033 void IMContextWrapper::GetCompositionString(GtkIMContext* aContext,
2034 nsAString& aCompositionString) {
2035 gchar* preedit_string;
2036 gint cursor_pos;
2037 PangoAttrList* feedback_list;
2038 gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
2039 &cursor_pos);
2040 if (preedit_string && *preedit_string) {
2041 CopyUTF8toUTF16(MakeStringSpan(preedit_string), aCompositionString);
2042 } else {
2043 aCompositionString.Truncate();
2046 MOZ_LOG(gIMELog, LogLevel::Info,
2047 ("0x%p GetCompositionString(aContext=0x%p), "
2048 "aCompositionString=\"%s\"",
2049 this, aContext, preedit_string));
2051 pango_attr_list_unref(feedback_list);
2052 g_free(preedit_string);
2055 bool IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME(
2056 EventMessage aFollowingEvent) {
2057 if (!mLastFocusedWindow) {
2058 return false;
2061 if (!mIsKeySnooped &&
2062 ((!mProcessingKeyEvent && mPostingKeyEvents.IsEmpty()) ||
2063 (mProcessingKeyEvent && mKeyboardEventWasDispatched))) {
2064 return true;
2067 // A "keydown" or "keyup" event handler may change focus with the
2068 // following event. In such case, we need to cancel this composition.
2069 // So, we need to store IM context now because mComposingContext may be
2070 // overwritten with different context if calling this method recursively.
2071 // Note that we don't need to grab the context here because |context|
2072 // will be used only for checking if it's same as mComposingContext.
2073 GtkIMContext* oldCurrentContext = GetCurrentContext();
2074 GtkIMContext* oldComposingContext = mComposingContext;
2076 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2078 if (mProcessingKeyEvent || !mPostingKeyEvents.IsEmpty()) {
2079 if (mProcessingKeyEvent) {
2080 mKeyboardEventWasDispatched = true;
2082 // If we're not handling a key event synchronously, the signal may be
2083 // sent by IME without sending key event to us. In such case, we
2084 // should dispatch keyboard event for the last key event which was
2085 // posted to other IME process.
2086 GdkEventKey* sourceEvent = mProcessingKeyEvent
2087 ? mProcessingKeyEvent
2088 : mPostingKeyEvents.GetFirstEvent();
2090 MOZ_LOG(
2091 gIMELog, LogLevel::Info,
2092 ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
2093 "aFollowingEvent=%s), dispatch %s %s "
2094 "event: { type=%s, keyval=%s, unicode=0x%X, state=%s, "
2095 "time=%u, hardware_keycode=%u, group=%u }",
2096 this, ToChar(aFollowingEvent),
2097 ToChar(sourceEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp),
2098 mProcessingKeyEvent ? "processing" : "posted",
2099 GetEventType(sourceEvent), gdk_keyval_name(sourceEvent->keyval),
2100 gdk_keyval_to_unicode(sourceEvent->keyval),
2101 GetEventStateName(sourceEvent->state, mIMContextID).get(),
2102 sourceEvent->time, sourceEvent->hardware_keycode, sourceEvent->group));
2104 // Let's dispatch eKeyDown event or eKeyUp event now. Note that only
2105 // when we're not in a dead key composition, we should mark the
2106 // eKeyDown and eKeyUp event as "processed by IME" since we should
2107 // expose raw keyCode and key value to web apps the key event is a
2108 // part of a dead key sequence.
2109 // FYI: We should ignore if default of preceding keydown or keyup
2110 // event is prevented since even on the other browsers, web
2111 // applications cannot cancel the following composition event.
2112 // Spec bug: https://github.com/w3c/uievents/issues/180
2113 KeymapWrapper::DispatchKeyDownOrKeyUpEvent(lastFocusedWindow, sourceEvent,
2114 !mMaybeInDeadKeySequence,
2115 &mKeyboardEventWasConsumed);
2116 MOZ_LOG(gIMELog, LogLevel::Info,
2117 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup "
2118 "event is dispatched",
2119 this));
2121 if (!mProcessingKeyEvent) {
2122 MOZ_LOG(gIMELog, LogLevel::Info,
2123 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), removing first "
2124 "event from the queue",
2125 this));
2126 mPostingKeyEvents.RemoveEvent(sourceEvent);
2128 } else {
2129 MOZ_ASSERT(mIsKeySnooped);
2130 // Currently, we support key snooper mode of uim and wayland only.
2131 MOZ_ASSERT(mIMContextID == IMContextID::Uim ||
2132 mIMContextID == IMContextID::Wayland);
2133 // uim sends "preedit_start" signal and "preedit_changed" separately
2134 // at starting composition, "commit" and "preedit_end" separately at
2135 // committing composition.
2137 // Currently, we should dispatch only fake eKeyDown event because
2138 // we cannot decide which is the last signal of each key operation
2139 // and Chromium also dispatches only "keydown" event in this case.
2140 bool dispatchFakeKeyDown = false;
2141 switch (aFollowingEvent) {
2142 case eCompositionStart:
2143 case eCompositionCommit:
2144 case eCompositionCommitAsIs:
2145 case eContentCommandInsertText:
2146 dispatchFakeKeyDown = true;
2147 break;
2148 // XXX Unfortunately, I don't have a good idea to prevent to
2149 // dispatch redundant eKeyDown event for eCompositionStart
2150 // immediately after "delete_surrounding" signal. However,
2151 // not dispatching eKeyDown event is worse than dispatching
2152 // redundant eKeyDown events.
2153 case eContentCommandDelete:
2154 dispatchFakeKeyDown = true;
2155 break;
2156 // We need to prevent to dispatch redundant eKeyDown event for
2157 // eCompositionChange immediately after eCompositionStart. So,
2158 // We should not dispatch eKeyDown event if dispatched composition
2159 // string is still empty string.
2160 case eCompositionChange:
2161 dispatchFakeKeyDown = !mDispatchedCompositionString.IsEmpty();
2162 break;
2163 default:
2164 MOZ_ASSERT_UNREACHABLE("Do you forget to handle the case?");
2165 break;
2168 if (dispatchFakeKeyDown) {
2169 WidgetKeyboardEvent fakeKeyDownEvent(true, eKeyDown, lastFocusedWindow);
2170 fakeKeyDownEvent.mKeyCode = NS_VK_PROCESSKEY;
2171 fakeKeyDownEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
2172 // It's impossible to get physical key information in this case but
2173 // this should be okay since web apps shouldn't do anything with
2174 // physical key information during composition.
2175 fakeKeyDownEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN;
2177 MOZ_LOG(gIMELog, LogLevel::Info,
2178 ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
2179 "aFollowingEvent=%s), dispatch fake eKeyDown event",
2180 this, ToChar(aFollowingEvent)));
2182 KeymapWrapper::DispatchKeyDownOrKeyUpEvent(
2183 lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed);
2184 MOZ_LOG(gIMELog, LogLevel::Info,
2185 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), "
2186 "fake keydown event is dispatched",
2187 this));
2191 if (lastFocusedWindow->IsDestroyed() ||
2192 lastFocusedWindow != mLastFocusedWindow) {
2193 MOZ_LOG(gIMELog, LogLevel::Warning,
2194 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the "
2195 "focused widget was destroyed/changed by a key event",
2196 this));
2197 return false;
2200 // If the dispatched keydown event caused moving focus and that also
2201 // caused changing active context, we need to cancel composition here.
2202 if (GetCurrentContext() != oldCurrentContext) {
2203 MOZ_LOG(gIMELog, LogLevel::Warning,
2204 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the key "
2205 "event causes changing active IM context",
2206 this));
2207 if (mComposingContext == oldComposingContext) {
2208 // Only when the context is still composing, we should call
2209 // ResetIME() here. Otherwise, it should've already been
2210 // cleaned up.
2211 ResetIME();
2213 return false;
2216 return true;
2219 bool IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext) {
2220 MOZ_LOG(gIMELog, LogLevel::Info,
2221 ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext));
2223 if (IsComposing()) {
2224 MOZ_LOG(gIMELog, LogLevel::Error,
2225 ("0x%p DispatchCompositionStart(), FAILED, "
2226 "we're already in composition",
2227 this));
2228 return true;
2231 if (!mLastFocusedWindow) {
2232 MOZ_LOG(gIMELog, LogLevel::Error,
2233 ("0x%p DispatchCompositionStart(), FAILED, "
2234 "there are no focused window in this module",
2235 this));
2236 return false;
2239 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
2240 MOZ_LOG(gIMELog, LogLevel::Error,
2241 ("0x%p DispatchCompositionStart(), FAILED, "
2242 "cannot query the selection offset",
2243 this));
2244 return false;
2247 if (NS_WARN_IF(!mContentSelection->HasRange())) {
2248 MOZ_LOG(gIMELog, LogLevel::Error,
2249 ("0x%p DispatchCompositionStart(), FAILED, "
2250 "due to no selection",
2251 this));
2252 return false;
2255 mComposingContext = static_cast<GtkIMContext*>(g_object_ref(aContext));
2256 MOZ_ASSERT(mComposingContext);
2258 // Keep the last focused window alive
2259 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2261 // XXX The composition start point might be changed by composition events
2262 // even though we strongly hope it doesn't happen.
2263 // Every composition event should have the start offset for the result
2264 // because it may high cost if we query the offset every time.
2265 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
2266 mDispatchedCompositionString.Truncate();
2268 // If this composition is started by a key press, we need to dispatch
2269 // eKeyDown or eKeyUp event before dispatching eCompositionStart event.
2270 // Note that dispatching a keyboard event which is marked as "processed
2271 // by IME" is okay since Chromium also dispatches keyboard event as so.
2272 if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionStart)) {
2273 MOZ_LOG(gIMELog, LogLevel::Warning,
2274 ("0x%p DispatchCompositionStart(), Warning, "
2275 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2276 this));
2277 return false;
2280 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
2281 nsresult rv = dispatcher->BeginNativeInputTransaction();
2282 if (NS_WARN_IF(NS_FAILED(rv))) {
2283 MOZ_LOG(gIMELog, LogLevel::Error,
2284 ("0x%p DispatchCompositionStart(), FAILED, "
2285 "due to BeginNativeInputTransaction() failure",
2286 this));
2287 return false;
2290 static bool sHasSetTelemetry = false;
2291 if (!sHasSetTelemetry) {
2292 sHasSetTelemetry = true;
2293 NS_ConvertUTF8toUTF16 im(GetIMName());
2294 // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
2295 if (im.Length() > 72) {
2296 if (NS_IS_SURROGATE_PAIR(im[72 - 2], im[72 - 1])) {
2297 im.Truncate(72 - 2);
2298 } else {
2299 im.Truncate(72 - 1);
2301 // U+2026 is "..."
2302 im.Append(char16_t(0x2026));
2304 Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_LINUX, im,
2305 true);
2308 MOZ_LOG(gIMELog, LogLevel::Debug,
2309 ("0x%p DispatchCompositionStart(), dispatching "
2310 "compositionstart... (mCompositionStart=%u)",
2311 this, mCompositionStart));
2312 mCompositionState = eCompositionState_CompositionStartDispatched;
2313 nsEventStatus status;
2314 dispatcher->StartComposition(status);
2315 if (lastFocusedWindow->IsDestroyed() ||
2316 lastFocusedWindow != mLastFocusedWindow) {
2317 MOZ_LOG(gIMELog, LogLevel::Error,
2318 ("0x%p DispatchCompositionStart(), FAILED, the focused "
2319 "widget was destroyed/changed by compositionstart event",
2320 this));
2321 return false;
2324 return true;
2327 bool IMContextWrapper::DispatchCompositionChangeEvent(
2328 GtkIMContext* aContext, const nsAString& aCompositionString) {
2329 MOZ_LOG(
2330 gIMELog, LogLevel::Info,
2331 ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext));
2333 if (!mLastFocusedWindow) {
2334 MOZ_LOG(gIMELog, LogLevel::Error,
2335 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2336 "there are no focused window in this module",
2337 this));
2338 return false;
2341 if (!IsComposing()) {
2342 MOZ_LOG(gIMELog, LogLevel::Debug,
2343 ("0x%p DispatchCompositionChangeEvent(), the composition "
2344 "wasn't started, force starting...",
2345 this));
2346 if (!DispatchCompositionStart(aContext)) {
2347 return false;
2350 // If this composition string change caused by a key press, we need to
2351 // dispatch eKeyDown or eKeyUp before dispatching eCompositionChange event.
2352 else if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionChange)) {
2353 MOZ_LOG(gIMELog, LogLevel::Warning,
2354 ("0x%p DispatchCompositionChangeEvent(), Warning, "
2355 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2356 this));
2357 return false;
2360 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
2361 nsresult rv = dispatcher->BeginNativeInputTransaction();
2362 if (NS_WARN_IF(NS_FAILED(rv))) {
2363 MOZ_LOG(gIMELog, LogLevel::Error,
2364 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2365 "due to BeginNativeInputTransaction() failure",
2366 this));
2367 return false;
2370 // Store the selected string which will be removed by following
2371 // compositionchange event.
2372 if (mCompositionState == eCompositionState_CompositionStartDispatched) {
2373 if (NS_WARN_IF(!EnsureToCacheContentSelection(
2374 &mSelectedStringRemovedByComposition))) {
2375 // XXX How should we behave in this case??
2376 } else if (mContentSelection->HasRange()) {
2377 // XXX We should assume, for now, any web applications don't change
2378 // selection at handling this compositionchange event.
2379 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
2380 } else {
2381 // If there is no selection range, we should keep previously storing
2382 // mCompositionStart.
2386 RefPtr<TextRangeArray> rangeArray =
2387 CreateTextRangeArray(aContext, aCompositionString);
2389 rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray);
2390 if (NS_WARN_IF(NS_FAILED(rv))) {
2391 MOZ_LOG(gIMELog, LogLevel::Error,
2392 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2393 "due to SetPendingComposition() failure",
2394 this));
2395 return false;
2398 mCompositionState = eCompositionState_CompositionChangeEventDispatched;
2400 // We cannot call SetCursorPosition for e10s-aware.
2401 // DispatchEvent is async on e10s, so composition rect isn't updated now
2402 // on tab parent.
2403 mDispatchedCompositionString = aCompositionString;
2404 mLayoutChanged = false;
2405 mCompositionTargetRange.mOffset =
2406 mCompositionStart + rangeArray->TargetClauseOffset();
2407 mCompositionTargetRange.mLength = rangeArray->TargetClauseLength();
2409 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2410 nsEventStatus status;
2411 rv = dispatcher->FlushPendingComposition(status);
2412 if (NS_WARN_IF(NS_FAILED(rv))) {
2413 MOZ_LOG(gIMELog, LogLevel::Error,
2414 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2415 "due to FlushPendingComposition() failure",
2416 this));
2417 return false;
2420 if (lastFocusedWindow->IsDestroyed() ||
2421 lastFocusedWindow != mLastFocusedWindow) {
2422 MOZ_LOG(gIMELog, LogLevel::Error,
2423 ("0x%p DispatchCompositionChangeEvent(), FAILED, the "
2424 "focused widget was destroyed/changed by "
2425 "compositionchange event",
2426 this));
2427 return false;
2429 return true;
2432 bool IMContextWrapper::DispatchCompositionCommitEvent(
2433 GtkIMContext* aContext, const nsAString* aCommitString) {
2434 MOZ_LOG(gIMELog, LogLevel::Info,
2435 ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, "
2436 "aCommitString=0x%p, (\"%s\"))",
2437 this, aContext, aCommitString,
2438 aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));
2440 if (!mLastFocusedWindow) {
2441 MOZ_LOG(gIMELog, LogLevel::Error,
2442 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2443 "there are no focused window in this module",
2444 this));
2445 return false;
2448 // TODO: We need special care to handle request to commit composition
2449 // by content while we're committing composition because we have
2450 // commit string information now but IME may not have composition
2451 // anymore. Therefore, we may not be able to handle commit as
2452 // expected. However, this is rare case because this situation
2453 // never occurs with remote content. So, it's okay to fix this
2454 // issue later. (Perhaps, TextEventDisptcher should do it for
2455 // all platforms. E.g., creating WillCommitComposition()?)
2456 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2457 RefPtr<TextEventDispatcher> dispatcher;
2458 if (!IsComposing() &&
2459 !StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
2460 if (!aCommitString || aCommitString->IsEmpty()) {
2461 MOZ_LOG(gIMELog, LogLevel::Error,
2462 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2463 "did nothing due to inserting empty string without composition",
2464 this));
2465 return true;
2467 if (MOZ_UNLIKELY(!EnsureToCacheContentSelection())) {
2468 MOZ_LOG(gIMELog, LogLevel::Warning,
2469 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2470 "Failed to cache selection before dispatching "
2471 "eContentCommandInsertText event",
2472 this));
2474 if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandInsertText)) {
2475 MOZ_LOG(gIMELog, LogLevel::Warning,
2476 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2477 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2478 this));
2479 return false;
2481 // Emulate selection until receiving actual selection range. This is
2482 // important for OnSelectionChange. If selection is not changed by web
2483 // apps, i.e., selection range is same as what selection expects, we
2484 // shouldn't reset IME because the trigger of causing this commit may be an
2485 // input for next composition and we shouldn't cancel it.
2486 if (mContentSelection.isSome()) {
2487 mContentSelection->Collapse(
2488 (mContentSelection->HasRange()
2489 ? mContentSelection->OffsetAndDataRef().StartOffset()
2490 : mCompositionStart) +
2491 aCommitString->Length());
2492 MOZ_LOG(gIMELog, LogLevel::Info,
2493 ("0x%p DispatchCompositionCommitEvent(), mContentSelection=%s",
2494 this, ToString(mContentSelection).c_str()));
2496 MOZ_ASSERT(!dispatcher);
2497 } else {
2498 if (!IsComposing()) {
2499 if (!aCommitString || aCommitString->IsEmpty()) {
2500 MOZ_LOG(gIMELog, LogLevel::Error,
2501 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2502 "there is no composition and empty commit string",
2503 this));
2504 return true;
2506 MOZ_LOG(gIMELog, LogLevel::Debug,
2507 ("0x%p DispatchCompositionCommitEvent(), "
2508 "the composition wasn't started, force starting...",
2509 this));
2510 if (!DispatchCompositionStart(aContext)) {
2511 return false;
2514 // If this commit caused by a key press, we need to dispatch eKeyDown or
2515 // eKeyUp before dispatching composition events.
2516 else if (!MaybeDispatchKeyEventAsProcessedByIME(
2517 aCommitString ? eCompositionCommit : eCompositionCommitAsIs)) {
2518 MOZ_LOG(gIMELog, LogLevel::Warning,
2519 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2520 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2521 this));
2522 mCompositionState = eCompositionState_NotComposing;
2523 return false;
2526 dispatcher = GetTextEventDispatcher();
2527 MOZ_ASSERT(dispatcher);
2528 nsresult rv = dispatcher->BeginNativeInputTransaction();
2529 if (NS_WARN_IF(NS_FAILED(rv))) {
2530 MOZ_LOG(gIMELog, LogLevel::Error,
2531 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2532 "due to BeginNativeInputTransaction() failure",
2533 this));
2534 return false;
2537 // Emulate selection until receiving actual selection range.
2538 const uint32_t offsetToPutCaret =
2539 mCompositionStart + (aCommitString
2540 ? aCommitString->Length()
2541 : mDispatchedCompositionString.Length());
2542 if (mContentSelection.isSome()) {
2543 mContentSelection->Collapse(offsetToPutCaret);
2544 } else {
2545 // TODO: We should guarantee that there should be at least fake selection
2546 // for IME at here. Then, we can keep the last writing mode.
2547 mContentSelection.emplace(offsetToPutCaret, WritingMode());
2551 mCompositionState = eCompositionState_NotComposing;
2552 // Reset dead key sequence too because GTK doesn't support dead key chain
2553 // (i.e., a key press doesn't cause both producing some characters and
2554 // restarting new dead key sequence at one time). So, committing
2555 // composition means end of a dead key sequence.
2556 mMaybeInDeadKeySequence = false;
2557 mCompositionStart = UINT32_MAX;
2558 mCompositionTargetRange.Clear();
2559 mDispatchedCompositionString.Truncate();
2560 mSelectedStringRemovedByComposition.Truncate();
2562 if (!dispatcher) {
2563 MOZ_ASSERT(aCommitString);
2564 MOZ_ASSERT(!aCommitString->IsEmpty());
2565 nsEventStatus status = nsEventStatus_eIgnore;
2566 WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText,
2567 lastFocusedWindow);
2568 insertTextEvent.mString.emplace(*aCommitString);
2569 lastFocusedWindow->DispatchEvent(&insertTextEvent, status);
2571 if (!insertTextEvent.mSucceeded) {
2572 MOZ_LOG(gIMELog, LogLevel::Error,
2573 ("0x%p DispatchCompositionChangeEvent(), FAILED, inserting "
2574 "text failed",
2575 this));
2576 return false;
2578 } else {
2579 nsEventStatus status = nsEventStatus_eIgnore;
2580 nsresult rv = dispatcher->CommitComposition(status, aCommitString);
2581 if (NS_WARN_IF(NS_FAILED(rv))) {
2582 MOZ_LOG(gIMELog, LogLevel::Error,
2583 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2584 "due to CommitComposition() failure",
2585 this));
2586 return false;
2590 if (lastFocusedWindow->IsDestroyed() ||
2591 lastFocusedWindow != mLastFocusedWindow) {
2592 MOZ_LOG(gIMELog, LogLevel::Error,
2593 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2594 "the focused widget was destroyed/changed by "
2595 "compositioncommit event",
2596 this));
2597 return false;
2600 return true;
2603 already_AddRefed<TextRangeArray> IMContextWrapper::CreateTextRangeArray(
2604 GtkIMContext* aContext, const nsAString& aCompositionString) {
2605 MOZ_LOG(gIMELog, LogLevel::Info,
2606 ("0x%p CreateTextRangeArray(aContext=0x%p, "
2607 "aCompositionString=\"%s\" (Length()=%zu))",
2608 this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(),
2609 aCompositionString.Length()));
2611 RefPtr<TextRangeArray> textRangeArray = new TextRangeArray();
2613 gchar* preedit_string;
2614 gint cursor_pos_in_chars;
2615 PangoAttrList* feedback_list;
2616 gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
2617 &cursor_pos_in_chars);
2618 if (!preedit_string || !*preedit_string) {
2619 if (!aCompositionString.IsEmpty()) {
2620 MOZ_LOG(gIMELog, LogLevel::Error,
2621 ("0x%p CreateTextRangeArray(), FAILED, due to "
2622 "preedit_string is null",
2623 this));
2625 pango_attr_list_unref(feedback_list);
2626 g_free(preedit_string);
2627 return textRangeArray.forget();
2630 // Convert caret offset from offset in characters to offset in UTF-16
2631 // string. If we couldn't proper offset in UTF-16 string, we should
2632 // assume that the caret is at the end of the composition string.
2633 uint32_t caretOffsetInUTF16 = aCompositionString.Length();
2634 if (NS_WARN_IF(cursor_pos_in_chars < 0)) {
2635 // Note that this case is undocumented. We should assume that the
2636 // caret is at the end of the composition string.
2637 } else if (cursor_pos_in_chars == 0) {
2638 caretOffsetInUTF16 = 0;
2639 } else {
2640 gchar* charAfterCaret =
2641 g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars);
2642 if (NS_WARN_IF(!charAfterCaret)) {
2643 MOZ_LOG(gIMELog, LogLevel::Warning,
2644 ("0x%p CreateTextRangeArray(), failed to get UTF-8 "
2645 "string before the caret (cursor_pos_in_chars=%d)",
2646 this, cursor_pos_in_chars));
2647 } else {
2648 glong caretOffset = 0;
2649 gunichar2* utf16StrBeforeCaret =
2650 g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string,
2651 nullptr, &caretOffset, nullptr);
2652 if (NS_WARN_IF(!utf16StrBeforeCaret) || NS_WARN_IF(caretOffset < 0)) {
2653 MOZ_LOG(gIMELog, LogLevel::Warning,
2654 ("0x%p CreateTextRangeArray(), WARNING, failed to "
2655 "convert to UTF-16 string before the caret "
2656 "(cursor_pos_in_chars=%d, caretOffset=%ld)",
2657 this, cursor_pos_in_chars, caretOffset));
2658 } else {
2659 caretOffsetInUTF16 = static_cast<uint32_t>(caretOffset);
2660 uint32_t compositionStringLength = aCompositionString.Length();
2661 if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) {
2662 MOZ_LOG(gIMELog, LogLevel::Warning,
2663 ("0x%p CreateTextRangeArray(), WARNING, "
2664 "caretOffsetInUTF16=%u is larger than "
2665 "compositionStringLength=%u",
2666 this, caretOffsetInUTF16, compositionStringLength));
2667 caretOffsetInUTF16 = compositionStringLength;
2670 if (utf16StrBeforeCaret) {
2671 g_free(utf16StrBeforeCaret);
2676 PangoAttrIterator* iter;
2677 iter = pango_attr_list_get_iterator(feedback_list);
2678 if (!iter) {
2679 MOZ_LOG(gIMELog, LogLevel::Error,
2680 ("0x%p CreateTextRangeArray(), FAILED, iterator couldn't "
2681 "be allocated",
2682 this));
2683 pango_attr_list_unref(feedback_list);
2684 g_free(preedit_string);
2685 return textRangeArray.forget();
2688 uint32_t minOffsetOfClauses = aCompositionString.Length();
2689 uint32_t maxOffsetOfClauses = 0;
2690 do {
2691 TextRange range;
2692 if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) {
2693 continue;
2695 MOZ_ASSERT(range.Length());
2696 minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset);
2697 maxOffsetOfClauses = std::max(maxOffsetOfClauses, range.mEndOffset);
2698 textRangeArray->AppendElement(range);
2699 } while (pango_attr_iterator_next(iter));
2701 // If the IME doesn't define clause from the start of the composition,
2702 // we should insert dummy clause information since TextRangeArray assumes
2703 // that there must be a clause whose start is 0 when there is one or
2704 // more clauses.
2705 if (minOffsetOfClauses) {
2706 TextRange dummyClause;
2707 dummyClause.mStartOffset = 0;
2708 dummyClause.mEndOffset = minOffsetOfClauses;
2709 dummyClause.mRangeType = TextRangeType::eRawClause;
2710 textRangeArray->InsertElementAt(0, dummyClause);
2711 maxOffsetOfClauses = std::max(maxOffsetOfClauses, dummyClause.mEndOffset);
2712 MOZ_LOG(gIMELog, LogLevel::Warning,
2713 ("0x%p CreateTextRangeArray(), inserting a dummy clause "
2714 "at the beginning of the composition string mStartOffset=%u, "
2715 "mEndOffset=%u, mRangeType=%s",
2716 this, dummyClause.mStartOffset, dummyClause.mEndOffset,
2717 ToChar(dummyClause.mRangeType)));
2720 // If the IME doesn't define clause at end of the composition, we should
2721 // insert dummy clause information since TextRangeArray assumes that there
2722 // must be a clase whose end is the length of the composition string when
2723 // there is one or more clauses.
2724 if (!textRangeArray->IsEmpty() &&
2725 maxOffsetOfClauses < aCompositionString.Length()) {
2726 TextRange dummyClause;
2727 dummyClause.mStartOffset = maxOffsetOfClauses;
2728 dummyClause.mEndOffset = aCompositionString.Length();
2729 dummyClause.mRangeType = TextRangeType::eRawClause;
2730 textRangeArray->AppendElement(dummyClause);
2731 MOZ_LOG(gIMELog, LogLevel::Warning,
2732 ("0x%p CreateTextRangeArray(), inserting a dummy clause "
2733 "at the end of the composition string mStartOffset=%u, "
2734 "mEndOffset=%u, mRangeType=%s",
2735 this, dummyClause.mStartOffset, dummyClause.mEndOffset,
2736 ToChar(dummyClause.mRangeType)));
2739 TextRange range;
2740 range.mStartOffset = range.mEndOffset = caretOffsetInUTF16;
2741 range.mRangeType = TextRangeType::eCaret;
2742 textRangeArray->AppendElement(range);
2743 MOZ_LOG(
2744 gIMELog, LogLevel::Debug,
2745 ("0x%p CreateTextRangeArray(), mStartOffset=%u, "
2746 "mEndOffset=%u, mRangeType=%s",
2747 this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
2749 pango_attr_iterator_destroy(iter);
2750 pango_attr_list_unref(feedback_list);
2751 g_free(preedit_string);
2753 return textRangeArray.forget();
2756 /* static */
2757 nscolor IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor) {
2758 PangoColor& pangoColor = aPangoAttrColor->color;
2759 uint8_t r = pangoColor.red / 0x100;
2760 uint8_t g = pangoColor.green / 0x100;
2761 uint8_t b = pangoColor.blue / 0x100;
2762 return NS_RGB(r, g, b);
2765 bool IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter,
2766 const gchar* aUTF8CompositionString,
2767 uint32_t aUTF16CaretOffset,
2768 TextRange& aTextRange) const {
2769 // Set the range offsets in UTF-16 string.
2770 gint utf8ClauseStart, utf8ClauseEnd;
2771 pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd);
2772 if (utf8ClauseStart == utf8ClauseEnd) {
2773 MOZ_LOG(gIMELog, LogLevel::Error,
2774 ("0x%p SetTextRange(), FAILED, due to collapsed range", this));
2775 return false;
2778 if (!utf8ClauseStart) {
2779 aTextRange.mStartOffset = 0;
2780 } else {
2781 glong utf16PreviousClausesLength;
2782 gunichar2* utf16PreviousClausesString =
2783 g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr,
2784 &utf16PreviousClausesLength, nullptr);
2786 if (NS_WARN_IF(!utf16PreviousClausesString)) {
2787 MOZ_LOG(gIMELog, LogLevel::Error,
2788 ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
2789 "failure (retrieving previous string of current clause)",
2790 this));
2791 return false;
2794 aTextRange.mStartOffset = utf16PreviousClausesLength;
2795 g_free(utf16PreviousClausesString);
2798 glong utf16CurrentClauseLength;
2799 gunichar2* utf16CurrentClauseString = g_utf8_to_utf16(
2800 aUTF8CompositionString + utf8ClauseStart, utf8ClauseEnd - utf8ClauseStart,
2801 nullptr, &utf16CurrentClauseLength, nullptr);
2803 if (NS_WARN_IF(!utf16CurrentClauseString)) {
2804 MOZ_LOG(gIMELog, LogLevel::Error,
2805 ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
2806 "failure (retrieving current clause)",
2807 this));
2808 return false;
2811 // iBus Chewing IME tells us that there is an empty clause at the end of
2812 // the composition string but we should ignore it since our code doesn't
2813 // assume that there is an empty clause.
2814 if (!utf16CurrentClauseLength) {
2815 MOZ_LOG(gIMELog, LogLevel::Warning,
2816 ("0x%p SetTextRange(), FAILED, due to current clause length "
2817 "is 0",
2818 this));
2819 return false;
2822 aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength;
2823 g_free(utf16CurrentClauseString);
2824 utf16CurrentClauseString = nullptr;
2826 // Set styles
2827 TextRangeStyle& style = aTextRange.mRangeStyle;
2829 // Underline
2830 PangoAttrInt* attrUnderline = reinterpret_cast<PangoAttrInt*>(
2831 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE));
2832 if (attrUnderline) {
2833 switch (attrUnderline->value) {
2834 case PANGO_UNDERLINE_NONE:
2835 style.mLineStyle = TextRangeStyle::LineStyle::None;
2836 break;
2837 case PANGO_UNDERLINE_DOUBLE:
2838 style.mLineStyle = TextRangeStyle::LineStyle::Double;
2839 break;
2840 case PANGO_UNDERLINE_ERROR:
2841 style.mLineStyle = TextRangeStyle::LineStyle::Wavy;
2842 break;
2843 case PANGO_UNDERLINE_SINGLE:
2844 case PANGO_UNDERLINE_LOW:
2845 style.mLineStyle = TextRangeStyle::LineStyle::Solid;
2846 break;
2847 default:
2848 MOZ_LOG(gIMELog, LogLevel::Warning,
2849 ("0x%p SetTextRange(), retrieved unknown underline "
2850 "style: %d",
2851 this, attrUnderline->value));
2852 style.mLineStyle = TextRangeStyle::LineStyle::Solid;
2853 break;
2855 style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
2857 // Underline color
2858 PangoAttrColor* attrUnderlineColor = reinterpret_cast<PangoAttrColor*>(
2859 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE_COLOR));
2860 if (attrUnderlineColor) {
2861 style.mUnderlineColor = ToNscolor(attrUnderlineColor);
2862 style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR;
2864 } else {
2865 style.mLineStyle = TextRangeStyle::LineStyle::None;
2866 style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
2869 // Don't set colors if they are not specified. They should be computed by
2870 // textframe if only one of the colors are specified.
2872 // Foreground color (text color)
2873 PangoAttrColor* attrForeground = reinterpret_cast<PangoAttrColor*>(
2874 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND));
2875 if (attrForeground) {
2876 style.mForegroundColor = ToNscolor(attrForeground);
2877 style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR;
2880 // Background color
2881 PangoAttrColor* attrBackground = reinterpret_cast<PangoAttrColor*>(
2882 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND));
2883 if (attrBackground) {
2884 style.mBackgroundColor = ToNscolor(attrBackground);
2885 style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR;
2889 * We need to judge the meaning of the clause for a11y. Before we support
2890 * IME specific composition string style, we used following rules:
2892 * 1: If attrUnderline and attrForground are specified, we assumed the
2893 * clause is TextRangeType::eSelectedClause.
2894 * 2: If only attrUnderline is specified, we assumed the clause is
2895 * TextRangeType::eConvertedClause.
2896 * 3: If only attrForground is specified, we assumed the clause is
2897 * TextRangeType::eSelectedRawClause.
2898 * 4: If neither attrUnderline nor attrForeground is specified, we assumed
2899 * the clause is TextRangeType::eRawClause.
2901 * However, this rules are odd since there can be two or more selected
2902 * clauses. Additionally, our old rules caused that IME developers/users
2903 * cannot specify composition string style as they want.
2905 * So, we shouldn't guess the meaning from its visual style.
2908 // If the range covers whole of composition string and the caret is at
2909 // the end of the composition string, the range is probably not converted.
2910 if (!utf8ClauseStart &&
2911 utf8ClauseEnd == static_cast<gint>(strlen(aUTF8CompositionString)) &&
2912 aTextRange.mEndOffset == aUTF16CaretOffset) {
2913 aTextRange.mRangeType = TextRangeType::eRawClause;
2915 // Typically, the caret is set at the start of the selected clause.
2916 // So, if the caret is in the clause, we can assume that the clause is
2917 // selected.
2918 else if (aTextRange.mStartOffset <= aUTF16CaretOffset &&
2919 aTextRange.mEndOffset > aUTF16CaretOffset) {
2920 aTextRange.mRangeType = TextRangeType::eSelectedClause;
2922 // Otherwise, we should assume that the clause is converted but not
2923 // selected.
2924 else {
2925 aTextRange.mRangeType = TextRangeType::eConvertedClause;
2928 MOZ_LOG(gIMELog, LogLevel::Debug,
2929 ("0x%p SetTextRange(), succeeded, aTextRange= { "
2930 "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }",
2931 this, aTextRange.mStartOffset, aTextRange.mEndOffset,
2932 ToChar(aTextRange.mRangeType),
2933 GetTextRangeStyleText(aTextRange.mRangeStyle).get()));
2935 return true;
2938 void IMContextWrapper::SetCursorPosition(GtkIMContext* aContext) {
2939 MOZ_LOG(
2940 gIMELog, LogLevel::Info,
2941 ("0x%p SetCursorPosition(aContext=0x%p), "
2942 "mCompositionTargetRange={ mOffset=%u, mLength=%u }, "
2943 "mContentSelection=%s",
2944 this, aContext, mCompositionTargetRange.mOffset,
2945 mCompositionTargetRange.mLength, ToString(mContentSelection).c_str()));
2947 bool useCaret = false;
2948 if (!mCompositionTargetRange.IsValid()) {
2949 if (mContentSelection.isNothing()) {
2950 MOZ_LOG(gIMELog, LogLevel::Error,
2951 ("0x%p SetCursorPosition(), FAILED, "
2952 "mCompositionTargetRange and mContentSelection are invalid",
2953 this));
2954 return;
2956 if (!mContentSelection->HasRange()) {
2957 MOZ_LOG(gIMELog, LogLevel::Warning,
2958 ("0x%p SetCursorPosition(), FAILED, "
2959 "mCompositionTargetRange is invalid and there is no selection",
2960 this));
2961 return;
2963 useCaret = true;
2966 if (!mLastFocusedWindow) {
2967 MOZ_LOG(gIMELog, LogLevel::Error,
2968 ("0x%p SetCursorPosition(), FAILED, due to no focused "
2969 "window",
2970 this));
2971 return;
2974 if (MOZ_UNLIKELY(!aContext)) {
2975 MOZ_LOG(gIMELog, LogLevel::Error,
2976 ("0x%p SetCursorPosition(), FAILED, due to no context", this));
2977 return;
2980 WidgetQueryContentEvent queryCaretOrTextRectEvent(
2981 true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow);
2982 if (useCaret) {
2983 queryCaretOrTextRectEvent.InitForQueryCaretRect(
2984 mContentSelection->OffsetAndDataRef().StartOffset());
2985 } else {
2986 if (mContentSelection->WritingModeRef().IsVertical()) {
2987 // For preventing the candidate window to overlap the target
2988 // clause, we should set fake (typically, very tall) caret rect.
2989 uint32_t length =
2990 mCompositionTargetRange.mLength ? mCompositionTargetRange.mLength : 1;
2991 queryCaretOrTextRectEvent.InitForQueryTextRect(
2992 mCompositionTargetRange.mOffset, length);
2993 } else {
2994 queryCaretOrTextRectEvent.InitForQueryTextRect(
2995 mCompositionTargetRange.mOffset, 1);
2998 nsEventStatus status;
2999 mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status);
3000 if (queryCaretOrTextRectEvent.Failed()) {
3001 MOZ_LOG(gIMELog, LogLevel::Error,
3002 ("0x%p SetCursorPosition(), FAILED, %s was failed", this,
3003 useCaret ? "eQueryCaretRect" : "eQueryTextRect"));
3004 return;
3007 nsWindow* rootWindow =
3008 static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());
3010 // Get the position of the rootWindow in screen.
3011 LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset();
3013 // Get the position of IM context owner window in screen.
3014 LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset();
3016 // Compute the caret position in the IM owner window.
3017 LayoutDeviceIntRect rect =
3018 queryCaretOrTextRectEvent.mReply->mRect + root - owner;
3019 rect.width = 0;
3020 GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect);
3022 gtk_im_context_set_cursor_location(aContext, &area);
3025 nsresult IMContextWrapper::GetCurrentParagraph(nsAString& aText,
3026 uint32_t& aCursorPos) {
3027 MOZ_LOG(gIMELog, LogLevel::Info,
3028 ("0x%p GetCurrentParagraph(), mCompositionState=%s", this,
3029 GetCompositionStateName()));
3031 if (!mLastFocusedWindow) {
3032 MOZ_LOG(gIMELog, LogLevel::Error,
3033 ("0x%p GetCurrentParagraph(), FAILED, there are no "
3034 "focused window in this module",
3035 this));
3036 return NS_ERROR_NULL_POINTER;
3039 nsEventStatus status;
3041 uint32_t selOffset = mCompositionStart;
3042 uint32_t selLength = mSelectedStringRemovedByComposition.Length();
3044 // If focused editor doesn't have composition string, we should use
3045 // current selection.
3046 if (!EditorHasCompositionString()) {
3047 // Query cursor position & selection
3048 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
3049 MOZ_LOG(gIMELog, LogLevel::Error,
3050 ("0x%p GetCurrentParagraph(), FAILED, due to no "
3051 "valid selection information",
3052 this));
3053 return NS_ERROR_FAILURE;
3056 if (mContentSelection.isSome() && mContentSelection->HasRange()) {
3057 selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
3058 selLength = mContentSelection->OffsetAndDataRef().Length();
3059 } else {
3060 // If there is no range, let's get all text instead...
3061 selOffset = 0u;
3062 selLength = INT32_MAX; // TODO: Change to UINT32_MAX, but see below
3066 MOZ_LOG(gIMELog, LogLevel::Debug,
3067 ("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u", this,
3068 selOffset, selLength));
3070 // XXX nsString::Find and nsString::RFind take int32_t for offset, so,
3071 // we cannot support this request when the current offset is larger
3072 // than INT32_MAX.
3073 if (selOffset > INT32_MAX || selLength > INT32_MAX ||
3074 selOffset + selLength > INT32_MAX) {
3075 MOZ_LOG(gIMELog, LogLevel::Error,
3076 ("0x%p GetCurrentParagraph(), FAILED, The selection is "
3077 "out of range",
3078 this));
3079 return NS_ERROR_FAILURE;
3082 // Get all text contents of the focused editor
3083 WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
3084 mLastFocusedWindow);
3085 queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
3086 mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
3087 if (NS_WARN_IF(queryTextContentEvent.Failed())) {
3088 return NS_ERROR_FAILURE;
3091 if (selOffset + selLength > queryTextContentEvent.mReply->DataLength()) {
3092 MOZ_LOG(gIMELog, LogLevel::Error,
3093 ("0x%p GetCurrentParagraph(), FAILED, The selection is "
3094 "invalid, queryTextContentEvent={ mReply=%s }",
3095 this, ToString(queryTextContentEvent.mReply).c_str()));
3096 return NS_ERROR_FAILURE;
3099 // Remove composing string and restore the selected string because
3100 // GtkEntry doesn't remove selected string until committing, however,
3101 // our editor does it. We should emulate the behavior for IME.
3102 nsAutoString textContent(queryTextContentEvent.mReply->DataRef());
3103 if (EditorHasCompositionString() &&
3104 mDispatchedCompositionString != mSelectedStringRemovedByComposition) {
3105 textContent.Replace(mCompositionStart,
3106 mDispatchedCompositionString.Length(),
3107 mSelectedStringRemovedByComposition);
3110 // Get only the focused paragraph, by looking for newlines
3111 int32_t parStart = 0;
3112 if (selOffset > 0) {
3113 parStart = Substring(textContent, 0, selOffset - 1).RFind(u"\n") + 1;
3115 int32_t parEnd = textContent.Find(u"\n", selOffset + selLength);
3116 if (parEnd < 0) {
3117 parEnd = textContent.Length();
3119 aText = nsDependentSubstring(textContent, parStart, parEnd - parStart);
3120 aCursorPos = selOffset - uint32_t(parStart);
3122 MOZ_LOG(
3123 gIMELog, LogLevel::Debug,
3124 ("0x%p GetCurrentParagraph(), succeeded, aText=%s, "
3125 "aText.Length()=%zu, aCursorPos=%u",
3126 this, NS_ConvertUTF16toUTF8(aText).get(), aText.Length(), aCursorPos));
3128 return NS_OK;
3131 nsresult IMContextWrapper::DeleteText(GtkIMContext* aContext, int32_t aOffset,
3132 uint32_t aNChars) {
3133 MOZ_LOG(gIMELog, LogLevel::Info,
3134 ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), "
3135 "mCompositionState=%s",
3136 this, aContext, aOffset, aNChars, GetCompositionStateName()));
3138 if (!mLastFocusedWindow) {
3139 MOZ_LOG(gIMELog, LogLevel::Error,
3140 ("0x%p DeleteText(), FAILED, there are no focused window "
3141 "in this module",
3142 this));
3143 return NS_ERROR_NULL_POINTER;
3146 if (!aNChars) {
3147 MOZ_LOG(gIMELog, LogLevel::Error,
3148 ("0x%p DeleteText(), FAILED, aNChars must not be zero", this));
3149 return NS_ERROR_INVALID_ARG;
3152 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
3153 nsEventStatus status;
3155 // First, we should cancel current composition because editor cannot
3156 // handle changing selection and deleting text.
3157 uint32_t selOffset;
3158 bool wasComposing = IsComposing();
3159 bool editorHadCompositionString = EditorHasCompositionString();
3160 if (wasComposing) {
3161 selOffset = mCompositionStart;
3162 if (!DispatchCompositionCommitEvent(aContext,
3163 &mSelectedStringRemovedByComposition)) {
3164 MOZ_LOG(gIMELog, LogLevel::Error,
3165 ("0x%p DeleteText(), FAILED, quitting from DeletText", this));
3166 return NS_ERROR_FAILURE;
3168 } else {
3169 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
3170 MOZ_LOG(gIMELog, LogLevel::Error,
3171 ("0x%p DeleteText(), FAILED, due to no valid selection "
3172 "information",
3173 this));
3174 return NS_ERROR_FAILURE;
3176 if (!mContentSelection->HasRange()) {
3177 MOZ_LOG(gIMELog, LogLevel::Debug,
3178 ("0x%p DeleteText(), does nothing, due to no selection range",
3179 this));
3180 return NS_OK;
3182 selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
3185 // Get all text contents of the focused editor
3186 WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
3187 mLastFocusedWindow);
3188 queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
3189 mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
3190 if (NS_WARN_IF(queryTextContentEvent.Failed())) {
3191 return NS_ERROR_FAILURE;
3193 if (queryTextContentEvent.mReply->IsDataEmpty()) {
3194 MOZ_LOG(gIMELog, LogLevel::Error,
3195 ("0x%p DeleteText(), FAILED, there is no contents", this));
3196 return NS_ERROR_FAILURE;
3199 NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(
3200 queryTextContentEvent.mReply->DataRef(), 0, selOffset));
3201 glong offsetInUTF8Characters =
3202 g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset;
3203 if (offsetInUTF8Characters < 0) {
3204 MOZ_LOG(gIMELog, LogLevel::Error,
3205 ("0x%p DeleteText(), FAILED, aOffset is too small for "
3206 "current cursor pos (computed offset: %ld)",
3207 this, offsetInUTF8Characters));
3208 return NS_ERROR_FAILURE;
3211 AppendUTF16toUTF8(
3212 nsDependentSubstring(queryTextContentEvent.mReply->DataRef(), selOffset),
3213 utf8Str);
3214 glong countOfCharactersInUTF8 =
3215 g_utf8_strlen(utf8Str.get(), utf8Str.Length());
3216 glong endInUTF8Characters = offsetInUTF8Characters + aNChars;
3217 if (countOfCharactersInUTF8 < endInUTF8Characters) {
3218 MOZ_LOG(gIMELog, LogLevel::Error,
3219 ("0x%p DeleteText(), FAILED, aNChars is too large for "
3220 "current contents (content length: %ld, computed end offset: %ld)",
3221 this, countOfCharactersInUTF8, endInUTF8Characters));
3222 return NS_ERROR_FAILURE;
3225 gchar* charAtOffset =
3226 g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters);
3227 gchar* charAtEnd =
3228 g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters);
3230 // Set selection to delete
3231 WidgetSelectionEvent selectionEvent(true, eSetSelection, mLastFocusedWindow);
3233 nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0,
3234 charAtOffset - utf8Str.get());
3235 selectionEvent.mOffset = NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length();
3237 nsDependentCSubstring utf8DeletingStr(utf8Str, utf8StrBeforeOffset.Length(),
3238 charAtEnd - charAtOffset);
3239 selectionEvent.mLength = NS_ConvertUTF8toUTF16(utf8DeletingStr).Length();
3241 selectionEvent.mReversed = false;
3242 selectionEvent.mExpandToClusterBoundary = false;
3243 lastFocusedWindow->DispatchEvent(&selectionEvent, status);
3245 if (!selectionEvent.mSucceeded || lastFocusedWindow != mLastFocusedWindow ||
3246 lastFocusedWindow->Destroyed()) {
3247 MOZ_LOG(gIMELog, LogLevel::Error,
3248 ("0x%p DeleteText(), FAILED, setting selection caused "
3249 "focus change or window destroyed",
3250 this));
3251 return NS_ERROR_FAILURE;
3254 // If this deleting text caused by a key press, we need to dispatch
3255 // eKeyDown or eKeyUp before dispatching eContentCommandDelete event.
3256 if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandDelete)) {
3257 MOZ_LOG(gIMELog, LogLevel::Warning,
3258 ("0x%p DeleteText(), Warning, "
3259 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
3260 this));
3261 return NS_ERROR_FAILURE;
3264 // Delete the selection
3265 WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete,
3266 mLastFocusedWindow);
3267 mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
3269 if (!contentCommandEvent.mSucceeded ||
3270 lastFocusedWindow != mLastFocusedWindow ||
3271 lastFocusedWindow->Destroyed()) {
3272 MOZ_LOG(gIMELog, LogLevel::Error,
3273 ("0x%p DeleteText(), FAILED, deleting the selection caused "
3274 "focus change or window destroyed",
3275 this));
3276 return NS_ERROR_FAILURE;
3279 if (!wasComposing) {
3280 return NS_OK;
3283 // Restore the composition at new caret position.
3284 if (!DispatchCompositionStart(aContext)) {
3285 MOZ_LOG(
3286 gIMELog, LogLevel::Error,
3287 ("0x%p DeleteText(), FAILED, resterting composition start", this));
3288 return NS_ERROR_FAILURE;
3291 if (!editorHadCompositionString) {
3292 return NS_OK;
3295 nsAutoString compositionString;
3296 GetCompositionString(aContext, compositionString);
3297 if (!DispatchCompositionChangeEvent(aContext, compositionString)) {
3298 MOZ_LOG(
3299 gIMELog, LogLevel::Error,
3300 ("0x%p DeleteText(), FAILED, restoring composition string", this));
3301 return NS_ERROR_FAILURE;
3304 return NS_OK;
3307 bool IMContextWrapper::EnsureToCacheContentSelection(
3308 nsAString* aSelectedString) {
3309 if (aSelectedString) {
3310 aSelectedString->Truncate();
3313 if (mContentSelection.isSome()) {
3314 if (mContentSelection->HasRange() && aSelectedString) {
3315 aSelectedString->Assign(mContentSelection->OffsetAndDataRef().DataRef());
3317 return true;
3320 RefPtr<nsWindow> dispatcherWindow =
3321 mLastFocusedWindow ? mLastFocusedWindow : mOwnerWindow;
3322 if (NS_WARN_IF(!dispatcherWindow)) {
3323 MOZ_LOG(gIMELog, LogLevel::Error,
3324 ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
3325 "no focused window",
3326 this));
3327 return false;
3330 nsEventStatus status;
3331 WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
3332 dispatcherWindow);
3333 dispatcherWindow->DispatchEvent(&querySelectedTextEvent, status);
3334 if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
3335 MOZ_LOG(gIMELog, LogLevel::Error,
3336 ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
3337 "failure of query selection event",
3338 this));
3339 return false;
3342 mContentSelection = Some(ContentSelection(querySelectedTextEvent));
3343 if (mContentSelection->HasRange()) {
3344 if (!mContentSelection->OffsetAndDataRef().IsDataEmpty() &&
3345 aSelectedString) {
3346 aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef());
3350 MOZ_LOG(
3351 gIMELog, LogLevel::Debug,
3352 ("0x%p EnsureToCacheContentSelection(), Succeeded, mContentSelection=%s",
3353 this, ToString(mContentSelection).c_str()));
3354 return true;
3357 } // namespace widget
3358 } // namespace mozilla