Bug 1822331 [wpt PR 38990] - [@scope] Enable implicit :scope and relative selectors...
[gecko.git] / widget / gtk / IMContextWrapper.cpp
blob4950a89a0cfd43f2b578ec94c03b68e624fddc28
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"
13 #include "nsGtkKeyUtils.h"
14 #include "nsWindow.h"
15 #include "mozilla/AutoRestore.h"
16 #include "mozilla/Likely.h"
17 #include "mozilla/LookAndFeel.h"
18 #include "mozilla/MiscEvents.h"
19 #include "mozilla/Preferences.h"
20 #include "mozilla/StaticPrefs_intl.h"
21 #include "mozilla/Telemetry.h"
22 #include "mozilla/TextEventDispatcher.h"
23 #include "mozilla/TextEvents.h"
24 #include "mozilla/ToString.h"
25 #include "mozilla/WritingModes.h"
27 // For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync`
28 // rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
29 // big file.
30 // Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
31 mozilla::LazyLogModule gIMELog("IMEHandler");
33 namespace mozilla {
34 namespace widget {
36 static inline const char* ToChar(bool aBool) {
37 return aBool ? "true" : "false";
40 static const char* GetEventType(GdkEventKey* aKeyEvent) {
41 switch (aKeyEvent->type) {
42 case GDK_KEY_PRESS:
43 return "GDK_KEY_PRESS";
44 case GDK_KEY_RELEASE:
45 return "GDK_KEY_RELEASE";
46 default:
47 return "Unknown";
51 class GetEventStateName : public nsAutoCString {
52 public:
53 explicit GetEventStateName(guint aState,
54 IMContextWrapper::IMContextID aIMContextID =
55 IMContextWrapper::IMContextID::Unknown) {
56 if (aState & GDK_SHIFT_MASK) {
57 AppendModifier("shift");
59 if (aState & GDK_CONTROL_MASK) {
60 AppendModifier("control");
62 if (aState & GDK_MOD1_MASK) {
63 AppendModifier("mod1");
65 if (aState & GDK_MOD2_MASK) {
66 AppendModifier("mod2");
68 if (aState & GDK_MOD3_MASK) {
69 AppendModifier("mod3");
71 if (aState & GDK_MOD4_MASK) {
72 AppendModifier("mod4");
74 if (aState & GDK_MOD4_MASK) {
75 AppendModifier("mod5");
77 if (aState & GDK_MOD4_MASK) {
78 AppendModifier("mod5");
80 switch (aIMContextID) {
81 case IMContextWrapper::IMContextID::IBus:
82 static const guint IBUS_HANDLED_MASK = 1 << 24;
83 static const guint IBUS_IGNORED_MASK = 1 << 25;
84 if (aState & IBUS_HANDLED_MASK) {
85 AppendModifier("IBUS_HANDLED_MASK");
87 if (aState & IBUS_IGNORED_MASK) {
88 AppendModifier("IBUS_IGNORED_MASK");
90 break;
91 case IMContextWrapper::IMContextID::Fcitx:
92 case IMContextWrapper::IMContextID::Fcitx5:
93 static const guint FcitxKeyState_HandledMask = 1 << 24;
94 static const guint FcitxKeyState_IgnoredMask = 1 << 25;
95 if (aState & FcitxKeyState_HandledMask) {
96 AppendModifier("FcitxKeyState_HandledMask");
98 if (aState & FcitxKeyState_IgnoredMask) {
99 AppendModifier("FcitxKeyState_IgnoredMask");
101 break;
102 default:
103 break;
107 private:
108 void AppendModifier(const char* aModifierName) {
109 if (!IsEmpty()) {
110 AppendLiteral(" + ");
112 Append(aModifierName);
116 class GetTextRangeStyleText final : public nsAutoCString {
117 public:
118 explicit GetTextRangeStyleText(const TextRangeStyle& aStyle) {
119 if (!aStyle.IsDefined()) {
120 AssignLiteral("{ IsDefined()=false }");
121 return;
124 if (aStyle.IsLineStyleDefined()) {
125 AppendLiteral("{ mLineStyle=");
126 AppendLineStyle(aStyle.mLineStyle);
127 if (aStyle.IsUnderlineColorDefined()) {
128 AppendLiteral(", mUnderlineColor=");
129 AppendColor(aStyle.mUnderlineColor);
130 } else {
131 AppendLiteral(", IsUnderlineColorDefined=false");
133 } else {
134 AppendLiteral("{ IsLineStyleDefined()=false");
137 if (aStyle.IsForegroundColorDefined()) {
138 AppendLiteral(", mForegroundColor=");
139 AppendColor(aStyle.mForegroundColor);
140 } else {
141 AppendLiteral(", IsForegroundColorDefined()=false");
144 if (aStyle.IsBackgroundColorDefined()) {
145 AppendLiteral(", mBackgroundColor=");
146 AppendColor(aStyle.mBackgroundColor);
147 } else {
148 AppendLiteral(", IsBackgroundColorDefined()=false");
151 AppendLiteral(" }");
153 void AppendLineStyle(TextRangeStyle::LineStyle aLineStyle) {
154 switch (aLineStyle) {
155 case TextRangeStyle::LineStyle::None:
156 AppendLiteral("LineStyle::None");
157 break;
158 case TextRangeStyle::LineStyle::Solid:
159 AppendLiteral("LineStyle::Solid");
160 break;
161 case TextRangeStyle::LineStyle::Dotted:
162 AppendLiteral("LineStyle::Dotted");
163 break;
164 case TextRangeStyle::LineStyle::Dashed:
165 AppendLiteral("LineStyle::Dashed");
166 break;
167 case TextRangeStyle::LineStyle::Double:
168 AppendLiteral("LineStyle::Double");
169 break;
170 case TextRangeStyle::LineStyle::Wavy:
171 AppendLiteral("LineStyle::Wavy");
172 break;
173 default:
174 AppendPrintf("Invalid(0x%02X)",
175 static_cast<TextRangeStyle::LineStyleType>(aLineStyle));
176 break;
179 void AppendColor(nscolor aColor) {
180 AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }", NS_GET_R(aColor),
181 NS_GET_G(aColor), NS_GET_B(aColor), NS_GET_A(aColor));
183 virtual ~GetTextRangeStyleText() = default;
186 const static bool kUseSimpleContextDefault = false;
188 /******************************************************************************
189 * SelectionStyleProvider
191 * IME (e.g., fcitx, ~4.2.8.3) may look up selection colors of widget, which
192 * is related to the window associated with the IM context, to support any
193 * colored widgets. Our editor (like <input type="text">) is rendered as
194 * native GtkTextView as far as possible by default and if editor color is
195 * changed by web apps, nsTextFrame may swap background color of foreground
196 * color of composition string for making composition string is always
197 * visually distinct in normal text.
199 * So, we would like IME to set style of composition string to good colors
200 * in GtkTextView. Therefore, this class overwrites selection colors of
201 * our widget with selection colors of GtkTextView so that it's possible IME
202 * to refer selection colors of GtkTextView via our widget.
203 ******************************************************************************/
205 static Maybe<nscolor> GetSystemColor(LookAndFeel::ColorID aId) {
206 return LookAndFeel::GetColor(aId, LookAndFeel::ColorScheme::Light,
207 LookAndFeel::UseStandins::No);
210 class SelectionStyleProvider final {
211 public:
212 static SelectionStyleProvider* GetExistingInstance() { return sInstance; }
214 static SelectionStyleProvider* GetInstance() {
215 if (sHasShutDown) {
216 return nullptr;
218 if (!sInstance) {
219 sInstance = new SelectionStyleProvider();
221 return sInstance;
224 static void Shutdown() {
225 if (sInstance) {
226 g_object_unref(sInstance->mProvider);
228 delete sInstance;
229 sInstance = nullptr;
230 sHasShutDown = true;
233 // aGDKWindow is a GTK window which will be associated with an IM context.
234 void AttachTo(GdkWindow* aGDKWindow) {
235 GtkWidget* widget = nullptr;
236 // gdk_window_get_user_data() typically returns pointer to widget that
237 // window belongs to. If it's widget, fcitx retrieves selection colors
238 // of them. So, we need to overwrite its style.
239 gdk_window_get_user_data(aGDKWindow, (gpointer*)&widget);
240 if (GTK_IS_WIDGET(widget)) {
241 gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
242 GTK_STYLE_PROVIDER(mProvider),
243 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
247 void OnThemeChanged() {
248 // fcitx refers GtkStyle::text[GTK_STATE_SELECTED] and
249 // GtkStyle::bg[GTK_STATE_SELECTED] (although pair of text and *base*
250 // or *fg* and bg is correct). gtk_style_update_from_context() will
251 // set these colors using the widget's GtkStyleContext and so the
252 // colors can be controlled by a ":selected" CSS rule.
253 nsAutoCString style(":selected{");
254 // FYI: LookAndFeel always returns selection colors of GtkTextView.
255 if (auto selectionForegroundColor =
256 GetSystemColor(LookAndFeel::ColorID::Highlight)) {
257 double alpha =
258 static_cast<double>(NS_GET_A(*selectionForegroundColor)) / 0xFF;
259 style.AppendPrintf("color:rgba(%u,%u,%u,",
260 NS_GET_R(*selectionForegroundColor),
261 NS_GET_G(*selectionForegroundColor),
262 NS_GET_B(*selectionForegroundColor));
263 // We can't use AppendPrintf here, because it does locale-specific
264 // formatting of floating-point values.
265 style.AppendFloat(alpha);
266 style.AppendPrintf(");");
268 if (auto selectionBackgroundColor =
269 GetSystemColor(LookAndFeel::ColorID::Highlighttext)) {
270 double alpha =
271 static_cast<double>(NS_GET_A(*selectionBackgroundColor)) / 0xFF;
272 style.AppendPrintf("background-color:rgba(%u,%u,%u,",
273 NS_GET_R(*selectionBackgroundColor),
274 NS_GET_G(*selectionBackgroundColor),
275 NS_GET_B(*selectionBackgroundColor));
276 style.AppendFloat(alpha);
277 style.AppendPrintf(");");
279 style.AppendLiteral("}");
280 gtk_css_provider_load_from_data(mProvider, style.get(), -1, nullptr);
283 private:
284 static SelectionStyleProvider* sInstance;
285 static bool sHasShutDown;
286 GtkCssProvider* const mProvider;
288 SelectionStyleProvider() : mProvider(gtk_css_provider_new()) {
289 OnThemeChanged();
293 SelectionStyleProvider* SelectionStyleProvider::sInstance = nullptr;
294 bool SelectionStyleProvider::sHasShutDown = false;
296 /******************************************************************************
297 * IMContextWrapper
298 ******************************************************************************/
300 IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr;
301 guint16 IMContextWrapper::sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
302 bool IMContextWrapper::sUseSimpleContext;
304 NS_IMPL_ISUPPORTS(IMContextWrapper, TextEventDispatcherListener,
305 nsISupportsWeakReference)
307 IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow)
308 : mOwnerWindow(aOwnerWindow),
309 mLastFocusedWindow(nullptr),
310 mContext(nullptr),
311 mSimpleContext(nullptr),
312 mDummyContext(nullptr),
313 mComposingContext(nullptr),
314 mCompositionStart(UINT32_MAX),
315 mProcessingKeyEvent(nullptr),
316 mCompositionState(eCompositionState_NotComposing),
317 mIMContextID(IMContextID::Unknown),
318 mFallbackToKeyEvent(false),
319 mKeyboardEventWasDispatched(false),
320 mKeyboardEventWasConsumed(false),
321 mIsDeletingSurrounding(false),
322 mLayoutChanged(false),
323 mSetCursorPositionOnKeyEvent(true),
324 mPendingResettingIMContext(false),
325 mRetrieveSurroundingSignalReceived(false),
326 mMaybeInDeadKeySequence(false),
327 mIsIMInAsyncKeyHandlingMode(false),
328 mSetInputPurposeAndInputHints(false) {
329 static bool sFirstInstance = true;
330 if (sFirstInstance) {
331 sFirstInstance = false;
332 sUseSimpleContext =
333 Preferences::GetBool("intl.ime.use_simple_context_on_password_field",
334 kUseSimpleContextDefault);
336 Init();
339 static bool IsIBusInSyncMode() {
340 // See ibus_im_context_class_init() in client/gtk2/ibusimcontext.c
341 // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L610
342 const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE");
344 // See _get_boolean_env() in client/gtk2/ibusimcontext.c
345 // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L520-L537
346 if (!env) {
347 return false;
349 nsDependentCString envStr(env);
350 if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
351 envStr.EqualsLiteral("false") || envStr.EqualsLiteral("False") ||
352 envStr.EqualsLiteral("FALSE")) {
353 return false;
355 return true;
358 static bool GetFcitxBoolEnv(const char* aEnv) {
359 // See fcitx_utils_get_boolean_env in src/lib/fcitx-utils/utils.c
360 // https://github.com/fcitx/fcitx/blob/0c87840dc7d9460c2cb5feaeefec299d0d3d62ec/src/lib/fcitx-utils/utils.c#L721-L736
361 const char* env = PR_GetEnv(aEnv);
362 if (!env) {
363 return false;
365 nsDependentCString envStr(env);
366 if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
367 envStr.EqualsLiteral("false")) {
368 return false;
370 return true;
373 static bool IsFcitxInSyncMode() {
374 // See fcitx_im_context_class_init() in src/frontend/gtk2/fcitximcontext.c
375 // https://github.com/fcitx/fcitx/blob/78b98d9230dc9630e99d52e3172bdf440ffd08c4/src/frontend/gtk2/fcitximcontext.c#L395-L398
376 return GetFcitxBoolEnv("IBUS_ENABLE_SYNC_MODE") ||
377 GetFcitxBoolEnv("FCITX_ENABLE_SYNC_MODE");
380 nsDependentCSubstring IMContextWrapper::GetIMName() const {
381 const char* contextIDChar =
382 gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext));
383 if (!contextIDChar) {
384 return nsDependentCSubstring();
387 nsDependentCSubstring im(contextIDChar, strlen(contextIDChar));
389 // If the context is XIM, actual engine must be specified with
390 // |XMODIFIERS=@im=foo|.
391 const char* xmodifiersChar = PR_GetEnv("XMODIFIERS");
392 if (!xmodifiersChar || !im.EqualsLiteral("xim")) {
393 return im;
396 nsDependentCString xmodifiers(xmodifiersChar);
397 int32_t atIMValueStart = xmodifiers.Find("@im=") + 4;
398 if (atIMValueStart < 4 ||
399 xmodifiers.Length() <= static_cast<size_t>(atIMValueStart)) {
400 return im;
403 int32_t atIMValueEnd = xmodifiers.Find("@", atIMValueStart);
404 if (atIMValueEnd > atIMValueStart) {
405 return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
406 atIMValueEnd - atIMValueStart);
409 if (atIMValueEnd == kNotFound) {
410 return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
411 strlen(xmodifiersChar) - atIMValueStart);
414 return im;
417 void IMContextWrapper::Init() {
418 MozContainer* container = mOwnerWindow->GetMozContainer();
419 MOZ_ASSERT(container, "container is null");
420 GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container));
422 // Overwrite selection colors of the window before associating the window
423 // with IM context since IME may look up selection colors via IM context
424 // to support any colored widgets.
425 SelectionStyleProvider::GetInstance()->AttachTo(gdkWindow);
427 // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
428 // So, we don't need to check the result.
430 // Normal context.
431 mContext = gtk_im_multicontext_new();
432 gtk_im_context_set_client_window(mContext, gdkWindow);
433 g_signal_connect(mContext, "preedit_changed",
434 G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback),
435 this);
436 g_signal_connect(mContext, "retrieve_surrounding",
437 G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback),
438 this);
439 g_signal_connect(mContext, "delete_surrounding",
440 G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback),
441 this);
442 g_signal_connect(mContext, "commit",
443 G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback),
444 this);
445 g_signal_connect(mContext, "preedit_start",
446 G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
447 this);
448 g_signal_connect(mContext, "preedit_end",
449 G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
450 this);
451 nsDependentCSubstring im = GetIMName();
452 if (im.EqualsLiteral("ibus")) {
453 mIMContextID = IMContextID::IBus;
454 mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode();
455 // Although ibus has key snooper mode, it's forcibly disabled on Firefox
456 // in default settings by its whitelist since we always send key events
457 // to IME before handling shortcut keys. The whitelist can be
458 // customized with env, IBUS_NO_SNOOPER_APPS, but we don't need to
459 // support such rare cases for reducing maintenance cost.
460 mIsKeySnooped = false;
461 } else if (im.EqualsLiteral("fcitx")) {
462 mIMContextID = IMContextID::Fcitx;
463 mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode();
464 // Although Fcitx has key snooper mode similar to ibus, it's also
465 // disabled on Firefox in default settings by its whitelist. The
466 // whitelist can be customized with env, IBUS_NO_SNOOPER_APPS or
467 // FCITX_NO_SNOOPER_APPS, but we don't need to support such rare cases
468 // for reducing maintenance cost.
469 mIsKeySnooped = false;
470 } else if (im.EqualsLiteral("fcitx5")) {
471 mIMContextID = IMContextID::Fcitx5;
472 mIsIMInAsyncKeyHandlingMode = true; // does not have sync mode.
473 mIsKeySnooped = false; // never use key snooper.
474 } else if (im.EqualsLiteral("uim")) {
475 mIMContextID = IMContextID::Uim;
476 mIsIMInAsyncKeyHandlingMode = false;
477 // We cannot know if uim uses key snooper since it's build option of
478 // uim. Therefore, we need to retrieve the consideration from the
479 // pref for making users and distributions allowed to choose their
480 // preferred value.
481 mIsKeySnooped =
482 Preferences::GetBool("intl.ime.hack.uim.using_key_snooper", true);
483 } else if (im.EqualsLiteral("scim")) {
484 mIMContextID = IMContextID::Scim;
485 mIsIMInAsyncKeyHandlingMode = false;
486 mIsKeySnooped = false;
487 } else if (im.EqualsLiteral("iiim")) {
488 mIMContextID = IMContextID::IIIMF;
489 mIsIMInAsyncKeyHandlingMode = false;
490 mIsKeySnooped = false;
491 } else if (im.EqualsLiteral("wayland")) {
492 mIMContextID = IMContextID::Wayland;
493 mIsIMInAsyncKeyHandlingMode = false;
494 mIsKeySnooped = true;
495 } else {
496 mIMContextID = IMContextID::Unknown;
497 mIsIMInAsyncKeyHandlingMode = false;
498 mIsKeySnooped = false;
501 // Simple context
502 if (sUseSimpleContext) {
503 mSimpleContext = gtk_im_context_simple_new();
504 gtk_im_context_set_client_window(mSimpleContext, gdkWindow);
505 g_signal_connect(mSimpleContext, "preedit_changed",
506 G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback),
507 this);
508 g_signal_connect(
509 mSimpleContext, "retrieve_surrounding",
510 G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback), this);
511 g_signal_connect(mSimpleContext, "delete_surrounding",
512 G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback),
513 this);
514 g_signal_connect(mSimpleContext, "commit",
515 G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback),
516 this);
517 g_signal_connect(mSimpleContext, "preedit_start",
518 G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
519 this);
520 g_signal_connect(mSimpleContext, "preedit_end",
521 G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
522 this);
525 // Dummy context
526 mDummyContext = gtk_im_multicontext_new();
527 gtk_im_context_set_client_window(mDummyContext, gdkWindow);
529 MOZ_LOG(gIMELog, LogLevel::Info,
530 ("0x%p Init(), mOwnerWindow=%p, mContext=%p (im=\"%s\"), "
531 "mIsIMInAsyncKeyHandlingMode=%s, mIsKeySnooped=%s, "
532 "mSimpleContext=%p, mDummyContext=%p, "
533 "gtk_im_multicontext_get_context_id()=\"%s\", "
534 "PR_GetEnv(\"XMODIFIERS\")=\"%s\"",
535 this, mOwnerWindow, mContext, nsAutoCString(im).get(),
536 ToChar(mIsIMInAsyncKeyHandlingMode), ToChar(mIsKeySnooped),
537 mSimpleContext, mDummyContext,
538 gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)),
539 PR_GetEnv("XMODIFIERS")));
542 /* static */
543 void IMContextWrapper::Shutdown() { SelectionStyleProvider::Shutdown(); }
545 IMContextWrapper::~IMContextWrapper() {
546 if (this == sLastFocusedContext) {
547 sLastFocusedContext = nullptr;
549 MOZ_LOG(gIMELog, LogLevel::Info, ("0x%p ~IMContextWrapper()", this));
552 NS_IMETHODIMP
553 IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
554 const IMENotification& aNotification) {
555 switch (aNotification.mMessage) {
556 case REQUEST_TO_COMMIT_COMPOSITION:
557 case REQUEST_TO_CANCEL_COMPOSITION: {
558 nsWindow* window =
559 static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
560 return IsComposing() ? EndIMEComposition(window) : NS_OK;
562 case NOTIFY_IME_OF_FOCUS:
563 OnFocusChangeInGecko(true);
564 return NS_OK;
565 case NOTIFY_IME_OF_BLUR:
566 OnFocusChangeInGecko(false);
567 return NS_OK;
568 case NOTIFY_IME_OF_POSITION_CHANGE:
569 OnLayoutChange();
570 return NS_OK;
571 case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
572 OnUpdateComposition();
573 return NS_OK;
574 case NOTIFY_IME_OF_SELECTION_CHANGE: {
575 nsWindow* window =
576 static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
577 OnSelectionChange(window, aNotification);
578 return NS_OK;
580 default:
581 return NS_ERROR_NOT_IMPLEMENTED;
585 NS_IMETHODIMP_(void)
586 IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
587 // XXX When input transaction is being stolen by add-on, what should we do?
590 NS_IMETHODIMP_(void)
591 IMContextWrapper::WillDispatchKeyboardEvent(
592 TextEventDispatcher* aTextEventDispatcher,
593 WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
594 void* aData) {
595 KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent,
596 static_cast<GdkEventKey*>(aData));
599 TextEventDispatcher* IMContextWrapper::GetTextEventDispatcher() {
600 if (NS_WARN_IF(!mLastFocusedWindow)) {
601 return nullptr;
603 TextEventDispatcher* dispatcher =
604 mLastFocusedWindow->GetTextEventDispatcher();
605 // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr.
606 MOZ_RELEASE_ASSERT(dispatcher);
607 return dispatcher;
610 NS_IMETHODIMP_(IMENotificationRequests)
611 IMContextWrapper::GetIMENotificationRequests() {
612 IMENotificationRequests::Notifications notifications =
613 IMENotificationRequests::NOTIFY_NOTHING;
614 // If it's not enabled, we don't need position change notification.
615 if (IsEnabled()) {
616 notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE;
618 return IMENotificationRequests(notifications);
621 void IMContextWrapper::OnDestroyWindow(nsWindow* aWindow) {
622 MOZ_LOG(
623 gIMELog, LogLevel::Info,
624 ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
625 "mOwnerWindow=0x%p, mLastFocusedModule=0x%p",
626 this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext));
628 MOZ_ASSERT(aWindow, "aWindow must not be null");
630 if (mLastFocusedWindow == aWindow) {
631 if (IsComposing()) {
632 EndIMEComposition(aWindow);
634 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
635 mLastFocusedWindow = nullptr;
638 if (mOwnerWindow != aWindow) {
639 return;
642 if (sLastFocusedContext == this) {
643 sLastFocusedContext = nullptr;
647 * NOTE:
648 * The given window is the owner of this, so, we must release the
649 * contexts now. But that might be referred from other nsWindows
650 * (they are children of this. But we don't know why there are the
651 * cases). So, we need to clear the pointers that refers to contexts
652 * and this if the other referrers are still alive. See bug 349727.
654 if (mContext) {
655 PrepareToDestroyContext(mContext);
656 gtk_im_context_set_client_window(mContext, nullptr);
657 g_object_unref(mContext);
658 mContext = nullptr;
661 if (mSimpleContext) {
662 gtk_im_context_set_client_window(mSimpleContext, nullptr);
663 g_object_unref(mSimpleContext);
664 mSimpleContext = nullptr;
667 if (mDummyContext) {
668 // mContext and mDummyContext have the same slaveType and signal_data
669 // so no need for another workaround_gtk_im_display_closed.
670 gtk_im_context_set_client_window(mDummyContext, nullptr);
671 g_object_unref(mDummyContext);
672 mDummyContext = nullptr;
675 if (NS_WARN_IF(mComposingContext)) {
676 g_object_unref(mComposingContext);
677 mComposingContext = nullptr;
680 mOwnerWindow = nullptr;
681 mLastFocusedWindow = nullptr;
682 mInputContext.mIMEState.mEnabled = IMEEnabled::Disabled;
683 mPostingKeyEvents.Clear();
685 MOZ_LOG(gIMELog, LogLevel::Debug,
686 ("0x%p OnDestroyWindow(), succeeded, Completely destroyed", this));
689 void IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext) {
690 if (mIMContextID == IMContextID::IIIMF) {
691 // IIIM module registers handlers for the "closed" signal on the
692 // display, but the signal handler is not disconnected when the module
693 // is unloaded. To prevent the module from being unloaded, use static
694 // variable to hold reference of slave context class declared by IIIM.
695 // Note that this does not grab any instance, it grabs the "class".
696 static gpointer sGtkIIIMContextClass = nullptr;
697 if (!sGtkIIIMContextClass) {
698 // We retrieved slave context class with g_type_name() and actual
699 // slave context instance when our widget was GTK2. That must be
700 // _GtkIMContext::priv::slave in GTK3. However, _GtkIMContext::priv
701 // is an opacity struct named _GtkIMMulticontextPrivate, i.e., it's
702 // not exposed by GTK3. Therefore, we cannot access the instance
703 // safely. So, we need to retrieve the slave context class with
704 // g_type_from_name("GtkIMContextIIIM") directly (anyway, we needed
705 // to compare the class name with "GtkIMContextIIIM").
706 GType IIMContextType = g_type_from_name("GtkIMContextIIIM");
707 if (IIMContextType) {
708 sGtkIIIMContextClass = g_type_class_ref(IIMContextType);
709 MOZ_LOG(gIMELog, LogLevel::Info,
710 ("0x%p PrepareToDestroyContext(), added to reference to "
711 "GtkIMContextIIIM class to prevent it from being unloaded",
712 this));
713 } else {
714 MOZ_LOG(gIMELog, LogLevel::Error,
715 ("0x%p PrepareToDestroyContext(), FAILED to prevent the "
716 "IIIM module from being uploaded",
717 this));
723 void IMContextWrapper::OnFocusWindow(nsWindow* aWindow) {
724 if (MOZ_UNLIKELY(IsDestroyed())) {
725 return;
728 MOZ_LOG(gIMELog, LogLevel::Info,
729 ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this,
730 aWindow, mLastFocusedWindow));
731 mLastFocusedWindow = aWindow;
734 void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) {
735 if (MOZ_UNLIKELY(IsDestroyed())) {
736 return;
739 MOZ_LOG(
740 gIMELog, LogLevel::Info,
741 ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
742 "mIMEFocusState=%s",
743 this, aWindow, mLastFocusedWindow, ToString(mIMEFocusState).c_str()));
745 if (mLastFocusedWindow != aWindow) {
746 return;
749 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
752 KeyHandlingState IMContextWrapper::OnKeyEvent(
753 nsWindow* aCaller, GdkEventKey* aEvent,
754 bool aKeyboardEventWasDispatched /* = false */) {
755 MOZ_ASSERT(aEvent, "aEvent must be non-null");
757 if (!mInputContext.mIMEState.IsEditable() || MOZ_UNLIKELY(IsDestroyed())) {
758 return KeyHandlingState::eNotHandled;
761 MOZ_LOG(gIMELog, LogLevel::Info, (">>>>>>>>>>>>>>>>"));
762 MOZ_LOG(
763 gIMELog, LogLevel::Info,
764 ("0x%p OnKeyEvent(aCaller=0x%p, "
765 "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X, state=%s, "
766 "time=%u, hardware_keycode=%u, group=%u }, "
767 "aKeyboardEventWasDispatched=%s)",
768 this, aCaller, aEvent, GetEventType(aEvent),
769 gdk_keyval_name(aEvent->keyval), gdk_keyval_to_unicode(aEvent->keyval),
770 GetEventStateName(aEvent->state, mIMContextID).get(), aEvent->time,
771 aEvent->hardware_keycode, aEvent->group,
772 ToChar(aKeyboardEventWasDispatched)));
773 MOZ_LOG(
774 gIMELog, LogLevel::Info,
775 ("0x%p OnKeyEvent(), mMaybeInDeadKeySequence=%s, "
776 "mCompositionState=%s, current context=%p, active context=%p, "
777 "mIMContextID=%s, mIsIMInAsyncKeyHandlingMode=%s",
778 this, ToChar(mMaybeInDeadKeySequence), GetCompositionStateName(),
779 GetCurrentContext(), GetActiveContext(), ToString(mIMContextID).c_str(),
780 ToChar(mIsIMInAsyncKeyHandlingMode)));
782 if (aCaller != mLastFocusedWindow) {
783 MOZ_LOG(gIMELog, LogLevel::Error,
784 ("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
785 "window, mLastFocusedWindow=0x%p",
786 this, mLastFocusedWindow));
787 return KeyHandlingState::eNotHandled;
790 // Even if old IM context has composition, key event should be sent to
791 // current context since the user expects so.
792 GtkIMContext* currentContext = GetCurrentContext();
793 if (MOZ_UNLIKELY(!currentContext)) {
794 MOZ_LOG(gIMELog, LogLevel::Error,
795 ("0x%p OnKeyEvent(), FAILED, there are no context", this));
796 return KeyHandlingState::eNotHandled;
799 if (mSetCursorPositionOnKeyEvent) {
800 SetCursorPosition(currentContext);
801 mSetCursorPositionOnKeyEvent = false;
804 // Let's support dead key event even if active keyboard layout also
805 // supports complicated composition like CJK IME.
806 bool isDeadKey =
807 KeymapWrapper::ComputeDOMKeyNameIndex(aEvent) == KEY_NAME_INDEX_Dead;
808 mMaybeInDeadKeySequence |= isDeadKey;
810 // If current context is mSimpleContext, both ibus and fcitx handles key
811 // events synchronously. So, only when current context is mContext which
812 // is GtkIMMulticontext, the key event may be handled by IME asynchronously.
813 bool probablyHandledAsynchronously =
814 mIsIMInAsyncKeyHandlingMode && currentContext == mContext;
816 // If we're not sure whether the event is handled asynchronously, this is
817 // set to true.
818 bool maybeHandledAsynchronously = false;
820 // If aEvent is a synthesized event for async handling, this will be set to
821 // true.
822 bool isHandlingAsyncEvent = false;
824 // If we've decided that the event won't be synthesized asyncrhonously
825 // by IME, but actually IME did it, this is set to true.
826 bool isUnexpectedAsyncEvent = false;
828 // If IM is ibus or fcitx and it handles key events asynchronously,
829 // they mark aEvent->state as "handled by me" when they post key event
830 // to another process. Unfortunately, we need to check this hacky
831 // flag because it's difficult to store all pending key events by
832 // an array or a hashtable.
833 if (probablyHandledAsynchronously) {
834 switch (mIMContextID) {
835 case IMContextID::IBus: {
836 // See src/ibustypes.h
837 static const guint IBUS_IGNORED_MASK = 1 << 25;
838 // If IBUS_IGNORED_MASK was set to aEvent->state, the event
839 // has already been handled by another process and it wasn't
840 // used by IME.
841 isHandlingAsyncEvent = !!(aEvent->state & IBUS_IGNORED_MASK);
842 if (!isHandlingAsyncEvent) {
843 // On some environments, IBUS_IGNORED_MASK flag is not set as
844 // expected. In such case, we keep pusing all events into the queue.
845 // I.e., that causes eating a lot of memory until it's blurred.
846 // Therefore, we need to check whether there is same timestamp event
847 // in the queue. This redundant cost should be low because in most
848 // causes, key events in the queue should be 2 or 4.
849 isHandlingAsyncEvent =
850 mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
851 if (isHandlingAsyncEvent) {
852 MOZ_LOG(gIMELog, LogLevel::Info,
853 ("0x%p OnKeyEvent(), aEvent->state does not have "
854 "IBUS_IGNORED_MASK but "
855 "same event in the queue. So, assuming it's a "
856 "synthesized event",
857 this));
861 // If it's a synthesized event, let's remove it from the posting
862 // event queue first. Otherwise the following blocks cannot use
863 // `break`.
864 if (isHandlingAsyncEvent) {
865 MOZ_LOG(gIMELog, LogLevel::Info,
866 ("0x%p OnKeyEvent(), aEvent->state has IBUS_IGNORED_MASK "
867 "or aEvent is in the "
868 "posting event queue, so, it won't be handled "
869 "asynchronously anymore. Removing "
870 "the posted events from the queue",
871 this));
872 probablyHandledAsynchronously = false;
873 mPostingKeyEvents.RemoveEvent(aEvent);
876 // ibus won't send back key press events in a dead key sequcne.
877 if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
878 probablyHandledAsynchronously = false;
879 if (isHandlingAsyncEvent) {
880 isUnexpectedAsyncEvent = true;
881 break;
883 // Some keyboard layouts which have dead keys may send
884 // "empty" key event to make us call
885 // gtk_im_context_filter_keypress() to commit composed
886 // character during a GDK_KEY_PRESS event dispatching.
887 if (!gdk_keyval_to_unicode(aEvent->keyval) &&
888 !aEvent->hardware_keycode) {
889 isUnexpectedAsyncEvent = true;
890 break;
892 break;
894 // ibus may handle key events synchronously if focused editor is
895 // <input type="password"> or |ime-mode: disabled;|. However, in
896 // some environments, not so actually. Therefore, we need to check
897 // the result of gtk_im_context_filter_keypress() later.
898 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
899 probablyHandledAsynchronously = false;
900 maybeHandledAsynchronously = !isHandlingAsyncEvent;
901 break;
903 break;
905 case IMContextID::Fcitx:
906 case IMContextID::Fcitx5: {
907 // See src/lib/fcitx-utils/keysym.h
908 static const guint FcitxKeyState_IgnoredMask = 1 << 25;
909 // If FcitxKeyState_IgnoredMask was set to aEvent->state,
910 // the event has already been handled by another process and
911 // it wasn't used by IME.
912 isHandlingAsyncEvent = !!(aEvent->state & FcitxKeyState_IgnoredMask);
913 if (!isHandlingAsyncEvent) {
914 // On some environments, FcitxKeyState_IgnoredMask flag *might* be not
915 // set as expected. If there were such cases, we'd keep pusing all
916 // events into the queue. I.e., that would cause eating a lot of
917 // memory until it'd be blurred. Therefore, we should check whether
918 // there is same timestamp event in the queue. This redundant cost
919 // should be low because in most causes, key events in the queue
920 // should be 2 or 4.
921 isHandlingAsyncEvent =
922 mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
923 if (isHandlingAsyncEvent) {
924 MOZ_LOG(gIMELog, LogLevel::Info,
925 ("0x%p OnKeyEvent(), aEvent->state does not have "
926 "FcitxKeyState_IgnoredMask "
927 "but same event in the queue. So, assuming it's a "
928 "synthesized event",
929 this));
933 // fcitx won't send back key press events in a dead key sequcne.
934 if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
935 probablyHandledAsynchronously = false;
936 if (isHandlingAsyncEvent) {
937 isUnexpectedAsyncEvent = true;
938 break;
940 // Some keyboard layouts which have dead keys may send
941 // "empty" key event to make us call
942 // gtk_im_context_filter_keypress() to commit composed
943 // character during a GDK_KEY_PRESS event dispatching.
944 if (!gdk_keyval_to_unicode(aEvent->keyval) &&
945 !aEvent->hardware_keycode) {
946 isUnexpectedAsyncEvent = true;
947 break;
951 // fcitx handles key events asynchronously even if focused
952 // editor cannot use IME actually.
954 if (isHandlingAsyncEvent) {
955 MOZ_LOG(gIMELog, LogLevel::Info,
956 ("0x%p OnKeyEvent(), aEvent->state has "
957 "FcitxKeyState_IgnoredMask or aEvent is in "
958 "the posting event queue, so, it won't be handled "
959 "asynchronously anymore. "
960 "Removing the posted events from the queue",
961 this));
962 probablyHandledAsynchronously = false;
963 mPostingKeyEvents.RemoveEvent(aEvent);
964 break;
966 break;
968 default:
969 MOZ_ASSERT_UNREACHABLE(
970 "IME may handle key event "
971 "asyncrhonously, but not yet confirmed if it comes agian "
972 "actually");
976 if (!isUnexpectedAsyncEvent) {
977 mKeyboardEventWasDispatched = aKeyboardEventWasDispatched;
978 mKeyboardEventWasConsumed = false;
979 } else {
980 // If we didn't expect this event, we've alreday dispatched eKeyDown
981 // event or eKeyUp event for that.
982 mKeyboardEventWasDispatched = true;
983 // And in this case, we need to assume that another key event hasn't
984 // been receivied and mKeyboardEventWasConsumed keeps storing the
985 // dispatched eKeyDown or eKeyUp event's state.
987 mFallbackToKeyEvent = false;
988 mProcessingKeyEvent = aEvent;
989 gboolean isFiltered = gtk_im_context_filter_keypress(currentContext, aEvent);
991 // If we're not sure whether the event is handled by IME asynchronously or
992 // synchronously, we need to trust the result of
993 // gtk_im_context_filter_keypress(). If it consumed and but did nothing,
994 // we can assume that another event will be synthesized.
995 if (!isHandlingAsyncEvent && maybeHandledAsynchronously) {
996 probablyHandledAsynchronously |=
997 isFiltered && !mFallbackToKeyEvent && !mKeyboardEventWasDispatched;
1000 if (aEvent->type == GDK_KEY_PRESS) {
1001 if (isFiltered && probablyHandledAsynchronously) {
1002 sWaitingSynthesizedKeyPressHardwareKeyCode = aEvent->hardware_keycode;
1003 } else {
1004 sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
1008 // The caller of this shouldn't handle aEvent anymore if we've dispatched
1009 // composition events or modified content with other events.
1010 bool filterThisEvent = isFiltered && !mFallbackToKeyEvent;
1012 if (IsComposingOnCurrentContext() && !isFiltered &&
1013 aEvent->type == GDK_KEY_PRESS && mDispatchedCompositionString.IsEmpty()) {
1014 // A Hangul input engine for SCIM doesn't emit preedit_end
1015 // signal even when composition string becomes empty. On the
1016 // other hand, we should allow to make composition with empty
1017 // string for other languages because there *might* be such
1018 // IM. For compromising this issue, we should dispatch
1019 // compositionend event, however, we don't need to reset IM
1020 // actually.
1021 // NOTE: Don't dispatch key events as "processed by IME" since
1022 // we need to dispatch keyboard events as IME wasn't handled it.
1023 mProcessingKeyEvent = nullptr;
1024 DispatchCompositionCommitEvent(currentContext, &EmptyString());
1025 mProcessingKeyEvent = aEvent;
1026 // In this case, even though we handle the keyboard event here,
1027 // but we should dispatch keydown event as
1028 filterThisEvent = false;
1031 if (filterThisEvent && !mKeyboardEventWasDispatched) {
1032 // If IME handled the key event but we've not dispatched eKeyDown nor
1033 // eKeyUp event yet, we need to dispatch here unless the key event is
1034 // now being handled by other IME process.
1035 if (!probablyHandledAsynchronously) {
1036 MaybeDispatchKeyEventAsProcessedByIME(eVoidEvent);
1037 // Be aware, the widget might have been gone here.
1039 // If we need to wait reply from IM, IM may send some signals to us
1040 // without sending the key event again. In such case, we need to
1041 // dispatch keyboard events with a copy of aEvent. Therefore, we
1042 // need to use information of this key event to dispatch an KeyDown
1043 // or eKeyUp event later.
1044 else {
1045 MOZ_LOG(gIMELog, LogLevel::Info,
1046 ("0x%p OnKeyEvent(), putting aEvent into the queue...", this));
1047 mPostingKeyEvents.PutEvent(aEvent);
1051 mProcessingKeyEvent = nullptr;
1053 if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) {
1054 // If the key event hasn't been handled by active IME nor keyboard
1055 // layout, we can assume that the dead key sequence has been or was
1056 // ended. Note that we should not reset it when the key event is
1057 // GDK_KEY_RELEASE since it may not be filtered by active keyboard
1058 // layout even in composition.
1059 mMaybeInDeadKeySequence = false;
1062 if (aEvent->type == GDK_KEY_RELEASE) {
1063 if (const GdkEventKey* pendingKeyPressEvent =
1064 mPostingKeyEvents.GetCorrespondingKeyPressEvent(aEvent)) {
1065 MOZ_LOG(gIMELog, LogLevel::Warning,
1066 ("0x%p OnKeyEvent(), forgetting a pending GDK_KEY_PRESS event "
1067 "because GDK_KEY_RELEASE for the event is handled",
1068 this));
1069 mPostingKeyEvents.RemoveEvent(pendingKeyPressEvent);
1073 MOZ_LOG(
1074 gIMELog, LogLevel::Debug,
1075 ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s "
1076 "(isFiltered=%s, mFallbackToKeyEvent=%s, "
1077 "probablyHandledAsynchronously=%s, maybeHandledAsynchronously=%s), "
1078 "mPostingKeyEvents.Length()=%zu, mCompositionState=%s, "
1079 "mMaybeInDeadKeySequence=%s, mKeyboardEventWasDispatched=%s, "
1080 "mKeyboardEventWasConsumed=%s",
1081 this, ToChar(filterThisEvent), ToChar(isFiltered),
1082 ToChar(mFallbackToKeyEvent), ToChar(probablyHandledAsynchronously),
1083 ToChar(maybeHandledAsynchronously), mPostingKeyEvents.Length(),
1084 GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence),
1085 ToChar(mKeyboardEventWasDispatched), ToChar(mKeyboardEventWasConsumed)));
1086 MOZ_LOG(gIMELog, LogLevel::Info, ("<<<<<<<<<<<<<<<<\n\n"));
1088 if (filterThisEvent) {
1089 return KeyHandlingState::eHandled;
1091 // If another call of this method has already dispatched eKeyDown event,
1092 // we should return KeyHandlingState::eNotHandledButEventDispatched because
1093 // the caller should've stopped handling the event if preceding eKeyDown
1094 // event was consumed.
1095 if (aKeyboardEventWasDispatched) {
1096 return KeyHandlingState::eNotHandledButEventDispatched;
1098 if (!mKeyboardEventWasDispatched) {
1099 return KeyHandlingState::eNotHandled;
1101 return mKeyboardEventWasConsumed
1102 ? KeyHandlingState::eNotHandledButEventConsumed
1103 : KeyHandlingState::eNotHandledButEventDispatched;
1106 void IMContextWrapper::OnFocusChangeInGecko(bool aFocus) {
1107 MOZ_LOG(gIMELog, LogLevel::Info,
1108 ("0x%p OnFocusChangeInGecko(aFocus=%s),mCompositionState=%s, "
1109 "mIMEFocusState=%s, mSetInputPurposeAndInputHints=%s",
1110 this, ToChar(aFocus), GetCompositionStateName(),
1111 ToString(mIMEFocusState).c_str(),
1112 ToChar(mSetInputPurposeAndInputHints)));
1114 // We shouldn't carry over the removed string to another editor.
1115 mSelectedStringRemovedByComposition.Truncate();
1116 mContentSelection.reset();
1118 if (aFocus) {
1119 if (mSetInputPurposeAndInputHints) {
1120 mSetInputPurposeAndInputHints = false;
1121 SetInputPurposeAndInputHints();
1123 NotifyIMEOfFocusChange(IMEFocusState::Focused);
1124 } else {
1125 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
1128 // When the focus changes, we need to inform IM about the new cursor
1129 // position. Chinese input methods generally rely on this because they
1130 // usually don't start composition until a character is picked.
1131 if (aFocus && EnsureToCacheContentSelection()) {
1132 SetCursorPosition(GetActiveContext());
1136 void IMContextWrapper::ResetIME() {
1137 MOZ_LOG(gIMELog, LogLevel::Info,
1138 ("0x%p ResetIME(), mCompositionState=%s, mIMEFocusState=%s", this,
1139 GetCompositionStateName(), ToString(mIMEFocusState).c_str()));
1141 GtkIMContext* activeContext = GetActiveContext();
1142 if (MOZ_UNLIKELY(!activeContext)) {
1143 MOZ_LOG(gIMELog, LogLevel::Error,
1144 ("0x%p ResetIME(), FAILED, there are no context", this));
1145 return;
1148 RefPtr<IMContextWrapper> kungFuDeathGrip(this);
1149 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
1151 mPendingResettingIMContext = false;
1152 gtk_im_context_reset(activeContext);
1154 // The last focused window might have been destroyed by a DOM event handler
1155 // which was called by us during a call of gtk_im_context_reset().
1156 if (!lastFocusedWindow ||
1157 NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) ||
1158 lastFocusedWindow->Destroyed()) {
1159 return;
1162 nsAutoString compositionString;
1163 GetCompositionString(activeContext, compositionString);
1165 MOZ_LOG(gIMELog, LogLevel::Debug,
1166 ("0x%p ResetIME() called gtk_im_context_reset(), "
1167 "activeContext=0x%p, mCompositionState=%s, compositionString=%s, "
1168 "mIMEFocusState=%s",
1169 this, activeContext, GetCompositionStateName(),
1170 NS_ConvertUTF16toUTF8(compositionString).get(),
1171 ToString(mIMEFocusState).c_str()));
1173 // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still
1174 // used in Japan!) sends only "preedit_changed" signal with empty
1175 // composition string synchronously. Therefore, if composition string
1176 // is now empty string, we should assume that the IME won't send
1177 // "commit" signal.
1178 if (IsComposing() && compositionString.IsEmpty()) {
1179 // WARNING: The widget might have been gone after this.
1180 DispatchCompositionCommitEvent(activeContext, &EmptyString());
1184 nsresult IMContextWrapper::EndIMEComposition(nsWindow* aCaller) {
1185 if (MOZ_UNLIKELY(IsDestroyed())) {
1186 return NS_OK;
1189 MOZ_LOG(gIMELog, LogLevel::Info,
1190 ("0x%p EndIMEComposition(aCaller=0x%p), "
1191 "mCompositionState=%s",
1192 this, aCaller, GetCompositionStateName()));
1194 if (aCaller != mLastFocusedWindow) {
1195 MOZ_LOG(gIMELog, LogLevel::Error,
1196 ("0x%p EndIMEComposition(), FAILED, the caller isn't "
1197 "focused window, mLastFocusedWindow=0x%p",
1198 this, mLastFocusedWindow));
1199 return NS_OK;
1202 if (!IsComposing()) {
1203 return NS_OK;
1206 // Currently, GTK has API neither to commit nor to cancel composition
1207 // forcibly. Therefore, TextComposition will recompute commit string for
1208 // the request even if native IME will cause unexpected commit string.
1209 // So, we don't need to emulate commit or cancel composition with
1210 // proper composition events.
1211 // XXX ResetIME() might not enough for finishing compositoin on some
1212 // environments. We should emulate focus change too because some IMEs
1213 // may commit or cancel composition at blur.
1214 ResetIME();
1216 return NS_OK;
1219 void IMContextWrapper::OnLayoutChange() {
1220 if (MOZ_UNLIKELY(IsDestroyed())) {
1221 return;
1224 if (IsComposing()) {
1225 SetCursorPosition(GetActiveContext());
1226 } else {
1227 // If not composing, candidate window position is updated before key
1228 // down
1229 mSetCursorPositionOnKeyEvent = true;
1231 mLayoutChanged = true;
1234 void IMContextWrapper::OnUpdateComposition() {
1235 if (MOZ_UNLIKELY(IsDestroyed())) {
1236 return;
1239 if (!IsComposing()) {
1240 // Composition has been committed. So we need update selection for
1241 // caret later
1242 mContentSelection.reset();
1243 EnsureToCacheContentSelection();
1244 mSetCursorPositionOnKeyEvent = true;
1247 // If we've already set candidate window position, we don't need to update
1248 // the position with update composition notification.
1249 if (!mLayoutChanged) {
1250 SetCursorPosition(GetActiveContext());
1254 void IMContextWrapper::SetInputContext(nsWindow* aCaller,
1255 const InputContext* aContext,
1256 const InputContextAction* aAction) {
1257 if (MOZ_UNLIKELY(IsDestroyed())) {
1258 return;
1261 MOZ_LOG(gIMELog, LogLevel::Info,
1262 ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ "
1263 "mEnabled=%s }, mHTMLInputType=%s })",
1264 this, aCaller, ToString(aContext->mIMEState.mEnabled).c_str(),
1265 NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));
1267 if (aCaller != mLastFocusedWindow) {
1268 MOZ_LOG(gIMELog, LogLevel::Error,
1269 ("0x%p SetInputContext(), FAILED, "
1270 "the caller isn't focused window, mLastFocusedWindow=0x%p",
1271 this, mLastFocusedWindow));
1272 return;
1275 if (!mContext) {
1276 MOZ_LOG(gIMELog, LogLevel::Error,
1277 ("0x%p SetInputContext(), FAILED, "
1278 "there are no context",
1279 this));
1280 return;
1283 if (sLastFocusedContext != this) {
1284 mInputContext = *aContext;
1285 MOZ_LOG(gIMELog, LogLevel::Debug,
1286 ("0x%p SetInputContext(), succeeded, "
1287 "but we're not active",
1288 this));
1289 return;
1292 const bool changingEnabledState =
1293 aContext->IsInputAttributeChanged(mInputContext);
1295 // Release current IME focus if IME is enabled.
1296 if (changingEnabledState && mInputContext.mIMEState.IsEditable()) {
1297 if (IsComposing()) {
1298 EndIMEComposition(mLastFocusedWindow);
1300 if (mIMEFocusState == IMEFocusState::Focused) {
1301 NotifyIMEOfFocusChange(IMEFocusState::BlurredWithoutFocusChange);
1305 mInputContext = *aContext;
1306 mSetInputPurposeAndInputHints = false;
1308 if (!changingEnabledState || !mInputContext.mIMEState.IsEditable()) {
1309 return;
1312 // If the input context was temporarily disabled without a focus change,
1313 // it must be ready to query content even if the focused content is in
1314 // a remote process. In this case, we should set IME focus right now.
1315 if (mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
1316 SetInputPurposeAndInputHints();
1317 NotifyIMEOfFocusChange(IMEFocusState::Focused);
1318 return;
1321 // Otherwise, we cannot set input-purpose and input-hints right now because
1322 // setting them may require to set focus immediately for IME own's UI.
1323 // However, at this moment, `ContentCacheInParent` does not have content
1324 // cache, it'll be available after `NOTIFY_IME_OF_FOCUS` notification.
1325 // Therefore, we set them at receiving the notification.
1326 mSetInputPurposeAndInputHints = true;
1329 void IMContextWrapper::SetInputPurposeAndInputHints() {
1330 GtkIMContext* currentContext = GetCurrentContext();
1331 if (!currentContext) {
1332 return;
1335 GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM;
1336 const nsString& inputType = mInputContext.mHTMLInputType;
1337 // Password case has difficult issue. Desktop IMEs disable composition if
1338 // input-purpose is password. For disabling IME on |ime-mode: disabled;|, we
1339 // need to check mEnabled value instead of inputType value. This hack also
1340 // enables composition on <input type="password" style="ime-mode: enabled;">.
1341 // This is right behavior of ime-mode on desktop.
1343 // On the other hand, IME for tablet devices may provide a specific software
1344 // keyboard for password field. If so, the behavior might look strange on
1345 // both:
1346 // <input type="text" style="ime-mode: disabled;">
1347 // <input type="password" style="ime-mode: enabled;">
1349 // Temporarily, we should focus on desktop environment for now. I.e., let's
1350 // ignore tablet devices for now. When somebody reports actual trouble on
1351 // tablet devices, we should try to look for a way to solve actual problem.
1352 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
1353 purpose = GTK_INPUT_PURPOSE_PASSWORD;
1354 } else if (inputType.EqualsLiteral("email")) {
1355 purpose = GTK_INPUT_PURPOSE_EMAIL;
1356 } else if (inputType.EqualsLiteral("url")) {
1357 purpose = GTK_INPUT_PURPOSE_URL;
1358 } else if (inputType.EqualsLiteral("tel")) {
1359 purpose = GTK_INPUT_PURPOSE_PHONE;
1360 } else if (inputType.EqualsLiteral("number")) {
1361 purpose = GTK_INPUT_PURPOSE_NUMBER;
1362 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("decimal")) {
1363 purpose = GTK_INPUT_PURPOSE_NUMBER;
1364 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("email")) {
1365 purpose = GTK_INPUT_PURPOSE_EMAIL;
1366 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("numeric")) {
1367 purpose = GTK_INPUT_PURPOSE_DIGITS;
1368 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("tel")) {
1369 purpose = GTK_INPUT_PURPOSE_PHONE;
1370 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("url")) {
1371 purpose = GTK_INPUT_PURPOSE_URL;
1373 // Search by type and inputmode isn't supported on GTK.
1375 g_object_set(currentContext, "input-purpose", purpose, nullptr);
1377 // Although GtkInputHints is enum type, value is bit field.
1378 gint hints = GTK_INPUT_HINT_NONE;
1379 if (mInputContext.mHTMLInputMode.EqualsLiteral("none")) {
1380 hints |= GTK_INPUT_HINT_INHIBIT_OSK;
1383 if (mInputContext.mAutocapitalize.EqualsLiteral("characters")) {
1384 hints |= GTK_INPUT_HINT_UPPERCASE_CHARS;
1385 } else if (mInputContext.mAutocapitalize.EqualsLiteral("sentences")) {
1386 hints |= GTK_INPUT_HINT_UPPERCASE_SENTENCES;
1387 } else if (mInputContext.mAutocapitalize.EqualsLiteral("words")) {
1388 hints |= GTK_INPUT_HINT_UPPERCASE_WORDS;
1391 g_object_set(currentContext, "input-hints", hints, nullptr);
1394 InputContext IMContextWrapper::GetInputContext() {
1395 mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
1396 return mInputContext;
1399 GtkIMContext* IMContextWrapper::GetCurrentContext() const {
1400 if (IsEnabled()) {
1401 return mContext;
1403 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
1404 return mSimpleContext;
1406 return mDummyContext;
1409 bool IMContextWrapper::IsValidContext(GtkIMContext* aContext) const {
1410 if (!aContext) {
1411 return false;
1413 return aContext == mContext || aContext == mSimpleContext ||
1414 aContext == mDummyContext;
1417 bool IMContextWrapper::IsEnabled() const {
1418 return mInputContext.mIMEState.mEnabled == IMEEnabled::Enabled ||
1419 (!sUseSimpleContext &&
1420 mInputContext.mIMEState.mEnabled == IMEEnabled::Password);
1423 void IMContextWrapper::NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState) {
1424 MOZ_ASSERT_IF(aIMEFocusState == IMEFocusState::BlurredWithoutFocusChange,
1425 mIMEFocusState != IMEFocusState::Blurred);
1426 if (mIMEFocusState == aIMEFocusState) {
1427 return;
1430 MOZ_LOG(gIMELog, LogLevel::Info,
1431 ("0x%p NotifyIMEOfFocusChange(aIMEFocusState=%s), mIMEFocusState=%s, "
1432 "sLastFocusedContext=0x%p",
1433 this, ToString(aIMEFocusState).c_str(),
1434 ToString(mIMEFocusState).c_str(), sLastFocusedContext));
1435 MOZ_ASSERT(!mSetInputPurposeAndInputHints);
1437 // If we've already made IME blurred at setting the input context disabled
1438 // and it's now completely blurred by a focus move, we need only to update
1439 // mIMEFocusState and when the input context gets enabled, we cannot set
1440 // IME focus immediately.
1441 if (aIMEFocusState == IMEFocusState::Blurred &&
1442 mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
1443 mIMEFocusState = IMEFocusState::Blurred;
1444 return;
1447 auto Blur = [&](IMEFocusState aInternalState) {
1448 GtkIMContext* currentContext = GetCurrentContext();
1449 if (MOZ_UNLIKELY(!currentContext)) {
1450 MOZ_LOG(gIMELog, LogLevel::Error,
1451 ("0x%p NotifyIMEOfFocusChange()::Blur(), FAILED, "
1452 "there is no context",
1453 this));
1454 return;
1456 gtk_im_context_focus_out(currentContext);
1457 mIMEFocusState = aInternalState;
1460 if (aIMEFocusState != IMEFocusState::Focused) {
1461 return Blur(aIMEFocusState);
1464 GtkIMContext* currentContext = GetCurrentContext();
1465 if (MOZ_UNLIKELY(!currentContext)) {
1466 MOZ_LOG(gIMELog, LogLevel::Error,
1467 ("0x%p NotifyIMEOfFocusChange(), FAILED, "
1468 "there is no context",
1469 this));
1470 return;
1473 if (sLastFocusedContext && sLastFocusedContext != this) {
1474 sLastFocusedContext->NotifyIMEOfFocusChange(IMEFocusState::Blurred);
1477 sLastFocusedContext = this;
1479 // Forget all posted key events when focus is moved since they shouldn't
1480 // be fired in different editor.
1481 sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
1482 mPostingKeyEvents.Clear();
1484 gtk_im_context_focus_in(currentContext);
1485 mIMEFocusState = aIMEFocusState;
1486 mSetCursorPositionOnKeyEvent = true;
1488 if (!IsEnabled()) {
1489 // We should release IME focus for uim and scim.
1490 // These IMs are using snooper that is released at losing focus.
1491 Blur(IMEFocusState::BlurredWithoutFocusChange);
1495 void IMContextWrapper::OnSelectionChange(
1496 nsWindow* aCaller, const IMENotification& aIMENotification) {
1497 const bool isSelectionRangeChanged =
1498 mContentSelection.isNothing() ||
1499 !aIMENotification.mSelectionChangeData.EqualsRange(
1500 mContentSelection.ref());
1501 mContentSelection =
1502 Some(ContentSelection(aIMENotification.mSelectionChangeData));
1503 const bool retrievedSurroundingSignalReceived =
1504 mRetrieveSurroundingSignalReceived;
1505 mRetrieveSurroundingSignalReceived = false;
1507 if (MOZ_UNLIKELY(IsDestroyed())) {
1508 return;
1511 const IMENotification::SelectionChangeDataBase& selectionChangeData =
1512 aIMENotification.mSelectionChangeData;
1514 MOZ_LOG(gIMELog, LogLevel::Info,
1515 ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
1516 "mSelectionChangeData=%s }), "
1517 "mCompositionState=%s, mIsDeletingSurrounding=%s, "
1518 "mRetrieveSurroundingSignalReceived=%s, isSelectionRangeChanged=%s",
1519 this, aCaller, ToString(selectionChangeData).c_str(),
1520 GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
1521 ToChar(retrievedSurroundingSignalReceived),
1522 ToChar(isSelectionRangeChanged)));
1524 if (aCaller != mLastFocusedWindow) {
1525 MOZ_LOG(gIMELog, LogLevel::Error,
1526 ("0x%p OnSelectionChange(), FAILED, "
1527 "the caller isn't focused window, mLastFocusedWindow=0x%p",
1528 this, mLastFocusedWindow));
1529 return;
1532 if (!IsComposing()) {
1533 // Now we have no composition (mostly situation on calling this method)
1534 // If we have it, it will set by
1535 // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED.
1536 mSetCursorPositionOnKeyEvent = true;
1539 // The focused editor might have placeholder text with normal text node.
1540 // In such case, the text node must be removed from a compositionstart
1541 // event handler. So, we're dispatching eCompositionStart,
1542 // we should ignore selection change notification.
1543 if (mCompositionState == eCompositionState_CompositionStartDispatched) {
1544 if (NS_WARN_IF(mContentSelection.isNothing())) {
1545 MOZ_LOG(gIMELog, LogLevel::Error,
1546 ("0x%p OnSelectionChange(), FAILED, "
1547 "new offset is too large, cannot keep composing",
1548 this));
1549 } else if (mContentSelection->HasRange()) {
1550 // Modify the selection start offset with new offset.
1551 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
1552 // XXX We should modify mSelectedStringRemovedByComposition?
1553 // But how?
1554 MOZ_LOG(gIMELog, LogLevel::Debug,
1555 ("0x%p OnSelectionChange(), ignored, mCompositionStart "
1556 "is updated to %u, the selection change doesn't cause "
1557 "resetting IM context",
1558 this, mCompositionStart));
1559 // And don't reset the IM context.
1560 return;
1561 } else {
1562 MOZ_LOG(
1563 gIMELog, LogLevel::Debug,
1564 ("0x%p OnSelectionChange(), ignored, because of no selection range",
1565 this));
1566 return;
1568 // Otherwise, reset the IM context due to impossible to keep composing.
1571 // If the selection change is caused by deleting surrounding text,
1572 // we shouldn't need to notify IME of selection change.
1573 if (mIsDeletingSurrounding) {
1574 return;
1577 bool occurredBeforeComposition =
1578 IsComposing() && !selectionChangeData.mOccurredDuringComposition &&
1579 !selectionChangeData.mCausedByComposition;
1580 if (occurredBeforeComposition) {
1581 mPendingResettingIMContext = true;
1584 // When the selection change is caused by dispatching composition event,
1585 // selection set event and/or occurred before starting current composition,
1586 // we shouldn't notify IME of that and commit existing composition.
1587 // Don't do this even if selection is not changed actually. For example,
1588 // fcitx has direct input mode which does not insert composing string, but
1589 // inserts commited text for each key sequence (i.e., there is "invisible"
1590 // composition string). In the world after bug 1712269, we don't use a
1591 // set of composition events for this kind of IME. Therefore,
1592 // SelectionChangeData.mCausedByComposition is not expected value for here
1593 // if this call is caused by a preceding commit. And if the preceding commit
1594 // is triggered by a key type for next word, resetting IME state makes fcitx
1595 // discard the pending input for the next word. Thus, we need to check
1596 // whether the selection range is actually changed here.
1597 if (!selectionChangeData.mCausedByComposition &&
1598 !selectionChangeData.mCausedBySelectionEvent && isSelectionRangeChanged &&
1599 !occurredBeforeComposition) {
1600 // Hack for ibus-pinyin. ibus-pinyin will synthesize a set of
1601 // composition which commits with empty string after calling
1602 // gtk_im_context_reset(). Therefore, selecting text causes
1603 // unexpectedly removing it. For preventing it but not breaking the
1604 // other IMEs which use surrounding text, we should call it only when
1605 // surrounding text has been retrieved after last selection range was
1606 // set. If it's not retrieved, that means that current IME doesn't
1607 // have any content cache, so, it must not need the notification of
1608 // selection change.
1609 if (IsComposing() || retrievedSurroundingSignalReceived) {
1610 ResetIME();
1615 /* static */
1616 void IMContextWrapper::OnThemeChanged() {
1617 if (auto* provider = SelectionStyleProvider::GetExistingInstance()) {
1618 provider->OnThemeChanged();
1622 /* static */
1623 void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
1624 IMContextWrapper* aModule) {
1625 aModule->OnStartCompositionNative(aContext);
1628 void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) {
1629 // IME may synthesize composition asynchronously after filtering a
1630 // GDK_KEY_PRESS event. In that case, we should handle composition with
1631 // emulating the usual case, i.e., this is called in the stack of
1632 // OnKeyEvent().
1633 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1634 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1635 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1636 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1637 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1638 KEY_NAME_INDEX_USE_STRING) {
1639 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1640 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1644 MOZ_LOG(gIMELog, LogLevel::Info,
1645 ("0x%p OnStartCompositionNative(aContext=0x%p), "
1646 "current context=0x%p, mComposingContext=0x%p",
1647 this, aContext, GetCurrentContext(), mComposingContext));
1649 // See bug 472635, we should do nothing if IM context doesn't match.
1650 if (GetCurrentContext() != aContext) {
1651 MOZ_LOG(gIMELog, LogLevel::Error,
1652 ("0x%p OnStartCompositionNative(), FAILED, "
1653 "given context doesn't match",
1654 this));
1655 return;
1658 if (mComposingContext && aContext != mComposingContext) {
1659 // XXX For now, we should ignore this odd case, just logging.
1660 MOZ_LOG(gIMELog, LogLevel::Warning,
1661 ("0x%p OnStartCompositionNative(), Warning, "
1662 "there is already a composing context but starting new "
1663 "composition with different context",
1664 this));
1667 // IME may start composition without "preedit_start" signal. Therefore,
1668 // mComposingContext will be initialized in DispatchCompositionStart().
1670 if (!DispatchCompositionStart(aContext)) {
1671 return;
1673 mCompositionTargetRange.mOffset = mCompositionStart;
1674 mCompositionTargetRange.mLength = 0;
1677 /* static */
1678 void IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext,
1679 IMContextWrapper* aModule) {
1680 aModule->OnEndCompositionNative(aContext);
1683 void IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext) {
1684 MOZ_LOG(gIMELog, LogLevel::Info,
1685 ("0x%p OnEndCompositionNative(aContext=0x%p), mComposingContext=0x%p",
1686 this, aContext, mComposingContext));
1688 // See bug 472635, we should do nothing if IM context doesn't match.
1689 // Note that if this is called after focus move, the context may different
1690 // from any our owning context.
1691 if (!IsValidContext(aContext)) {
1692 MOZ_LOG(gIMELog, LogLevel::Error,
1693 ("0x%p OnEndCompositionNative(), FAILED, "
1694 "given context doesn't match with any context",
1695 this));
1696 return;
1699 // If we've not started composition with aContext, we should ignore it.
1700 if (aContext != mComposingContext) {
1701 MOZ_LOG(gIMELog, LogLevel::Warning,
1702 ("0x%p OnEndCompositionNative(), Warning, "
1703 "given context doesn't match with mComposingContext",
1704 this));
1705 return;
1708 g_object_unref(mComposingContext);
1709 mComposingContext = nullptr;
1711 // If we already handled the commit event, we should do nothing here.
1712 if (IsComposing()) {
1713 if (!DispatchCompositionCommitEvent(aContext)) {
1714 // If the widget is destroyed, we should do nothing anymore.
1715 return;
1719 if (mPendingResettingIMContext) {
1720 ResetIME();
1724 /* static */
1725 void IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext,
1726 IMContextWrapper* aModule) {
1727 aModule->OnChangeCompositionNative(aContext);
1730 void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) {
1731 // IME may synthesize composition asynchronously after filtering a
1732 // GDK_KEY_PRESS event. In that case, we should handle composition with
1733 // emulating the usual case, i.e., this is called in the stack of
1734 // OnKeyEvent().
1735 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1736 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1737 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1738 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1739 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1740 KEY_NAME_INDEX_USE_STRING) {
1741 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1742 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1746 MOZ_LOG(gIMELog, LogLevel::Info,
1747 ("0x%p OnChangeCompositionNative(aContext=0x%p), "
1748 "mComposingContext=0x%p",
1749 this, aContext, mComposingContext));
1751 // See bug 472635, we should do nothing if IM context doesn't match.
1752 // Note that if this is called after focus move, the context may different
1753 // from any our owning context.
1754 if (!IsValidContext(aContext)) {
1755 MOZ_LOG(gIMELog, LogLevel::Error,
1756 ("0x%p OnChangeCompositionNative(), FAILED, "
1757 "given context doesn't match with any context",
1758 this));
1759 return;
1762 if (mComposingContext && aContext != mComposingContext) {
1763 // XXX For now, we should ignore this odd case, just logging.
1764 MOZ_LOG(gIMELog, LogLevel::Warning,
1765 ("0x%p OnChangeCompositionNative(), Warning, "
1766 "given context doesn't match with composing context",
1767 this));
1770 nsAutoString compositionString;
1771 GetCompositionString(aContext, compositionString);
1772 if (!IsComposing() && compositionString.IsEmpty()) {
1773 MOZ_LOG(gIMELog, LogLevel::Warning,
1774 ("0x%p OnChangeCompositionNative(), Warning, does nothing "
1775 "because has not started composition and composing string is "
1776 "empty",
1777 this));
1778 mDispatchedCompositionString.Truncate();
1779 return; // Don't start the composition with empty string.
1782 // Be aware, widget can be gone
1783 DispatchCompositionChangeEvent(aContext, compositionString);
1786 /* static */
1787 gboolean IMContextWrapper::OnRetrieveSurroundingCallback(
1788 GtkIMContext* aContext, IMContextWrapper* aModule) {
1789 return aModule->OnRetrieveSurroundingNative(aContext);
1792 gboolean IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext) {
1793 MOZ_LOG(gIMELog, LogLevel::Info,
1794 ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), "
1795 "current context=0x%p",
1796 this, aContext, GetCurrentContext()));
1798 // See bug 472635, we should do nothing if IM context doesn't match.
1799 if (GetCurrentContext() != aContext) {
1800 MOZ_LOG(gIMELog, LogLevel::Error,
1801 ("0x%p OnRetrieveSurroundingNative(), FAILED, "
1802 "given context doesn't match",
1803 this));
1804 return FALSE;
1807 nsAutoString uniStr;
1808 uint32_t cursorPos;
1809 if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) {
1810 return FALSE;
1813 // Despite taking a pointer and a length, IBus wants the string to be
1814 // zero-terminated and doesn't like U+0000 within the string.
1815 uniStr.ReplaceChar(char16_t(0), char16_t(0xFFFD));
1817 NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
1818 uint32_t cursorPosInUTF8 = utf8Str.Length();
1819 AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
1820 gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
1821 cursorPosInUTF8);
1822 mRetrieveSurroundingSignalReceived = true;
1823 return TRUE;
1826 /* static */
1827 gboolean IMContextWrapper::OnDeleteSurroundingCallback(
1828 GtkIMContext* aContext, gint aOffset, gint aNChars,
1829 IMContextWrapper* aModule) {
1830 return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
1833 gboolean IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext,
1834 gint aOffset,
1835 gint aNChars) {
1836 MOZ_LOG(gIMELog, LogLevel::Info,
1837 ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, "
1838 "aNChar=%d), current context=0x%p",
1839 this, aContext, aOffset, aNChars, GetCurrentContext()));
1841 // See bug 472635, we should do nothing if IM context doesn't match.
1842 if (GetCurrentContext() != aContext) {
1843 MOZ_LOG(gIMELog, LogLevel::Error,
1844 ("0x%p OnDeleteSurroundingNative(), FAILED, "
1845 "given context doesn't match",
1846 this));
1847 return FALSE;
1850 AutoRestore<bool> saveDeletingSurrounding(mIsDeletingSurrounding);
1851 mIsDeletingSurrounding = true;
1852 if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) {
1853 return TRUE;
1856 // failed
1857 MOZ_LOG(gIMELog, LogLevel::Error,
1858 ("0x%p OnDeleteSurroundingNative(), FAILED, "
1859 "cannot delete text",
1860 this));
1861 return FALSE;
1864 /* static */
1865 void IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext,
1866 const gchar* aString,
1867 IMContextWrapper* aModule) {
1868 aModule->OnCommitCompositionNative(aContext, aString);
1871 void IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext,
1872 const gchar* aUTF8Char) {
1873 const gchar emptyStr = 0;
1874 const gchar* commitString = aUTF8Char ? aUTF8Char : &emptyStr;
1875 NS_ConvertUTF8toUTF16 utf16CommitString(commitString);
1877 // IME may synthesize composition asynchronously after filtering a
1878 // GDK_KEY_PRESS event. In that case, we should handle composition with
1879 // emulating the usual case, i.e., this is called in the stack of
1880 // OnKeyEvent().
1881 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1882 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1883 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1884 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1885 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1886 KEY_NAME_INDEX_USE_STRING) {
1887 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1888 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1892 MOZ_LOG(gIMELog, LogLevel::Info,
1893 ("0x%p OnCommitCompositionNative(aContext=0x%p), "
1894 "current context=0x%p, active context=0x%p, commitString=\"%s\", "
1895 "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s",
1896 this, aContext, GetCurrentContext(), GetActiveContext(),
1897 commitString, mProcessingKeyEvent, ToChar(IsComposingOn(aContext))));
1899 // See bug 472635, we should do nothing if IM context doesn't match.
1900 if (!IsValidContext(aContext)) {
1901 MOZ_LOG(gIMELog, LogLevel::Error,
1902 ("0x%p OnCommitCompositionNative(), FAILED, "
1903 "given context doesn't match",
1904 this));
1905 return;
1908 // If we are not in composition and committing with empty string,
1909 // we need to do nothing because if we continued to handle this
1910 // signal, we would dispatch compositionstart, text, compositionend
1911 // events with empty string. Of course, they are unnecessary events
1912 // for Web applications and our editor.
1913 if (!IsComposingOn(aContext) && utf16CommitString.IsEmpty()) {
1914 MOZ_LOG(gIMELog, LogLevel::Warning,
1915 ("0x%p OnCommitCompositionNative(), Warning, does nothing "
1916 "because has not started composition and commit string is empty",
1917 this));
1918 return;
1921 // If IME doesn't change their keyevent that generated this commit,
1922 // we should treat that IME didn't handle the key event because
1923 // web applications want to receive "keydown" and "keypress" event
1924 // in such case.
1925 // NOTE: While a key event is being handled, this might be caused on
1926 // current context. Otherwise, this may be caused on active context.
1927 if (!IsComposingOn(aContext) && mProcessingKeyEvent &&
1928 mProcessingKeyEvent->type == GDK_KEY_PRESS &&
1929 aContext == GetCurrentContext()) {
1930 char keyval_utf8[8]; /* should have at least 6 bytes of space */
1931 gint keyval_utf8_len;
1932 guint32 keyval_unicode;
1934 keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval);
1935 keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8);
1936 keyval_utf8[keyval_utf8_len] = '\0';
1938 // If committing string is exactly same as a character which is
1939 // produced by the key, eKeyDown and eKeyPress event should be
1940 // dispatched by the caller of OnKeyEvent() normally. Note that
1941 // mMaybeInDeadKeySequence will be set to false by OnKeyEvent()
1942 // since we set mFallbackToKeyEvent to true here.
1943 if (!strcmp(commitString, keyval_utf8)) {
1944 MOZ_LOG(gIMELog, LogLevel::Info,
1945 ("0x%p OnCommitCompositionNative(), "
1946 "we'll send normal key event",
1947 this));
1948 mFallbackToKeyEvent = true;
1949 return;
1952 // If we're in a dead key sequence, commit string is a character in
1953 // the BMP and mProcessingKeyEvent produces some characters but it's
1954 // not same as committing string, we should dispatch an eKeyPress
1955 // event from here.
1956 WidgetKeyboardEvent keyDownEvent(true, eKeyDown, mLastFocusedWindow);
1957 KeymapWrapper::InitKeyEvent(keyDownEvent, mProcessingKeyEvent, false);
1958 if (mMaybeInDeadKeySequence && utf16CommitString.Length() == 1 &&
1959 keyDownEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
1960 mKeyboardEventWasDispatched = true;
1961 // Anyway, we're not in dead key sequence anymore.
1962 mMaybeInDeadKeySequence = false;
1964 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
1965 nsresult rv = dispatcher->BeginNativeInputTransaction();
1966 if (NS_WARN_IF(NS_FAILED(rv))) {
1967 MOZ_LOG(gIMELog, LogLevel::Error,
1968 ("0x%p OnCommitCompositionNative(), FAILED, "
1969 "due to BeginNativeInputTransaction() failure",
1970 this));
1971 return;
1974 // First, dispatch eKeyDown event.
1975 keyDownEvent.mKeyValue = utf16CommitString;
1976 nsEventStatus status = nsEventStatus_eIgnore;
1977 bool dispatched = dispatcher->DispatchKeyboardEvent(
1978 eKeyDown, keyDownEvent, status, mProcessingKeyEvent);
1979 if (!dispatched || status == nsEventStatus_eConsumeNoDefault) {
1980 mKeyboardEventWasConsumed = true;
1981 MOZ_LOG(gIMELog, LogLevel::Info,
1982 ("0x%p OnCommitCompositionNative(), "
1983 "doesn't dispatch eKeyPress event because the preceding "
1984 "eKeyDown event was not dispatched or was consumed",
1985 this));
1986 return;
1988 if (mLastFocusedWindow != keyDownEvent.mWidget ||
1989 mLastFocusedWindow->Destroyed()) {
1990 MOZ_LOG(gIMELog, LogLevel::Warning,
1991 ("0x%p OnCommitCompositionNative(), Warning, "
1992 "stop dispatching eKeyPress event because the preceding "
1993 "eKeyDown event caused changing focused widget or "
1994 "destroyed",
1995 this));
1996 return;
1998 MOZ_LOG(gIMELog, LogLevel::Info,
1999 ("0x%p OnCommitCompositionNative(), "
2000 "dispatched eKeyDown event for the committed character",
2001 this));
2003 // Next, dispatch eKeyPress event.
2004 dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status,
2005 mProcessingKeyEvent);
2006 MOZ_LOG(gIMELog, LogLevel::Info,
2007 ("0x%p OnCommitCompositionNative(), "
2008 "dispatched eKeyPress event for the committed character",
2009 this));
2010 return;
2014 NS_ConvertUTF8toUTF16 str(commitString);
2015 // Be aware, widget can be gone
2016 DispatchCompositionCommitEvent(aContext, &str);
2019 void IMContextWrapper::GetCompositionString(GtkIMContext* aContext,
2020 nsAString& aCompositionString) {
2021 gchar* preedit_string;
2022 gint cursor_pos;
2023 PangoAttrList* feedback_list;
2024 gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
2025 &cursor_pos);
2026 if (preedit_string && *preedit_string) {
2027 CopyUTF8toUTF16(MakeStringSpan(preedit_string), aCompositionString);
2028 } else {
2029 aCompositionString.Truncate();
2032 MOZ_LOG(gIMELog, LogLevel::Info,
2033 ("0x%p GetCompositionString(aContext=0x%p), "
2034 "aCompositionString=\"%s\"",
2035 this, aContext, preedit_string));
2037 pango_attr_list_unref(feedback_list);
2038 g_free(preedit_string);
2041 bool IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME(
2042 EventMessage aFollowingEvent) {
2043 if (!mLastFocusedWindow) {
2044 return false;
2047 if (!mIsKeySnooped &&
2048 ((!mProcessingKeyEvent && mPostingKeyEvents.IsEmpty()) ||
2049 (mProcessingKeyEvent && mKeyboardEventWasDispatched))) {
2050 return true;
2053 // A "keydown" or "keyup" event handler may change focus with the
2054 // following event. In such case, we need to cancel this composition.
2055 // So, we need to store IM context now because mComposingContext may be
2056 // overwritten with different context if calling this method recursively.
2057 // Note that we don't need to grab the context here because |context|
2058 // will be used only for checking if it's same as mComposingContext.
2059 GtkIMContext* oldCurrentContext = GetCurrentContext();
2060 GtkIMContext* oldComposingContext = mComposingContext;
2062 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2064 if (mProcessingKeyEvent || !mPostingKeyEvents.IsEmpty()) {
2065 if (mProcessingKeyEvent) {
2066 mKeyboardEventWasDispatched = true;
2068 // If we're not handling a key event synchronously, the signal may be
2069 // sent by IME without sending key event to us. In such case, we
2070 // should dispatch keyboard event for the last key event which was
2071 // posted to other IME process.
2072 GdkEventKey* sourceEvent = mProcessingKeyEvent
2073 ? mProcessingKeyEvent
2074 : mPostingKeyEvents.GetFirstEvent();
2076 MOZ_LOG(
2077 gIMELog, LogLevel::Info,
2078 ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
2079 "aFollowingEvent=%s), dispatch %s %s "
2080 "event: { type=%s, keyval=%s, unicode=0x%X, state=%s, "
2081 "time=%u, hardware_keycode=%u, group=%u }",
2082 this, ToChar(aFollowingEvent),
2083 ToChar(sourceEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp),
2084 mProcessingKeyEvent ? "processing" : "posted",
2085 GetEventType(sourceEvent), gdk_keyval_name(sourceEvent->keyval),
2086 gdk_keyval_to_unicode(sourceEvent->keyval),
2087 GetEventStateName(sourceEvent->state, mIMContextID).get(),
2088 sourceEvent->time, sourceEvent->hardware_keycode, sourceEvent->group));
2090 // Let's dispatch eKeyDown event or eKeyUp event now. Note that only
2091 // when we're not in a dead key composition, we should mark the
2092 // eKeyDown and eKeyUp event as "processed by IME" since we should
2093 // expose raw keyCode and key value to web apps the key event is a
2094 // part of a dead key sequence.
2095 // FYI: We should ignore if default of preceding keydown or keyup
2096 // event is prevented since even on the other browsers, web
2097 // applications cannot cancel the following composition event.
2098 // Spec bug: https://github.com/w3c/uievents/issues/180
2099 KeymapWrapper::DispatchKeyDownOrKeyUpEvent(lastFocusedWindow, sourceEvent,
2100 !mMaybeInDeadKeySequence,
2101 &mKeyboardEventWasConsumed);
2102 MOZ_LOG(gIMELog, LogLevel::Info,
2103 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup "
2104 "event is dispatched",
2105 this));
2107 if (!mProcessingKeyEvent) {
2108 MOZ_LOG(gIMELog, LogLevel::Info,
2109 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), removing first "
2110 "event from the queue",
2111 this));
2112 mPostingKeyEvents.RemoveEvent(sourceEvent);
2114 } else {
2115 MOZ_ASSERT(mIsKeySnooped);
2116 // Currently, we support key snooper mode of uim and wayland only.
2117 MOZ_ASSERT(mIMContextID == IMContextID::Uim ||
2118 mIMContextID == IMContextID::Wayland);
2119 // uim sends "preedit_start" signal and "preedit_changed" separately
2120 // at starting composition, "commit" and "preedit_end" separately at
2121 // committing composition.
2123 // Currently, we should dispatch only fake eKeyDown event because
2124 // we cannot decide which is the last signal of each key operation
2125 // and Chromium also dispatches only "keydown" event in this case.
2126 bool dispatchFakeKeyDown = false;
2127 switch (aFollowingEvent) {
2128 case eCompositionStart:
2129 case eCompositionCommit:
2130 case eCompositionCommitAsIs:
2131 case eContentCommandInsertText:
2132 dispatchFakeKeyDown = true;
2133 break;
2134 // XXX Unfortunately, I don't have a good idea to prevent to
2135 // dispatch redundant eKeyDown event for eCompositionStart
2136 // immediately after "delete_surrounding" signal. However,
2137 // not dispatching eKeyDown event is worse than dispatching
2138 // redundant eKeyDown events.
2139 case eContentCommandDelete:
2140 dispatchFakeKeyDown = true;
2141 break;
2142 // We need to prevent to dispatch redundant eKeyDown event for
2143 // eCompositionChange immediately after eCompositionStart. So,
2144 // We should not dispatch eKeyDown event if dispatched composition
2145 // string is still empty string.
2146 case eCompositionChange:
2147 dispatchFakeKeyDown = !mDispatchedCompositionString.IsEmpty();
2148 break;
2149 default:
2150 MOZ_ASSERT_UNREACHABLE("Do you forget to handle the case?");
2151 break;
2154 if (dispatchFakeKeyDown) {
2155 WidgetKeyboardEvent fakeKeyDownEvent(true, eKeyDown, lastFocusedWindow);
2156 fakeKeyDownEvent.mKeyCode = NS_VK_PROCESSKEY;
2157 fakeKeyDownEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
2158 // It's impossible to get physical key information in this case but
2159 // this should be okay since web apps shouldn't do anything with
2160 // physical key information during composition.
2161 fakeKeyDownEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN;
2163 MOZ_LOG(gIMELog, LogLevel::Info,
2164 ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
2165 "aFollowingEvent=%s), dispatch fake eKeyDown event",
2166 this, ToChar(aFollowingEvent)));
2168 KeymapWrapper::DispatchKeyDownOrKeyUpEvent(
2169 lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed);
2170 MOZ_LOG(gIMELog, LogLevel::Info,
2171 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), "
2172 "fake keydown event is dispatched",
2173 this));
2177 if (lastFocusedWindow->IsDestroyed() ||
2178 lastFocusedWindow != mLastFocusedWindow) {
2179 MOZ_LOG(gIMELog, LogLevel::Warning,
2180 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the "
2181 "focused widget was destroyed/changed by a key event",
2182 this));
2183 return false;
2186 // If the dispatched keydown event caused moving focus and that also
2187 // caused changing active context, we need to cancel composition here.
2188 if (GetCurrentContext() != oldCurrentContext) {
2189 MOZ_LOG(gIMELog, LogLevel::Warning,
2190 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the key "
2191 "event causes changing active IM context",
2192 this));
2193 if (mComposingContext == oldComposingContext) {
2194 // Only when the context is still composing, we should call
2195 // ResetIME() here. Otherwise, it should've already been
2196 // cleaned up.
2197 ResetIME();
2199 return false;
2202 return true;
2205 bool IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext) {
2206 MOZ_LOG(gIMELog, LogLevel::Info,
2207 ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext));
2209 if (IsComposing()) {
2210 MOZ_LOG(gIMELog, LogLevel::Error,
2211 ("0x%p DispatchCompositionStart(), FAILED, "
2212 "we're already in composition",
2213 this));
2214 return true;
2217 if (!mLastFocusedWindow) {
2218 MOZ_LOG(gIMELog, LogLevel::Error,
2219 ("0x%p DispatchCompositionStart(), FAILED, "
2220 "there are no focused window in this module",
2221 this));
2222 return false;
2225 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
2226 MOZ_LOG(gIMELog, LogLevel::Error,
2227 ("0x%p DispatchCompositionStart(), FAILED, "
2228 "cannot query the selection offset",
2229 this));
2230 return false;
2233 if (NS_WARN_IF(!mContentSelection->HasRange())) {
2234 MOZ_LOG(gIMELog, LogLevel::Error,
2235 ("0x%p DispatchCompositionStart(), FAILED, "
2236 "due to no selection",
2237 this));
2238 return false;
2241 mComposingContext = static_cast<GtkIMContext*>(g_object_ref(aContext));
2242 MOZ_ASSERT(mComposingContext);
2244 // Keep the last focused window alive
2245 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2247 // XXX The composition start point might be changed by composition events
2248 // even though we strongly hope it doesn't happen.
2249 // Every composition event should have the start offset for the result
2250 // because it may high cost if we query the offset every time.
2251 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
2252 mDispatchedCompositionString.Truncate();
2254 // If this composition is started by a key press, we need to dispatch
2255 // eKeyDown or eKeyUp event before dispatching eCompositionStart event.
2256 // Note that dispatching a keyboard event which is marked as "processed
2257 // by IME" is okay since Chromium also dispatches keyboard event as so.
2258 if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionStart)) {
2259 MOZ_LOG(gIMELog, LogLevel::Warning,
2260 ("0x%p DispatchCompositionStart(), Warning, "
2261 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2262 this));
2263 return false;
2266 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
2267 nsresult rv = dispatcher->BeginNativeInputTransaction();
2268 if (NS_WARN_IF(NS_FAILED(rv))) {
2269 MOZ_LOG(gIMELog, LogLevel::Error,
2270 ("0x%p DispatchCompositionStart(), FAILED, "
2271 "due to BeginNativeInputTransaction() failure",
2272 this));
2273 return false;
2276 static bool sHasSetTelemetry = false;
2277 if (!sHasSetTelemetry) {
2278 sHasSetTelemetry = true;
2279 NS_ConvertUTF8toUTF16 im(GetIMName());
2280 // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
2281 if (im.Length() > 72) {
2282 if (NS_IS_SURROGATE_PAIR(im[72 - 2], im[72 - 1])) {
2283 im.Truncate(72 - 2);
2284 } else {
2285 im.Truncate(72 - 1);
2287 // U+2026 is "..."
2288 im.Append(char16_t(0x2026));
2290 Telemetry::ScalarSet(Telemetry::ScalarID::WIDGET_IME_NAME_ON_LINUX, im,
2291 true);
2294 MOZ_LOG(gIMELog, LogLevel::Debug,
2295 ("0x%p DispatchCompositionStart(), dispatching "
2296 "compositionstart... (mCompositionStart=%u)",
2297 this, mCompositionStart));
2298 mCompositionState = eCompositionState_CompositionStartDispatched;
2299 nsEventStatus status;
2300 dispatcher->StartComposition(status);
2301 if (lastFocusedWindow->IsDestroyed() ||
2302 lastFocusedWindow != mLastFocusedWindow) {
2303 MOZ_LOG(gIMELog, LogLevel::Error,
2304 ("0x%p DispatchCompositionStart(), FAILED, the focused "
2305 "widget was destroyed/changed by compositionstart event",
2306 this));
2307 return false;
2310 return true;
2313 bool IMContextWrapper::DispatchCompositionChangeEvent(
2314 GtkIMContext* aContext, const nsAString& aCompositionString) {
2315 MOZ_LOG(
2316 gIMELog, LogLevel::Info,
2317 ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext));
2319 if (!mLastFocusedWindow) {
2320 MOZ_LOG(gIMELog, LogLevel::Error,
2321 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2322 "there are no focused window in this module",
2323 this));
2324 return false;
2327 if (!IsComposing()) {
2328 MOZ_LOG(gIMELog, LogLevel::Debug,
2329 ("0x%p DispatchCompositionChangeEvent(), the composition "
2330 "wasn't started, force starting...",
2331 this));
2332 if (!DispatchCompositionStart(aContext)) {
2333 return false;
2336 // If this composition string change caused by a key press, we need to
2337 // dispatch eKeyDown or eKeyUp before dispatching eCompositionChange event.
2338 else if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionChange)) {
2339 MOZ_LOG(gIMELog, LogLevel::Warning,
2340 ("0x%p DispatchCompositionChangeEvent(), Warning, "
2341 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2342 this));
2343 return false;
2346 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
2347 nsresult rv = dispatcher->BeginNativeInputTransaction();
2348 if (NS_WARN_IF(NS_FAILED(rv))) {
2349 MOZ_LOG(gIMELog, LogLevel::Error,
2350 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2351 "due to BeginNativeInputTransaction() failure",
2352 this));
2353 return false;
2356 // Store the selected string which will be removed by following
2357 // compositionchange event.
2358 if (mCompositionState == eCompositionState_CompositionStartDispatched) {
2359 if (NS_WARN_IF(!EnsureToCacheContentSelection(
2360 &mSelectedStringRemovedByComposition))) {
2361 // XXX How should we behave in this case??
2362 } else if (mContentSelection->HasRange()) {
2363 // XXX We should assume, for now, any web applications don't change
2364 // selection at handling this compositionchange event.
2365 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
2366 } else {
2367 // If there is no selection range, we should keep previously storing
2368 // mCompositionStart.
2372 RefPtr<TextRangeArray> rangeArray =
2373 CreateTextRangeArray(aContext, aCompositionString);
2375 rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray);
2376 if (NS_WARN_IF(NS_FAILED(rv))) {
2377 MOZ_LOG(gIMELog, LogLevel::Error,
2378 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2379 "due to SetPendingComposition() failure",
2380 this));
2381 return false;
2384 mCompositionState = eCompositionState_CompositionChangeEventDispatched;
2386 // We cannot call SetCursorPosition for e10s-aware.
2387 // DispatchEvent is async on e10s, so composition rect isn't updated now
2388 // on tab parent.
2389 mDispatchedCompositionString = aCompositionString;
2390 mLayoutChanged = false;
2391 mCompositionTargetRange.mOffset =
2392 mCompositionStart + rangeArray->TargetClauseOffset();
2393 mCompositionTargetRange.mLength = rangeArray->TargetClauseLength();
2395 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2396 nsEventStatus status;
2397 rv = dispatcher->FlushPendingComposition(status);
2398 if (NS_WARN_IF(NS_FAILED(rv))) {
2399 MOZ_LOG(gIMELog, LogLevel::Error,
2400 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2401 "due to FlushPendingComposition() failure",
2402 this));
2403 return false;
2406 if (lastFocusedWindow->IsDestroyed() ||
2407 lastFocusedWindow != mLastFocusedWindow) {
2408 MOZ_LOG(gIMELog, LogLevel::Error,
2409 ("0x%p DispatchCompositionChangeEvent(), FAILED, the "
2410 "focused widget was destroyed/changed by "
2411 "compositionchange event",
2412 this));
2413 return false;
2415 return true;
2418 bool IMContextWrapper::DispatchCompositionCommitEvent(
2419 GtkIMContext* aContext, const nsAString* aCommitString) {
2420 MOZ_LOG(gIMELog, LogLevel::Info,
2421 ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, "
2422 "aCommitString=0x%p, (\"%s\"))",
2423 this, aContext, aCommitString,
2424 aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));
2426 if (!mLastFocusedWindow) {
2427 MOZ_LOG(gIMELog, LogLevel::Error,
2428 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2429 "there are no focused window in this module",
2430 this));
2431 return false;
2434 // TODO: We need special care to handle request to commit composition
2435 // by content while we're committing composition because we have
2436 // commit string information now but IME may not have composition
2437 // anymore. Therefore, we may not be able to handle commit as
2438 // expected. However, this is rare case because this situation
2439 // never occurs with remote content. So, it's okay to fix this
2440 // issue later. (Perhaps, TextEventDisptcher should do it for
2441 // all platforms. E.g., creating WillCommitComposition()?)
2442 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2443 RefPtr<TextEventDispatcher> dispatcher;
2444 if (!IsComposing() &&
2445 !StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
2446 if (!aCommitString || aCommitString->IsEmpty()) {
2447 MOZ_LOG(gIMELog, LogLevel::Error,
2448 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2449 "did nothing due to inserting empty string without composition",
2450 this));
2451 return true;
2453 if (MOZ_UNLIKELY(!EnsureToCacheContentSelection())) {
2454 MOZ_LOG(gIMELog, LogLevel::Warning,
2455 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2456 "Failed to cache selection before dispatching "
2457 "eContentCommandInsertText event",
2458 this));
2460 if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandInsertText)) {
2461 MOZ_LOG(gIMELog, LogLevel::Warning,
2462 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2463 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2464 this));
2465 return false;
2467 // Emulate selection until receiving actual selection range. This is
2468 // important for OnSelectionChange. If selection is not changed by web
2469 // apps, i.e., selection range is same as what selection expects, we
2470 // shouldn't reset IME because the trigger of causing this commit may be an
2471 // input for next composition and we shouldn't cancel it.
2472 if (mContentSelection.isSome()) {
2473 mContentSelection->Collapse(
2474 (mContentSelection->HasRange()
2475 ? mContentSelection->OffsetAndDataRef().StartOffset()
2476 : mCompositionStart) +
2477 aCommitString->Length());
2478 MOZ_LOG(gIMELog, LogLevel::Info,
2479 ("0x%p DispatchCompositionCommitEvent(), mContentSelection=%s",
2480 this, ToString(mContentSelection).c_str()));
2482 MOZ_ASSERT(!dispatcher);
2483 } else {
2484 if (!IsComposing()) {
2485 if (!aCommitString || aCommitString->IsEmpty()) {
2486 MOZ_LOG(gIMELog, LogLevel::Error,
2487 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2488 "there is no composition and empty commit string",
2489 this));
2490 return true;
2492 MOZ_LOG(gIMELog, LogLevel::Debug,
2493 ("0x%p DispatchCompositionCommitEvent(), "
2494 "the composition wasn't started, force starting...",
2495 this));
2496 if (!DispatchCompositionStart(aContext)) {
2497 return false;
2500 // If this commit caused by a key press, we need to dispatch eKeyDown or
2501 // eKeyUp before dispatching composition events.
2502 else if (!MaybeDispatchKeyEventAsProcessedByIME(
2503 aCommitString ? eCompositionCommit : eCompositionCommitAsIs)) {
2504 MOZ_LOG(gIMELog, LogLevel::Warning,
2505 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2506 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2507 this));
2508 mCompositionState = eCompositionState_NotComposing;
2509 return false;
2512 dispatcher = GetTextEventDispatcher();
2513 MOZ_ASSERT(dispatcher);
2514 nsresult rv = dispatcher->BeginNativeInputTransaction();
2515 if (NS_WARN_IF(NS_FAILED(rv))) {
2516 MOZ_LOG(gIMELog, LogLevel::Error,
2517 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2518 "due to BeginNativeInputTransaction() failure",
2519 this));
2520 return false;
2523 // Emulate selection until receiving actual selection range.
2524 const uint32_t offsetToPutCaret =
2525 mCompositionStart + (aCommitString
2526 ? aCommitString->Length()
2527 : mDispatchedCompositionString.Length());
2528 if (mContentSelection.isSome()) {
2529 mContentSelection->Collapse(offsetToPutCaret);
2530 } else {
2531 // TODO: We should guarantee that there should be at least fake selection
2532 // for IME at here. Then, we can keep the last writing mode.
2533 mContentSelection.emplace(offsetToPutCaret, WritingMode());
2537 mCompositionState = eCompositionState_NotComposing;
2538 // Reset dead key sequence too because GTK doesn't support dead key chain
2539 // (i.e., a key press doesn't cause both producing some characters and
2540 // restarting new dead key sequence at one time). So, committing
2541 // composition means end of a dead key sequence.
2542 mMaybeInDeadKeySequence = false;
2543 mCompositionStart = UINT32_MAX;
2544 mCompositionTargetRange.Clear();
2545 mDispatchedCompositionString.Truncate();
2546 mSelectedStringRemovedByComposition.Truncate();
2548 if (!dispatcher) {
2549 MOZ_ASSERT(aCommitString);
2550 MOZ_ASSERT(!aCommitString->IsEmpty());
2551 nsEventStatus status = nsEventStatus_eIgnore;
2552 WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText,
2553 lastFocusedWindow);
2554 insertTextEvent.mString.emplace(*aCommitString);
2555 lastFocusedWindow->DispatchEvent(&insertTextEvent, status);
2557 if (!insertTextEvent.mSucceeded) {
2558 MOZ_LOG(gIMELog, LogLevel::Error,
2559 ("0x%p DispatchCompositionChangeEvent(), FAILED, inserting "
2560 "text failed",
2561 this));
2562 return false;
2564 } else {
2565 nsEventStatus status = nsEventStatus_eIgnore;
2566 nsresult rv = dispatcher->CommitComposition(status, aCommitString);
2567 if (NS_WARN_IF(NS_FAILED(rv))) {
2568 MOZ_LOG(gIMELog, LogLevel::Error,
2569 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2570 "due to CommitComposition() failure",
2571 this));
2572 return false;
2576 if (lastFocusedWindow->IsDestroyed() ||
2577 lastFocusedWindow != mLastFocusedWindow) {
2578 MOZ_LOG(gIMELog, LogLevel::Error,
2579 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2580 "the focused widget was destroyed/changed by "
2581 "compositioncommit event",
2582 this));
2583 return false;
2586 return true;
2589 already_AddRefed<TextRangeArray> IMContextWrapper::CreateTextRangeArray(
2590 GtkIMContext* aContext, const nsAString& aCompositionString) {
2591 MOZ_LOG(gIMELog, LogLevel::Info,
2592 ("0x%p CreateTextRangeArray(aContext=0x%p, "
2593 "aCompositionString=\"%s\" (Length()=%zu))",
2594 this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(),
2595 aCompositionString.Length()));
2597 RefPtr<TextRangeArray> textRangeArray = new TextRangeArray();
2599 gchar* preedit_string;
2600 gint cursor_pos_in_chars;
2601 PangoAttrList* feedback_list;
2602 gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
2603 &cursor_pos_in_chars);
2604 if (!preedit_string || !*preedit_string) {
2605 if (!aCompositionString.IsEmpty()) {
2606 MOZ_LOG(gIMELog, LogLevel::Error,
2607 ("0x%p CreateTextRangeArray(), FAILED, due to "
2608 "preedit_string is null",
2609 this));
2611 pango_attr_list_unref(feedback_list);
2612 g_free(preedit_string);
2613 return textRangeArray.forget();
2616 // Convert caret offset from offset in characters to offset in UTF-16
2617 // string. If we couldn't proper offset in UTF-16 string, we should
2618 // assume that the caret is at the end of the composition string.
2619 uint32_t caretOffsetInUTF16 = aCompositionString.Length();
2620 if (NS_WARN_IF(cursor_pos_in_chars < 0)) {
2621 // Note that this case is undocumented. We should assume that the
2622 // caret is at the end of the composition string.
2623 } else if (cursor_pos_in_chars == 0) {
2624 caretOffsetInUTF16 = 0;
2625 } else {
2626 gchar* charAfterCaret =
2627 g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars);
2628 if (NS_WARN_IF(!charAfterCaret)) {
2629 MOZ_LOG(gIMELog, LogLevel::Warning,
2630 ("0x%p CreateTextRangeArray(), failed to get UTF-8 "
2631 "string before the caret (cursor_pos_in_chars=%d)",
2632 this, cursor_pos_in_chars));
2633 } else {
2634 glong caretOffset = 0;
2635 gunichar2* utf16StrBeforeCaret =
2636 g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string,
2637 nullptr, &caretOffset, nullptr);
2638 if (NS_WARN_IF(!utf16StrBeforeCaret) || NS_WARN_IF(caretOffset < 0)) {
2639 MOZ_LOG(gIMELog, LogLevel::Warning,
2640 ("0x%p CreateTextRangeArray(), WARNING, failed to "
2641 "convert to UTF-16 string before the caret "
2642 "(cursor_pos_in_chars=%d, caretOffset=%ld)",
2643 this, cursor_pos_in_chars, caretOffset));
2644 } else {
2645 caretOffsetInUTF16 = static_cast<uint32_t>(caretOffset);
2646 uint32_t compositionStringLength = aCompositionString.Length();
2647 if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) {
2648 MOZ_LOG(gIMELog, LogLevel::Warning,
2649 ("0x%p CreateTextRangeArray(), WARNING, "
2650 "caretOffsetInUTF16=%u is larger than "
2651 "compositionStringLength=%u",
2652 this, caretOffsetInUTF16, compositionStringLength));
2653 caretOffsetInUTF16 = compositionStringLength;
2656 if (utf16StrBeforeCaret) {
2657 g_free(utf16StrBeforeCaret);
2662 PangoAttrIterator* iter;
2663 iter = pango_attr_list_get_iterator(feedback_list);
2664 if (!iter) {
2665 MOZ_LOG(gIMELog, LogLevel::Error,
2666 ("0x%p CreateTextRangeArray(), FAILED, iterator couldn't "
2667 "be allocated",
2668 this));
2669 pango_attr_list_unref(feedback_list);
2670 g_free(preedit_string);
2671 return textRangeArray.forget();
2674 uint32_t minOffsetOfClauses = aCompositionString.Length();
2675 uint32_t maxOffsetOfClauses = 0;
2676 do {
2677 TextRange range;
2678 if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) {
2679 continue;
2681 MOZ_ASSERT(range.Length());
2682 minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset);
2683 maxOffsetOfClauses = std::max(maxOffsetOfClauses, range.mEndOffset);
2684 textRangeArray->AppendElement(range);
2685 } while (pango_attr_iterator_next(iter));
2687 // If the IME doesn't define clause from the start of the composition,
2688 // we should insert dummy clause information since TextRangeArray assumes
2689 // that there must be a clause whose start is 0 when there is one or
2690 // more clauses.
2691 if (minOffsetOfClauses) {
2692 TextRange dummyClause;
2693 dummyClause.mStartOffset = 0;
2694 dummyClause.mEndOffset = minOffsetOfClauses;
2695 dummyClause.mRangeType = TextRangeType::eRawClause;
2696 textRangeArray->InsertElementAt(0, dummyClause);
2697 maxOffsetOfClauses = std::max(maxOffsetOfClauses, dummyClause.mEndOffset);
2698 MOZ_LOG(gIMELog, LogLevel::Warning,
2699 ("0x%p CreateTextRangeArray(), inserting a dummy clause "
2700 "at the beginning of the composition string mStartOffset=%u, "
2701 "mEndOffset=%u, mRangeType=%s",
2702 this, dummyClause.mStartOffset, dummyClause.mEndOffset,
2703 ToChar(dummyClause.mRangeType)));
2706 // If the IME doesn't define clause at end of the composition, we should
2707 // insert dummy clause information since TextRangeArray assumes that there
2708 // must be a clase whose end is the length of the composition string when
2709 // there is one or more clauses.
2710 if (!textRangeArray->IsEmpty() &&
2711 maxOffsetOfClauses < aCompositionString.Length()) {
2712 TextRange dummyClause;
2713 dummyClause.mStartOffset = maxOffsetOfClauses;
2714 dummyClause.mEndOffset = aCompositionString.Length();
2715 dummyClause.mRangeType = TextRangeType::eRawClause;
2716 textRangeArray->AppendElement(dummyClause);
2717 MOZ_LOG(gIMELog, LogLevel::Warning,
2718 ("0x%p CreateTextRangeArray(), inserting a dummy clause "
2719 "at the end of the composition string mStartOffset=%u, "
2720 "mEndOffset=%u, mRangeType=%s",
2721 this, dummyClause.mStartOffset, dummyClause.mEndOffset,
2722 ToChar(dummyClause.mRangeType)));
2725 TextRange range;
2726 range.mStartOffset = range.mEndOffset = caretOffsetInUTF16;
2727 range.mRangeType = TextRangeType::eCaret;
2728 textRangeArray->AppendElement(range);
2729 MOZ_LOG(
2730 gIMELog, LogLevel::Debug,
2731 ("0x%p CreateTextRangeArray(), mStartOffset=%u, "
2732 "mEndOffset=%u, mRangeType=%s",
2733 this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
2735 pango_attr_iterator_destroy(iter);
2736 pango_attr_list_unref(feedback_list);
2737 g_free(preedit_string);
2739 return textRangeArray.forget();
2742 /* static */
2743 nscolor IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor) {
2744 PangoColor& pangoColor = aPangoAttrColor->color;
2745 uint8_t r = pangoColor.red / 0x100;
2746 uint8_t g = pangoColor.green / 0x100;
2747 uint8_t b = pangoColor.blue / 0x100;
2748 return NS_RGB(r, g, b);
2751 bool IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter,
2752 const gchar* aUTF8CompositionString,
2753 uint32_t aUTF16CaretOffset,
2754 TextRange& aTextRange) const {
2755 // Set the range offsets in UTF-16 string.
2756 gint utf8ClauseStart, utf8ClauseEnd;
2757 pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd);
2758 if (utf8ClauseStart == utf8ClauseEnd) {
2759 MOZ_LOG(gIMELog, LogLevel::Error,
2760 ("0x%p SetTextRange(), FAILED, due to collapsed range", this));
2761 return false;
2764 if (!utf8ClauseStart) {
2765 aTextRange.mStartOffset = 0;
2766 } else {
2767 glong utf16PreviousClausesLength;
2768 gunichar2* utf16PreviousClausesString =
2769 g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr,
2770 &utf16PreviousClausesLength, nullptr);
2772 if (NS_WARN_IF(!utf16PreviousClausesString)) {
2773 MOZ_LOG(gIMELog, LogLevel::Error,
2774 ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
2775 "failure (retrieving previous string of current clause)",
2776 this));
2777 return false;
2780 aTextRange.mStartOffset = utf16PreviousClausesLength;
2781 g_free(utf16PreviousClausesString);
2784 glong utf16CurrentClauseLength;
2785 gunichar2* utf16CurrentClauseString = g_utf8_to_utf16(
2786 aUTF8CompositionString + utf8ClauseStart, utf8ClauseEnd - utf8ClauseStart,
2787 nullptr, &utf16CurrentClauseLength, nullptr);
2789 if (NS_WARN_IF(!utf16CurrentClauseString)) {
2790 MOZ_LOG(gIMELog, LogLevel::Error,
2791 ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
2792 "failure (retrieving current clause)",
2793 this));
2794 return false;
2797 // iBus Chewing IME tells us that there is an empty clause at the end of
2798 // the composition string but we should ignore it since our code doesn't
2799 // assume that there is an empty clause.
2800 if (!utf16CurrentClauseLength) {
2801 MOZ_LOG(gIMELog, LogLevel::Warning,
2802 ("0x%p SetTextRange(), FAILED, due to current clause length "
2803 "is 0",
2804 this));
2805 return false;
2808 aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength;
2809 g_free(utf16CurrentClauseString);
2810 utf16CurrentClauseString = nullptr;
2812 // Set styles
2813 TextRangeStyle& style = aTextRange.mRangeStyle;
2815 // Underline
2816 PangoAttrInt* attrUnderline = reinterpret_cast<PangoAttrInt*>(
2817 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE));
2818 if (attrUnderline) {
2819 switch (attrUnderline->value) {
2820 case PANGO_UNDERLINE_NONE:
2821 style.mLineStyle = TextRangeStyle::LineStyle::None;
2822 break;
2823 case PANGO_UNDERLINE_DOUBLE:
2824 style.mLineStyle = TextRangeStyle::LineStyle::Double;
2825 break;
2826 case PANGO_UNDERLINE_ERROR:
2827 style.mLineStyle = TextRangeStyle::LineStyle::Wavy;
2828 break;
2829 case PANGO_UNDERLINE_SINGLE:
2830 case PANGO_UNDERLINE_LOW:
2831 style.mLineStyle = TextRangeStyle::LineStyle::Solid;
2832 break;
2833 default:
2834 MOZ_LOG(gIMELog, LogLevel::Warning,
2835 ("0x%p SetTextRange(), retrieved unknown underline "
2836 "style: %d",
2837 this, attrUnderline->value));
2838 style.mLineStyle = TextRangeStyle::LineStyle::Solid;
2839 break;
2841 style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
2843 // Underline color
2844 PangoAttrColor* attrUnderlineColor = reinterpret_cast<PangoAttrColor*>(
2845 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE_COLOR));
2846 if (attrUnderlineColor) {
2847 style.mUnderlineColor = ToNscolor(attrUnderlineColor);
2848 style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR;
2850 } else {
2851 style.mLineStyle = TextRangeStyle::LineStyle::None;
2852 style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
2855 // Don't set colors if they are not specified. They should be computed by
2856 // textframe if only one of the colors are specified.
2858 // Foreground color (text color)
2859 PangoAttrColor* attrForeground = reinterpret_cast<PangoAttrColor*>(
2860 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND));
2861 if (attrForeground) {
2862 style.mForegroundColor = ToNscolor(attrForeground);
2863 style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR;
2866 // Background color
2867 PangoAttrColor* attrBackground = reinterpret_cast<PangoAttrColor*>(
2868 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND));
2869 if (attrBackground) {
2870 style.mBackgroundColor = ToNscolor(attrBackground);
2871 style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR;
2875 * We need to judge the meaning of the clause for a11y. Before we support
2876 * IME specific composition string style, we used following rules:
2878 * 1: If attrUnderline and attrForground are specified, we assumed the
2879 * clause is TextRangeType::eSelectedClause.
2880 * 2: If only attrUnderline is specified, we assumed the clause is
2881 * TextRangeType::eConvertedClause.
2882 * 3: If only attrForground is specified, we assumed the clause is
2883 * TextRangeType::eSelectedRawClause.
2884 * 4: If neither attrUnderline nor attrForeground is specified, we assumed
2885 * the clause is TextRangeType::eRawClause.
2887 * However, this rules are odd since there can be two or more selected
2888 * clauses. Additionally, our old rules caused that IME developers/users
2889 * cannot specify composition string style as they want.
2891 * So, we shouldn't guess the meaning from its visual style.
2894 // If the range covers whole of composition string and the caret is at
2895 // the end of the composition string, the range is probably not converted.
2896 if (!utf8ClauseStart &&
2897 utf8ClauseEnd == static_cast<gint>(strlen(aUTF8CompositionString)) &&
2898 aTextRange.mEndOffset == aUTF16CaretOffset) {
2899 aTextRange.mRangeType = TextRangeType::eRawClause;
2901 // Typically, the caret is set at the start of the selected clause.
2902 // So, if the caret is in the clause, we can assume that the clause is
2903 // selected.
2904 else if (aTextRange.mStartOffset <= aUTF16CaretOffset &&
2905 aTextRange.mEndOffset > aUTF16CaretOffset) {
2906 aTextRange.mRangeType = TextRangeType::eSelectedClause;
2908 // Otherwise, we should assume that the clause is converted but not
2909 // selected.
2910 else {
2911 aTextRange.mRangeType = TextRangeType::eConvertedClause;
2914 MOZ_LOG(gIMELog, LogLevel::Debug,
2915 ("0x%p SetTextRange(), succeeded, aTextRange= { "
2916 "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }",
2917 this, aTextRange.mStartOffset, aTextRange.mEndOffset,
2918 ToChar(aTextRange.mRangeType),
2919 GetTextRangeStyleText(aTextRange.mRangeStyle).get()));
2921 return true;
2924 void IMContextWrapper::SetCursorPosition(GtkIMContext* aContext) {
2925 MOZ_LOG(
2926 gIMELog, LogLevel::Info,
2927 ("0x%p SetCursorPosition(aContext=0x%p), "
2928 "mCompositionTargetRange={ mOffset=%u, mLength=%u }, "
2929 "mContentSelection=%s",
2930 this, aContext, mCompositionTargetRange.mOffset,
2931 mCompositionTargetRange.mLength, ToString(mContentSelection).c_str()));
2933 bool useCaret = false;
2934 if (!mCompositionTargetRange.IsValid()) {
2935 if (mContentSelection.isNothing()) {
2936 MOZ_LOG(gIMELog, LogLevel::Error,
2937 ("0x%p SetCursorPosition(), FAILED, "
2938 "mCompositionTargetRange and mContentSelection are invalid",
2939 this));
2940 return;
2942 if (!mContentSelection->HasRange()) {
2943 MOZ_LOG(gIMELog, LogLevel::Warning,
2944 ("0x%p SetCursorPosition(), FAILED, "
2945 "mCompositionTargetRange is invalid and there is no selection",
2946 this));
2947 return;
2949 useCaret = true;
2952 if (!mLastFocusedWindow) {
2953 MOZ_LOG(gIMELog, LogLevel::Error,
2954 ("0x%p SetCursorPosition(), FAILED, due to no focused "
2955 "window",
2956 this));
2957 return;
2960 if (MOZ_UNLIKELY(!aContext)) {
2961 MOZ_LOG(gIMELog, LogLevel::Error,
2962 ("0x%p SetCursorPosition(), FAILED, due to no context", this));
2963 return;
2966 WidgetQueryContentEvent queryCaretOrTextRectEvent(
2967 true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow);
2968 if (useCaret) {
2969 queryCaretOrTextRectEvent.InitForQueryCaretRect(
2970 mContentSelection->OffsetAndDataRef().StartOffset());
2971 } else {
2972 if (mContentSelection->WritingModeRef().IsVertical()) {
2973 // For preventing the candidate window to overlap the target
2974 // clause, we should set fake (typically, very tall) caret rect.
2975 uint32_t length =
2976 mCompositionTargetRange.mLength ? mCompositionTargetRange.mLength : 1;
2977 queryCaretOrTextRectEvent.InitForQueryTextRect(
2978 mCompositionTargetRange.mOffset, length);
2979 } else {
2980 queryCaretOrTextRectEvent.InitForQueryTextRect(
2981 mCompositionTargetRange.mOffset, 1);
2984 nsEventStatus status;
2985 mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status);
2986 if (queryCaretOrTextRectEvent.Failed()) {
2987 MOZ_LOG(gIMELog, LogLevel::Error,
2988 ("0x%p SetCursorPosition(), FAILED, %s was failed", this,
2989 useCaret ? "eQueryCaretRect" : "eQueryTextRect"));
2990 return;
2993 nsWindow* rootWindow =
2994 static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());
2996 // Get the position of the rootWindow in screen.
2997 LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset();
2999 // Get the position of IM context owner window in screen.
3000 LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset();
3002 // Compute the caret position in the IM owner window.
3003 LayoutDeviceIntRect rect =
3004 queryCaretOrTextRectEvent.mReply->mRect + root - owner;
3005 rect.width = 0;
3006 GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect);
3008 gtk_im_context_set_cursor_location(aContext, &area);
3011 nsresult IMContextWrapper::GetCurrentParagraph(nsAString& aText,
3012 uint32_t& aCursorPos) {
3013 MOZ_LOG(gIMELog, LogLevel::Info,
3014 ("0x%p GetCurrentParagraph(), mCompositionState=%s", this,
3015 GetCompositionStateName()));
3017 if (!mLastFocusedWindow) {
3018 MOZ_LOG(gIMELog, LogLevel::Error,
3019 ("0x%p GetCurrentParagraph(), FAILED, there are no "
3020 "focused window in this module",
3021 this));
3022 return NS_ERROR_NULL_POINTER;
3025 nsEventStatus status;
3027 uint32_t selOffset = mCompositionStart;
3028 uint32_t selLength = mSelectedStringRemovedByComposition.Length();
3030 // If focused editor doesn't have composition string, we should use
3031 // current selection.
3032 if (!EditorHasCompositionString()) {
3033 // Query cursor position & selection
3034 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
3035 MOZ_LOG(gIMELog, LogLevel::Error,
3036 ("0x%p GetCurrentParagraph(), FAILED, due to no "
3037 "valid selection information",
3038 this));
3039 return NS_ERROR_FAILURE;
3042 if (mContentSelection.isSome() && mContentSelection->HasRange()) {
3043 selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
3044 selLength = mContentSelection->OffsetAndDataRef().Length();
3045 } else {
3046 // If there is no range, let's get all text instead...
3047 selOffset = 0u;
3048 selLength = INT32_MAX; // TODO: Change to UINT32_MAX, but see below
3052 MOZ_LOG(gIMELog, LogLevel::Debug,
3053 ("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u", this,
3054 selOffset, selLength));
3056 // XXX nsString::Find and nsString::RFind take int32_t for offset, so,
3057 // we cannot support this request when the current offset is larger
3058 // than INT32_MAX.
3059 if (selOffset > INT32_MAX || selLength > INT32_MAX ||
3060 selOffset + selLength > INT32_MAX) {
3061 MOZ_LOG(gIMELog, LogLevel::Error,
3062 ("0x%p GetCurrentParagraph(), FAILED, The selection is "
3063 "out of range",
3064 this));
3065 return NS_ERROR_FAILURE;
3068 // Get all text contents of the focused editor
3069 WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
3070 mLastFocusedWindow);
3071 queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
3072 mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
3073 if (NS_WARN_IF(queryTextContentEvent.Failed())) {
3074 return NS_ERROR_FAILURE;
3077 if (selOffset + selLength > queryTextContentEvent.mReply->DataLength()) {
3078 MOZ_LOG(gIMELog, LogLevel::Error,
3079 ("0x%p GetCurrentParagraph(), FAILED, The selection is "
3080 "invalid, queryTextContentEvent={ mReply=%s }",
3081 this, ToString(queryTextContentEvent.mReply).c_str()));
3082 return NS_ERROR_FAILURE;
3085 // Remove composing string and restore the selected string because
3086 // GtkEntry doesn't remove selected string until committing, however,
3087 // our editor does it. We should emulate the behavior for IME.
3088 nsAutoString textContent(queryTextContentEvent.mReply->DataRef());
3089 if (EditorHasCompositionString() &&
3090 mDispatchedCompositionString != mSelectedStringRemovedByComposition) {
3091 textContent.Replace(mCompositionStart,
3092 mDispatchedCompositionString.Length(),
3093 mSelectedStringRemovedByComposition);
3096 // Get only the focused paragraph, by looking for newlines
3097 int32_t parStart = 0;
3098 if (selOffset > 0) {
3099 parStart = Substring(textContent, 0, selOffset - 1).RFind(u"\n") + 1;
3101 int32_t parEnd = textContent.Find(u"\n", selOffset + selLength);
3102 if (parEnd < 0) {
3103 parEnd = textContent.Length();
3105 aText = nsDependentSubstring(textContent, parStart, parEnd - parStart);
3106 aCursorPos = selOffset - uint32_t(parStart);
3108 MOZ_LOG(
3109 gIMELog, LogLevel::Debug,
3110 ("0x%p GetCurrentParagraph(), succeeded, aText=%s, "
3111 "aText.Length()=%zu, aCursorPos=%u",
3112 this, NS_ConvertUTF16toUTF8(aText).get(), aText.Length(), aCursorPos));
3114 return NS_OK;
3117 nsresult IMContextWrapper::DeleteText(GtkIMContext* aContext, int32_t aOffset,
3118 uint32_t aNChars) {
3119 MOZ_LOG(gIMELog, LogLevel::Info,
3120 ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), "
3121 "mCompositionState=%s",
3122 this, aContext, aOffset, aNChars, GetCompositionStateName()));
3124 if (!mLastFocusedWindow) {
3125 MOZ_LOG(gIMELog, LogLevel::Error,
3126 ("0x%p DeleteText(), FAILED, there are no focused window "
3127 "in this module",
3128 this));
3129 return NS_ERROR_NULL_POINTER;
3132 if (!aNChars) {
3133 MOZ_LOG(gIMELog, LogLevel::Error,
3134 ("0x%p DeleteText(), FAILED, aNChars must not be zero", this));
3135 return NS_ERROR_INVALID_ARG;
3138 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
3139 nsEventStatus status;
3141 // First, we should cancel current composition because editor cannot
3142 // handle changing selection and deleting text.
3143 uint32_t selOffset;
3144 bool wasComposing = IsComposing();
3145 bool editorHadCompositionString = EditorHasCompositionString();
3146 if (wasComposing) {
3147 selOffset = mCompositionStart;
3148 if (!DispatchCompositionCommitEvent(aContext,
3149 &mSelectedStringRemovedByComposition)) {
3150 MOZ_LOG(gIMELog, LogLevel::Error,
3151 ("0x%p DeleteText(), FAILED, quitting from DeletText", this));
3152 return NS_ERROR_FAILURE;
3154 } else {
3155 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
3156 MOZ_LOG(gIMELog, LogLevel::Error,
3157 ("0x%p DeleteText(), FAILED, due to no valid selection "
3158 "information",
3159 this));
3160 return NS_ERROR_FAILURE;
3162 if (!mContentSelection->HasRange()) {
3163 MOZ_LOG(gIMELog, LogLevel::Debug,
3164 ("0x%p DeleteText(), does nothing, due to no selection range",
3165 this));
3166 return NS_OK;
3168 selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
3171 // Get all text contents of the focused editor
3172 WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
3173 mLastFocusedWindow);
3174 queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
3175 mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
3176 if (NS_WARN_IF(queryTextContentEvent.Failed())) {
3177 return NS_ERROR_FAILURE;
3179 if (queryTextContentEvent.mReply->IsDataEmpty()) {
3180 MOZ_LOG(gIMELog, LogLevel::Error,
3181 ("0x%p DeleteText(), FAILED, there is no contents", this));
3182 return NS_ERROR_FAILURE;
3185 NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(
3186 queryTextContentEvent.mReply->DataRef(), 0, selOffset));
3187 glong offsetInUTF8Characters =
3188 g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset;
3189 if (offsetInUTF8Characters < 0) {
3190 MOZ_LOG(gIMELog, LogLevel::Error,
3191 ("0x%p DeleteText(), FAILED, aOffset is too small for "
3192 "current cursor pos (computed offset: %ld)",
3193 this, offsetInUTF8Characters));
3194 return NS_ERROR_FAILURE;
3197 AppendUTF16toUTF8(
3198 nsDependentSubstring(queryTextContentEvent.mReply->DataRef(), selOffset),
3199 utf8Str);
3200 glong countOfCharactersInUTF8 =
3201 g_utf8_strlen(utf8Str.get(), utf8Str.Length());
3202 glong endInUTF8Characters = offsetInUTF8Characters + aNChars;
3203 if (countOfCharactersInUTF8 < endInUTF8Characters) {
3204 MOZ_LOG(gIMELog, LogLevel::Error,
3205 ("0x%p DeleteText(), FAILED, aNChars is too large for "
3206 "current contents (content length: %ld, computed end offset: %ld)",
3207 this, countOfCharactersInUTF8, endInUTF8Characters));
3208 return NS_ERROR_FAILURE;
3211 gchar* charAtOffset =
3212 g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters);
3213 gchar* charAtEnd =
3214 g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters);
3216 // Set selection to delete
3217 WidgetSelectionEvent selectionEvent(true, eSetSelection, mLastFocusedWindow);
3219 nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0,
3220 charAtOffset - utf8Str.get());
3221 selectionEvent.mOffset = NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length();
3223 nsDependentCSubstring utf8DeletingStr(utf8Str, utf8StrBeforeOffset.Length(),
3224 charAtEnd - charAtOffset);
3225 selectionEvent.mLength = NS_ConvertUTF8toUTF16(utf8DeletingStr).Length();
3227 selectionEvent.mReversed = false;
3228 selectionEvent.mExpandToClusterBoundary = false;
3229 lastFocusedWindow->DispatchEvent(&selectionEvent, status);
3231 if (!selectionEvent.mSucceeded || lastFocusedWindow != mLastFocusedWindow ||
3232 lastFocusedWindow->Destroyed()) {
3233 MOZ_LOG(gIMELog, LogLevel::Error,
3234 ("0x%p DeleteText(), FAILED, setting selection caused "
3235 "focus change or window destroyed",
3236 this));
3237 return NS_ERROR_FAILURE;
3240 // If this deleting text caused by a key press, we need to dispatch
3241 // eKeyDown or eKeyUp before dispatching eContentCommandDelete event.
3242 if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandDelete)) {
3243 MOZ_LOG(gIMELog, LogLevel::Warning,
3244 ("0x%p DeleteText(), Warning, "
3245 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
3246 this));
3247 return NS_ERROR_FAILURE;
3250 // Delete the selection
3251 WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete,
3252 mLastFocusedWindow);
3253 mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
3255 if (!contentCommandEvent.mSucceeded ||
3256 lastFocusedWindow != mLastFocusedWindow ||
3257 lastFocusedWindow->Destroyed()) {
3258 MOZ_LOG(gIMELog, LogLevel::Error,
3259 ("0x%p DeleteText(), FAILED, deleting the selection caused "
3260 "focus change or window destroyed",
3261 this));
3262 return NS_ERROR_FAILURE;
3265 if (!wasComposing) {
3266 return NS_OK;
3269 // Restore the composition at new caret position.
3270 if (!DispatchCompositionStart(aContext)) {
3271 MOZ_LOG(
3272 gIMELog, LogLevel::Error,
3273 ("0x%p DeleteText(), FAILED, resterting composition start", this));
3274 return NS_ERROR_FAILURE;
3277 if (!editorHadCompositionString) {
3278 return NS_OK;
3281 nsAutoString compositionString;
3282 GetCompositionString(aContext, compositionString);
3283 if (!DispatchCompositionChangeEvent(aContext, compositionString)) {
3284 MOZ_LOG(
3285 gIMELog, LogLevel::Error,
3286 ("0x%p DeleteText(), FAILED, restoring composition string", this));
3287 return NS_ERROR_FAILURE;
3290 return NS_OK;
3293 bool IMContextWrapper::EnsureToCacheContentSelection(
3294 nsAString* aSelectedString) {
3295 if (aSelectedString) {
3296 aSelectedString->Truncate();
3299 if (mContentSelection.isSome()) {
3300 if (mContentSelection->HasRange() && aSelectedString) {
3301 aSelectedString->Assign(mContentSelection->OffsetAndDataRef().DataRef());
3303 return true;
3306 RefPtr<nsWindow> dispatcherWindow =
3307 mLastFocusedWindow ? mLastFocusedWindow : mOwnerWindow;
3308 if (NS_WARN_IF(!dispatcherWindow)) {
3309 MOZ_LOG(gIMELog, LogLevel::Error,
3310 ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
3311 "no focused window",
3312 this));
3313 return false;
3316 nsEventStatus status;
3317 WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
3318 dispatcherWindow);
3319 dispatcherWindow->DispatchEvent(&querySelectedTextEvent, status);
3320 if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
3321 MOZ_LOG(gIMELog, LogLevel::Error,
3322 ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
3323 "failure of query selection event",
3324 this));
3325 return false;
3328 mContentSelection = Some(ContentSelection(querySelectedTextEvent));
3329 if (mContentSelection->HasRange()) {
3330 if (!mContentSelection->OffsetAndDataRef().IsDataEmpty() &&
3331 aSelectedString) {
3332 aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef());
3336 MOZ_LOG(
3337 gIMELog, LogLevel::Debug,
3338 ("0x%p EnsureToCacheContentSelection(), Succeeded, mContentSelection=%s",
3339 this, ToString(mContentSelection).c_str()));
3340 return true;
3343 } // namespace widget
3344 } // namespace mozilla