Bug 1931425 - Limit how often moz-label's #setStyles runs r=reusable-components-revie...
[gecko.git] / widget / gtk / IMContextWrapper.cpp
blob1056c22d3f74b33c3b44baadbd72a2af2a5803c6
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=4 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/Logging.h"
8 #include "nsString.h"
9 #include "prtime.h"
10 #include "prenv.h"
12 #include "IMContextWrapper.h"
14 #include "GRefPtr.h"
15 #include "nsGtkKeyUtils.h"
16 #include "nsWindow.h"
17 #include "mozilla/AutoRestore.h"
18 #include "mozilla/Likely.h"
19 #include "mozilla/LookAndFeel.h"
20 #include "mozilla/MiscEvents.h"
21 #include "mozilla/Preferences.h"
22 #include "mozilla/StaticPrefs_intl.h"
23 #include "mozilla/glean/WidgetGtkMetrics.h"
24 #include "mozilla/TextEventDispatcher.h"
25 #include "mozilla/TextEvents.h"
26 #include "mozilla/ToString.h"
27 #include "mozilla/WritingModes.h"
29 // For collecting other people's log, tell `MOZ_LOG=IMEHandler:4,sync`
30 // rather than `MOZ_LOG=IMEHandler:5,sync` since using `5` may create too
31 // big file.
32 // Therefore you shouldn't use `LogLevel::Verbose` for logging usual behavior.
33 mozilla::LazyLogModule gIMELog("IMEHandler");
35 namespace mozilla {
36 namespace widget {
38 static inline const char* ToChar(bool aBool) {
39 return aBool ? "true" : "false";
42 static const char* GetEventType(GdkEventKey* aKeyEvent) {
43 switch (aKeyEvent->type) {
44 case GDK_KEY_PRESS:
45 return "GDK_KEY_PRESS";
46 case GDK_KEY_RELEASE:
47 return "GDK_KEY_RELEASE";
48 default:
49 return "Unknown";
53 class GetEventStateName : public nsAutoCString {
54 public:
55 explicit GetEventStateName(guint aState,
56 IMContextWrapper::IMContextID aIMContextID =
57 IMContextWrapper::IMContextID::Unknown) {
58 if (aState & GDK_SHIFT_MASK) {
59 AppendModifier("shift");
61 if (aState & GDK_CONTROL_MASK) {
62 AppendModifier("control");
64 if (aState & GDK_MOD1_MASK) {
65 AppendModifier("mod1");
67 if (aState & GDK_MOD2_MASK) {
68 AppendModifier("mod2");
70 if (aState & GDK_MOD3_MASK) {
71 AppendModifier("mod3");
73 if (aState & GDK_MOD4_MASK) {
74 AppendModifier("mod4");
76 if (aState & GDK_MOD4_MASK) {
77 AppendModifier("mod5");
79 if (aState & GDK_MOD4_MASK) {
80 AppendModifier("mod5");
82 switch (aIMContextID) {
83 case IMContextWrapper::IMContextID::IBus:
84 static const guint IBUS_HANDLED_MASK = 1 << 24;
85 static const guint IBUS_IGNORED_MASK = 1 << 25;
86 if (aState & IBUS_HANDLED_MASK) {
87 AppendModifier("IBUS_HANDLED_MASK");
89 if (aState & IBUS_IGNORED_MASK) {
90 AppendModifier("IBUS_IGNORED_MASK");
92 break;
93 case IMContextWrapper::IMContextID::Fcitx:
94 case IMContextWrapper::IMContextID::Fcitx5:
95 static const guint FcitxKeyState_HandledMask = 1 << 24;
96 static const guint FcitxKeyState_IgnoredMask = 1 << 25;
97 if (aState & FcitxKeyState_HandledMask) {
98 AppendModifier("FcitxKeyState_HandledMask");
100 if (aState & FcitxKeyState_IgnoredMask) {
101 AppendModifier("FcitxKeyState_IgnoredMask");
103 break;
104 default:
105 break;
109 private:
110 void AppendModifier(const char* aModifierName) {
111 if (!IsEmpty()) {
112 AppendLiteral(" + ");
114 Append(aModifierName);
118 class GetTextRangeStyleText final : public nsAutoCString {
119 public:
120 explicit GetTextRangeStyleText(const TextRangeStyle& aStyle) {
121 if (!aStyle.IsDefined()) {
122 AssignLiteral("{ IsDefined()=false }");
123 return;
126 if (aStyle.IsLineStyleDefined()) {
127 AppendLiteral("{ mLineStyle=");
128 AppendLineStyle(aStyle.mLineStyle);
129 if (aStyle.IsUnderlineColorDefined()) {
130 AppendLiteral(", mUnderlineColor=");
131 AppendColor(aStyle.mUnderlineColor);
132 } else {
133 AppendLiteral(", IsUnderlineColorDefined=false");
135 } else {
136 AppendLiteral("{ IsLineStyleDefined()=false");
139 if (aStyle.IsForegroundColorDefined()) {
140 AppendLiteral(", mForegroundColor=");
141 AppendColor(aStyle.mForegroundColor);
142 } else {
143 AppendLiteral(", IsForegroundColorDefined()=false");
146 if (aStyle.IsBackgroundColorDefined()) {
147 AppendLiteral(", mBackgroundColor=");
148 AppendColor(aStyle.mBackgroundColor);
149 } else {
150 AppendLiteral(", IsBackgroundColorDefined()=false");
153 AppendLiteral(" }");
155 void AppendLineStyle(TextRangeStyle::LineStyle aLineStyle) {
156 switch (aLineStyle) {
157 case TextRangeStyle::LineStyle::None:
158 AppendLiteral("LineStyle::None");
159 break;
160 case TextRangeStyle::LineStyle::Solid:
161 AppendLiteral("LineStyle::Solid");
162 break;
163 case TextRangeStyle::LineStyle::Dotted:
164 AppendLiteral("LineStyle::Dotted");
165 break;
166 case TextRangeStyle::LineStyle::Dashed:
167 AppendLiteral("LineStyle::Dashed");
168 break;
169 case TextRangeStyle::LineStyle::Double:
170 AppendLiteral("LineStyle::Double");
171 break;
172 case TextRangeStyle::LineStyle::Wavy:
173 AppendLiteral("LineStyle::Wavy");
174 break;
175 default:
176 AppendPrintf("Invalid(0x%02X)",
177 static_cast<TextRangeStyle::LineStyleType>(aLineStyle));
178 break;
181 void AppendColor(nscolor aColor) {
182 AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }", NS_GET_R(aColor),
183 NS_GET_G(aColor), NS_GET_B(aColor), NS_GET_A(aColor));
185 virtual ~GetTextRangeStyleText() = default;
188 const static bool kUseSimpleContextDefault = false;
190 /******************************************************************************
191 * SelectionStyleProvider
193 * IME (e.g., fcitx, ~4.2.8.3) may look up selection colors of widget, which
194 * is related to the window associated with the IM context, to support any
195 * colored widgets. Our editor (like <input type="text">) is rendered as
196 * native GtkTextView as far as possible by default and if editor color is
197 * changed by web apps, nsTextFrame may swap background color of foreground
198 * color of composition string for making composition string is always
199 * visually distinct in normal text.
201 * So, we would like IME to set style of composition string to good colors
202 * in GtkTextView. Therefore, this class overwrites selection colors of
203 * our widget with selection colors of GtkTextView so that it's possible IME
204 * to refer selection colors of GtkTextView via our widget.
205 ******************************************************************************/
207 static Maybe<nscolor> GetSystemColor(LookAndFeel::ColorID aId) {
208 return LookAndFeel::GetColor(aId, LookAndFeel::ColorScheme::Light,
209 LookAndFeel::UseStandins::No);
212 class SelectionStyleProvider final {
213 public:
214 static SelectionStyleProvider* GetExistingInstance() { return sInstance; }
216 static SelectionStyleProvider* GetInstance() {
217 if (sHasShutDown) {
218 return nullptr;
220 if (!sInstance) {
221 sInstance = new SelectionStyleProvider();
223 return sInstance;
226 static void Shutdown() {
227 if (sInstance) {
228 g_object_unref(sInstance->mProvider);
230 delete sInstance;
231 sInstance = nullptr;
232 sHasShutDown = true;
235 // aGDKWindow is a GTK window which will be associated with an IM context.
236 void AttachTo(GdkWindow* aGDKWindow) {
237 GtkWidget* widget = nullptr;
238 // gdk_window_get_user_data() typically returns pointer to widget that
239 // window belongs to. If it's widget, fcitx retrieves selection colors
240 // of them. So, we need to overwrite its style.
241 gdk_window_get_user_data(aGDKWindow, (gpointer*)&widget);
242 if (GTK_IS_WIDGET(widget)) {
243 gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
244 GTK_STYLE_PROVIDER(mProvider),
245 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
249 void OnThemeChanged() {
250 // fcitx refers GtkStyle::text[GTK_STATE_SELECTED] and
251 // GtkStyle::bg[GTK_STATE_SELECTED] (although pair of text and *base*
252 // or *fg* and bg is correct). gtk_style_update_from_context() will
253 // set these colors using the widget's GtkStyleContext and so the
254 // colors can be controlled by a ":selected" CSS rule.
255 nsAutoCString style(":selected{");
256 // FYI: LookAndFeel always returns selection colors of GtkTextView.
257 if (auto selectionForegroundColor =
258 GetSystemColor(LookAndFeel::ColorID::Highlight)) {
259 double alpha =
260 static_cast<double>(NS_GET_A(*selectionForegroundColor)) / 0xFF;
261 style.AppendPrintf("color:rgba(%u,%u,%u,",
262 NS_GET_R(*selectionForegroundColor),
263 NS_GET_G(*selectionForegroundColor),
264 NS_GET_B(*selectionForegroundColor));
265 // We can't use AppendPrintf here, because it does locale-specific
266 // formatting of floating-point values.
267 style.AppendFloat(alpha);
268 style.AppendPrintf(");");
270 if (auto selectionBackgroundColor =
271 GetSystemColor(LookAndFeel::ColorID::Highlighttext)) {
272 double alpha =
273 static_cast<double>(NS_GET_A(*selectionBackgroundColor)) / 0xFF;
274 style.AppendPrintf("background-color:rgba(%u,%u,%u,",
275 NS_GET_R(*selectionBackgroundColor),
276 NS_GET_G(*selectionBackgroundColor),
277 NS_GET_B(*selectionBackgroundColor));
278 style.AppendFloat(alpha);
279 style.AppendPrintf(");");
281 style.AppendLiteral("}");
282 gtk_css_provider_load_from_data(mProvider, style.get(), -1, nullptr);
285 private:
286 static SelectionStyleProvider* sInstance;
287 static bool sHasShutDown;
288 GtkCssProvider* const mProvider;
290 SelectionStyleProvider() : mProvider(gtk_css_provider_new()) {
291 OnThemeChanged();
295 SelectionStyleProvider* SelectionStyleProvider::sInstance = nullptr;
296 bool SelectionStyleProvider::sHasShutDown = false;
298 /******************************************************************************
299 * IMContextWrapper
300 ******************************************************************************/
302 IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr;
303 guint16 IMContextWrapper::sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
304 bool IMContextWrapper::sUseSimpleContext;
306 NS_IMPL_ISUPPORTS(IMContextWrapper, TextEventDispatcherListener,
307 nsISupportsWeakReference)
309 IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow)
310 : mOwnerWindow(aOwnerWindow),
311 mLastFocusedWindow(nullptr),
312 mContext(nullptr),
313 mSimpleContext(nullptr),
314 mDummyContext(nullptr),
315 mComposingContext(nullptr),
316 mCompositionStart(UINT32_MAX),
317 mProcessingKeyEvent(nullptr),
318 mCompositionState(eCompositionState_NotComposing),
319 mIMContextID(IMContextID::Unknown),
320 mFallbackToKeyEvent(false),
321 mKeyboardEventWasDispatched(false),
322 mKeyboardEventWasConsumed(false),
323 mIsDeletingSurrounding(false),
324 mLayoutChanged(false),
325 mSetCursorPositionOnKeyEvent(true),
326 mPendingResettingIMContext(false),
327 mRetrieveSurroundingSignalReceived(false),
328 mMaybeInDeadKeySequence(false),
329 mIsIMInAsyncKeyHandlingMode(false),
330 mSetInputPurposeAndInputHints(false) {
331 static bool sFirstInstance = true;
332 if (sFirstInstance) {
333 sFirstInstance = false;
334 sUseSimpleContext =
335 Preferences::GetBool("intl.ime.use_simple_context_on_password_field",
336 kUseSimpleContextDefault);
338 Init();
341 static bool IsIBusInSyncMode() {
342 // See ibus_im_context_class_init() in client/gtk2/ibusimcontext.c
343 // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L610
344 const char* env = PR_GetEnv("IBUS_ENABLE_SYNC_MODE");
346 // See _get_boolean_env() in client/gtk2/ibusimcontext.c
347 // https://github.com/ibus/ibus/blob/86963f2f94d1e4fc213b01c2bc2ba9dcf4b22219/client/gtk2/ibusimcontext.c#L520-L537
348 if (!env) {
349 return false;
351 nsDependentCString envStr(env);
352 if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
353 envStr.EqualsLiteral("false") || envStr.EqualsLiteral("False") ||
354 envStr.EqualsLiteral("FALSE")) {
355 return false;
357 return true;
360 static bool GetFcitxBoolEnv(const char* aEnv) {
361 // See fcitx_utils_get_boolean_env in src/lib/fcitx-utils/utils.c
362 // https://github.com/fcitx/fcitx/blob/0c87840dc7d9460c2cb5feaeefec299d0d3d62ec/src/lib/fcitx-utils/utils.c#L721-L736
363 const char* env = PR_GetEnv(aEnv);
364 if (!env) {
365 return false;
367 nsDependentCString envStr(env);
368 if (envStr.IsEmpty() || envStr.EqualsLiteral("0") ||
369 envStr.EqualsLiteral("false")) {
370 return false;
372 return true;
375 static bool IsFcitxInSyncMode() {
376 // See fcitx_im_context_class_init() in src/frontend/gtk2/fcitximcontext.c
377 // https://github.com/fcitx/fcitx/blob/78b98d9230dc9630e99d52e3172bdf440ffd08c4/src/frontend/gtk2/fcitximcontext.c#L395-L398
378 return GetFcitxBoolEnv("IBUS_ENABLE_SYNC_MODE") ||
379 GetFcitxBoolEnv("FCITX_ENABLE_SYNC_MODE");
382 nsDependentCSubstring IMContextWrapper::GetIMName() const {
383 const char* contextIDChar =
384 gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext));
385 if (!contextIDChar) {
386 return nsDependentCSubstring();
389 nsDependentCSubstring im(contextIDChar, strlen(contextIDChar));
391 // If the context is XIM, actual engine must be specified with
392 // |XMODIFIERS=@im=foo|.
393 const char* xmodifiersChar = PR_GetEnv("XMODIFIERS");
394 if (!xmodifiersChar || !im.EqualsLiteral("xim")) {
395 return im;
398 nsDependentCString xmodifiers(xmodifiersChar);
399 int32_t atIMValueStart = xmodifiers.Find("@im=") + 4;
400 if (atIMValueStart < 4 ||
401 xmodifiers.Length() <= static_cast<size_t>(atIMValueStart)) {
402 return im;
405 int32_t atIMValueEnd = xmodifiers.Find("@", atIMValueStart);
406 if (atIMValueEnd > atIMValueStart) {
407 return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
408 atIMValueEnd - atIMValueStart);
411 if (atIMValueEnd == kNotFound) {
412 return nsDependentCSubstring(xmodifiersChar + atIMValueStart,
413 strlen(xmodifiersChar) - atIMValueStart);
416 return im;
419 void IMContextWrapper::Init() {
420 MozContainer* container = mOwnerWindow->GetMozContainer();
421 MOZ_ASSERT(container, "container is null");
422 GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container));
424 // Overwrite selection colors of the window before associating the window
425 // with IM context since IME may look up selection colors via IM context
426 // to support any colored widgets.
427 SelectionStyleProvider::GetInstance()->AttachTo(gdkWindow);
429 // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
430 // So, we don't need to check the result.
432 // Normal context.
433 mContext = gtk_im_multicontext_new();
434 gtk_im_context_set_client_window(mContext, gdkWindow);
435 g_signal_connect(mContext, "preedit_changed",
436 G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback),
437 this);
438 g_signal_connect(mContext, "retrieve_surrounding",
439 G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback),
440 this);
441 g_signal_connect(mContext, "delete_surrounding",
442 G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback),
443 this);
444 g_signal_connect(mContext, "commit",
445 G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback),
446 this);
447 g_signal_connect(mContext, "preedit_start",
448 G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
449 this);
450 g_signal_connect(mContext, "preedit_end",
451 G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
452 this);
453 nsDependentCSubstring im = GetIMName();
454 if (im.EqualsLiteral("ibus")) {
455 mIMContextID = IMContextID::IBus;
456 mIsIMInAsyncKeyHandlingMode = !IsIBusInSyncMode();
457 // Although ibus has key snooper mode, it's forcibly disabled on Firefox
458 // in default settings by its whitelist since we always send key events
459 // to IME before handling shortcut keys. The whitelist can be
460 // customized with env, IBUS_NO_SNOOPER_APPS, but we don't need to
461 // support such rare cases for reducing maintenance cost.
462 mIsKeySnooped = false;
463 } else if (im.EqualsLiteral("fcitx")) {
464 mIMContextID = IMContextID::Fcitx;
465 mIsIMInAsyncKeyHandlingMode = !IsFcitxInSyncMode();
466 // Although Fcitx has key snooper mode similar to ibus, it's also
467 // disabled on Firefox in default settings by its whitelist. The
468 // whitelist can be customized with env, IBUS_NO_SNOOPER_APPS or
469 // FCITX_NO_SNOOPER_APPS, but we don't need to support such rare cases
470 // for reducing maintenance cost.
471 mIsKeySnooped = false;
472 } else if (im.EqualsLiteral("fcitx5")) {
473 mIMContextID = IMContextID::Fcitx5;
474 mIsIMInAsyncKeyHandlingMode = true; // does not have sync mode.
475 mIsKeySnooped = false; // never use key snooper.
476 } else if (im.EqualsLiteral("uim")) {
477 mIMContextID = IMContextID::Uim;
478 mIsIMInAsyncKeyHandlingMode = false;
479 // We cannot know if uim uses key snooper since it's build option of
480 // uim. Therefore, we need to retrieve the consideration from the
481 // pref for making users and distributions allowed to choose their
482 // preferred value.
483 mIsKeySnooped =
484 Preferences::GetBool("intl.ime.hack.uim.using_key_snooper", true);
485 } else if (im.EqualsLiteral("scim")) {
486 mIMContextID = IMContextID::Scim;
487 mIsIMInAsyncKeyHandlingMode = false;
488 mIsKeySnooped = false;
489 } else if (im.EqualsLiteral("iiim")) {
490 mIMContextID = IMContextID::IIIMF;
491 mIsIMInAsyncKeyHandlingMode = false;
492 mIsKeySnooped = false;
493 } else if (im.EqualsLiteral("wayland")) {
494 mIMContextID = IMContextID::Wayland;
495 mIsIMInAsyncKeyHandlingMode = false;
496 mIsKeySnooped = true;
497 } else {
498 mIMContextID = IMContextID::Unknown;
499 mIsIMInAsyncKeyHandlingMode = false;
500 mIsKeySnooped = false;
503 // Simple context
504 if (sUseSimpleContext) {
505 mSimpleContext = gtk_im_context_simple_new();
506 gtk_im_context_set_client_window(mSimpleContext, gdkWindow);
507 g_signal_connect(mSimpleContext, "preedit_changed",
508 G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback),
509 this);
510 g_signal_connect(
511 mSimpleContext, "retrieve_surrounding",
512 G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback), this);
513 g_signal_connect(mSimpleContext, "delete_surrounding",
514 G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback),
515 this);
516 g_signal_connect(mSimpleContext, "commit",
517 G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback),
518 this);
519 g_signal_connect(mSimpleContext, "preedit_start",
520 G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
521 this);
522 g_signal_connect(mSimpleContext, "preedit_end",
523 G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
524 this);
527 // Dummy context
528 mDummyContext = gtk_im_multicontext_new();
529 gtk_im_context_set_client_window(mDummyContext, gdkWindow);
531 MOZ_LOG(gIMELog, LogLevel::Info,
532 ("0x%p Init(), mOwnerWindow=%p, mContext=%p (im=\"%s\"), "
533 "mIsIMInAsyncKeyHandlingMode=%s, mIsKeySnooped=%s, "
534 "mSimpleContext=%p, mDummyContext=%p, "
535 "gtk_im_multicontext_get_context_id()=\"%s\", "
536 "PR_GetEnv(\"XMODIFIERS\")=\"%s\"",
537 this, mOwnerWindow, mContext, nsAutoCString(im).get(),
538 ToChar(mIsIMInAsyncKeyHandlingMode), ToChar(mIsKeySnooped),
539 mSimpleContext, mDummyContext,
540 gtk_im_multicontext_get_context_id(GTK_IM_MULTICONTEXT(mContext)),
541 PR_GetEnv("XMODIFIERS")));
544 /* static */
545 void IMContextWrapper::Shutdown() { SelectionStyleProvider::Shutdown(); }
547 IMContextWrapper::~IMContextWrapper() {
548 MOZ_ASSERT(!mContext);
549 MOZ_ASSERT(!mComposingContext);
550 if (this == sLastFocusedContext) {
551 sLastFocusedContext = nullptr;
553 MOZ_LOG(gIMELog, LogLevel::Info, ("0x%p ~IMContextWrapper()", this));
556 NS_IMETHODIMP
557 IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
558 const IMENotification& aNotification) {
559 switch (aNotification.mMessage) {
560 case REQUEST_TO_COMMIT_COMPOSITION:
561 case REQUEST_TO_CANCEL_COMPOSITION: {
562 nsWindow* window =
563 static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
564 return IsComposing() ? EndIMEComposition(window) : NS_OK;
566 case NOTIFY_IME_OF_FOCUS:
567 OnFocusChangeInGecko(true);
568 return NS_OK;
569 case NOTIFY_IME_OF_BLUR:
570 OnFocusChangeInGecko(false);
571 return NS_OK;
572 case NOTIFY_IME_OF_POSITION_CHANGE:
573 OnLayoutChange();
574 return NS_OK;
575 case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
576 OnUpdateComposition();
577 return NS_OK;
578 case NOTIFY_IME_OF_SELECTION_CHANGE: {
579 nsWindow* window =
580 static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
581 OnSelectionChange(window, aNotification);
582 return NS_OK;
584 default:
585 return NS_ERROR_NOT_IMPLEMENTED;
589 NS_IMETHODIMP_(void)
590 IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) {
591 // XXX When input transaction is being stolen by add-on, what should we do?
594 NS_IMETHODIMP_(void)
595 IMContextWrapper::WillDispatchKeyboardEvent(
596 TextEventDispatcher* aTextEventDispatcher,
597 WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
598 void* aData) {
599 KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent,
600 static_cast<GdkEventKey*>(aData));
603 TextEventDispatcher* IMContextWrapper::GetTextEventDispatcher() {
604 if (NS_WARN_IF(!mLastFocusedWindow)) {
605 return nullptr;
607 TextEventDispatcher* dispatcher =
608 mLastFocusedWindow->GetTextEventDispatcher();
609 // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr.
610 MOZ_RELEASE_ASSERT(dispatcher);
611 return dispatcher;
614 NS_IMETHODIMP_(IMENotificationRequests)
615 IMContextWrapper::GetIMENotificationRequests() {
616 IMENotificationRequests::Notifications notifications =
617 IMENotificationRequests::NOTIFY_NOTHING;
618 // If it's not enabled, we don't need position change notification.
619 if (IsEnabled()) {
620 notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE;
622 return IMENotificationRequests(notifications);
625 void IMContextWrapper::OnDestroyWindow(nsWindow* aWindow) {
626 MOZ_LOG(
627 gIMELog, LogLevel::Info,
628 ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
629 "mOwnerWindow=0x%p, mLastFocusedModule=0x%p",
630 this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext));
632 MOZ_ASSERT(aWindow, "aWindow must not be null");
634 if (mLastFocusedWindow == aWindow) {
635 if (IsComposing()) {
636 EndIMEComposition(aWindow);
638 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
639 mLastFocusedWindow = nullptr;
642 if (mOwnerWindow != aWindow) {
643 return;
646 if (sLastFocusedContext == this) {
647 sLastFocusedContext = nullptr;
651 * NOTE:
652 * The given window is the owner of this, so, we must disconnect from the
653 * contexts now. But that might be referred from other nsWindows
654 * (they are children of this. But we don't know why there are the
655 * cases). So, we need to clear the pointers that refers to contexts
656 * and this if the other referrers are still alive. See bug 349727.
658 if (mContext) {
659 PrepareToDestroyContext(mContext);
660 gtk_im_context_set_client_window(mContext, nullptr);
661 g_signal_handlers_disconnect_by_data(mContext, this);
662 g_object_unref(mContext);
663 mContext = nullptr;
666 if (mSimpleContext) {
667 gtk_im_context_set_client_window(mSimpleContext, nullptr);
668 g_signal_handlers_disconnect_by_data(mSimpleContext, this);
669 g_object_unref(mSimpleContext);
670 mSimpleContext = nullptr;
673 if (mDummyContext) {
674 // mContext and mDummyContext have the same slaveType and signal_data
675 // so no need for another workaround_gtk_im_display_closed.
676 gtk_im_context_set_client_window(mDummyContext, nullptr);
677 g_object_unref(mDummyContext);
678 mDummyContext = nullptr;
681 if (NS_WARN_IF(mComposingContext)) {
682 g_object_unref(mComposingContext);
683 mComposingContext = nullptr;
686 mOwnerWindow = nullptr;
687 mLastFocusedWindow = nullptr;
688 mInputContext.mIMEState.mEnabled = IMEEnabled::Disabled;
689 mPostingKeyEvents.Clear();
691 MOZ_LOG(gIMELog, LogLevel::Debug,
692 ("0x%p OnDestroyWindow(), succeeded, Completely destroyed", this));
695 void IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext) {
696 if (mIMContextID == IMContextID::IIIMF) {
697 // IIIM module registers handlers for the "closed" signal on the
698 // display, but the signal handler is not disconnected when the module
699 // is unloaded. To prevent the module from being unloaded, use static
700 // variable to hold reference of slave context class declared by IIIM.
701 // Note that this does not grab any instance, it grabs the "class".
702 static gpointer sGtkIIIMContextClass = nullptr;
703 if (!sGtkIIIMContextClass) {
704 // We retrieved slave context class with g_type_name() and actual
705 // slave context instance when our widget was GTK2. That must be
706 // _GtkIMContext::priv::slave in GTK3. However, _GtkIMContext::priv
707 // is an opacity struct named _GtkIMMulticontextPrivate, i.e., it's
708 // not exposed by GTK3. Therefore, we cannot access the instance
709 // safely. So, we need to retrieve the slave context class with
710 // g_type_from_name("GtkIMContextIIIM") directly (anyway, we needed
711 // to compare the class name with "GtkIMContextIIIM").
712 GType IIMContextType = g_type_from_name("GtkIMContextIIIM");
713 if (IIMContextType) {
714 sGtkIIIMContextClass = g_type_class_ref(IIMContextType);
715 MOZ_LOG(gIMELog, LogLevel::Info,
716 ("0x%p PrepareToDestroyContext(), added to reference to "
717 "GtkIMContextIIIM class to prevent it from being unloaded",
718 this));
719 } else {
720 MOZ_LOG(gIMELog, LogLevel::Error,
721 ("0x%p PrepareToDestroyContext(), FAILED to prevent the "
722 "IIIM module from being uploaded",
723 this));
729 void IMContextWrapper::OnFocusWindow(nsWindow* aWindow) {
730 if (MOZ_UNLIKELY(IsDestroyed())) {
731 return;
734 MOZ_LOG(gIMELog, LogLevel::Info,
735 ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p", this,
736 aWindow, mLastFocusedWindow));
737 mLastFocusedWindow = aWindow;
740 void IMContextWrapper::OnBlurWindow(nsWindow* aWindow) {
741 if (MOZ_UNLIKELY(IsDestroyed())) {
742 return;
745 MOZ_LOG(
746 gIMELog, LogLevel::Info,
747 ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
748 "mIMEFocusState=%s",
749 this, aWindow, mLastFocusedWindow, ToString(mIMEFocusState).c_str()));
751 if (mLastFocusedWindow != aWindow) {
752 return;
755 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
758 KeyHandlingState IMContextWrapper::OnKeyEvent(
759 nsWindow* aCaller, GdkEventKey* aEvent,
760 bool aKeyboardEventWasDispatched /* = false */) {
761 MOZ_ASSERT(aEvent, "aEvent must be non-null");
763 if (!mInputContext.mIMEState.IsEditable() || MOZ_UNLIKELY(IsDestroyed())) {
764 return KeyHandlingState::eNotHandled;
767 MOZ_LOG(gIMELog, LogLevel::Info, (">>>>>>>>>>>>>>>>"));
768 MOZ_LOG(
769 gIMELog, LogLevel::Info,
770 ("0x%p OnKeyEvent(aCaller=0x%p, "
771 "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X, state=%s, "
772 "time=%u, hardware_keycode=%u, group=%u }, "
773 "aKeyboardEventWasDispatched=%s)",
774 this, aCaller, aEvent, GetEventType(aEvent),
775 gdk_keyval_name(aEvent->keyval), gdk_keyval_to_unicode(aEvent->keyval),
776 GetEventStateName(aEvent->state, mIMContextID).get(), aEvent->time,
777 aEvent->hardware_keycode, aEvent->group,
778 ToChar(aKeyboardEventWasDispatched)));
779 MOZ_LOG(
780 gIMELog, LogLevel::Info,
781 ("0x%p OnKeyEvent(), mMaybeInDeadKeySequence=%s, "
782 "mCompositionState=%s, current context=%p, active context=%p, "
783 "mIMContextID=%s, mIsIMInAsyncKeyHandlingMode=%s",
784 this, ToChar(mMaybeInDeadKeySequence), GetCompositionStateName(),
785 GetCurrentContext(), GetActiveContext(), ToString(mIMContextID).c_str(),
786 ToChar(mIsIMInAsyncKeyHandlingMode)));
788 if (aCaller != mLastFocusedWindow) {
789 MOZ_LOG(gIMELog, LogLevel::Error,
790 ("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
791 "window, mLastFocusedWindow=0x%p",
792 this, mLastFocusedWindow));
793 return KeyHandlingState::eNotHandled;
796 // Even if old IM context has composition, key event should be sent to
797 // current context since the user expects so.
798 GtkIMContext* currentContext = GetCurrentContext();
799 if (MOZ_UNLIKELY(!currentContext)) {
800 MOZ_LOG(gIMELog, LogLevel::Error,
801 ("0x%p OnKeyEvent(), FAILED, there are no context", this));
802 return KeyHandlingState::eNotHandled;
805 if (mSetCursorPositionOnKeyEvent) {
806 SetCursorPosition(currentContext);
807 mSetCursorPositionOnKeyEvent = false;
810 // Let's support dead key event even if active keyboard layout also
811 // supports complicated composition like CJK IME.
812 bool isDeadKey =
813 KeymapWrapper::ComputeDOMKeyNameIndex(aEvent) == KEY_NAME_INDEX_Dead;
814 mMaybeInDeadKeySequence |= isDeadKey;
816 // If current context is mSimpleContext, both ibus and fcitx handles key
817 // events synchronously. So, only when current context is mContext which
818 // is GtkIMMulticontext, the key event may be handled by IME asynchronously.
819 bool probablyHandledAsynchronously =
820 mIsIMInAsyncKeyHandlingMode && currentContext == mContext;
822 // If we're not sure whether the event is handled asynchronously, this is
823 // set to true.
824 bool maybeHandledAsynchronously = false;
826 // If aEvent is a synthesized event for async handling, this will be set to
827 // true.
828 bool isHandlingAsyncEvent = false;
830 // If we've decided that the event won't be synthesized asyncrhonously
831 // by IME, but actually IME did it, this is set to true.
832 bool isUnexpectedAsyncEvent = false;
834 // If IM is ibus or fcitx and it handles key events asynchronously,
835 // they mark aEvent->state as "handled by me" when they post key event
836 // to another process. Unfortunately, we need to check this hacky
837 // flag because it's difficult to store all pending key events by
838 // an array or a hashtable.
839 if (probablyHandledAsynchronously) {
840 switch (mIMContextID) {
841 case IMContextID::IBus: {
842 // See src/ibustypes.h
843 static const guint IBUS_IGNORED_MASK = 1 << 25;
844 // If IBUS_IGNORED_MASK was set to aEvent->state, the event
845 // has already been handled by another process and it wasn't
846 // used by IME.
847 isHandlingAsyncEvent = !!(aEvent->state & IBUS_IGNORED_MASK);
848 if (!isHandlingAsyncEvent) {
849 // On some environments, IBUS_IGNORED_MASK flag is not set as
850 // expected. In such case, we keep pusing all events into the queue.
851 // I.e., that causes eating a lot of memory until it's blurred.
852 // Therefore, we need to check whether there is same timestamp event
853 // in the queue. This redundant cost should be low because in most
854 // causes, key events in the queue should be 2 or 4.
855 isHandlingAsyncEvent =
856 mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
857 if (isHandlingAsyncEvent) {
858 MOZ_LOG(gIMELog, LogLevel::Info,
859 ("0x%p OnKeyEvent(), aEvent->state does not have "
860 "IBUS_IGNORED_MASK but "
861 "same event in the queue. So, assuming it's a "
862 "synthesized event",
863 this));
867 // If it's a synthesized event, let's remove it from the posting
868 // event queue first. Otherwise the following blocks cannot use
869 // `break`.
870 if (isHandlingAsyncEvent) {
871 MOZ_LOG(gIMELog, LogLevel::Info,
872 ("0x%p OnKeyEvent(), aEvent->state has IBUS_IGNORED_MASK "
873 "or aEvent is in the "
874 "posting event queue, so, it won't be handled "
875 "asynchronously anymore. Removing "
876 "the posted events from the queue",
877 this));
878 probablyHandledAsynchronously = false;
879 mPostingKeyEvents.RemoveEvent(aEvent);
882 // ibus won't send back key press events in a dead key sequcne.
883 if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
884 probablyHandledAsynchronously = false;
885 if (isHandlingAsyncEvent) {
886 isUnexpectedAsyncEvent = true;
887 break;
889 // Some keyboard layouts which have dead keys may send
890 // "empty" key event to make us call
891 // gtk_im_context_filter_keypress() to commit composed
892 // character during a GDK_KEY_PRESS event dispatching.
893 if (!gdk_keyval_to_unicode(aEvent->keyval) &&
894 !aEvent->hardware_keycode) {
895 isUnexpectedAsyncEvent = true;
896 break;
898 break;
900 // ibus may handle key events synchronously if focused editor is
901 // <input type="password"> or |ime-mode: disabled;|. However, in
902 // some environments, not so actually. Therefore, we need to check
903 // the result of gtk_im_context_filter_keypress() later.
904 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
905 probablyHandledAsynchronously = false;
906 maybeHandledAsynchronously = !isHandlingAsyncEvent;
907 break;
909 break;
911 case IMContextID::Fcitx:
912 case IMContextID::Fcitx5: {
913 // See src/lib/fcitx-utils/keysym.h
914 static const guint FcitxKeyState_IgnoredMask = 1 << 25;
915 // If FcitxKeyState_IgnoredMask was set to aEvent->state,
916 // the event has already been handled by another process and
917 // it wasn't used by IME.
918 isHandlingAsyncEvent = !!(aEvent->state & FcitxKeyState_IgnoredMask);
919 if (!isHandlingAsyncEvent) {
920 // On some environments, FcitxKeyState_IgnoredMask flag *might* be not
921 // set as expected. If there were such cases, we'd keep pusing all
922 // events into the queue. I.e., that would cause eating a lot of
923 // memory until it'd be blurred. Therefore, we should check whether
924 // there is same timestamp event in the queue. This redundant cost
925 // should be low because in most causes, key events in the queue
926 // should be 2 or 4.
927 isHandlingAsyncEvent =
928 mPostingKeyEvents.IndexOf(aEvent) != GdkEventKeyQueue::NoIndex();
929 if (isHandlingAsyncEvent) {
930 MOZ_LOG(gIMELog, LogLevel::Info,
931 ("0x%p OnKeyEvent(), aEvent->state does not have "
932 "FcitxKeyState_IgnoredMask "
933 "but same event in the queue. So, assuming it's a "
934 "synthesized event",
935 this));
939 // fcitx won't send back key press events in a dead key sequcne.
940 if (mMaybeInDeadKeySequence && aEvent->type == GDK_KEY_PRESS) {
941 probablyHandledAsynchronously = false;
942 if (isHandlingAsyncEvent) {
943 isUnexpectedAsyncEvent = true;
944 break;
946 // Some keyboard layouts which have dead keys may send
947 // "empty" key event to make us call
948 // gtk_im_context_filter_keypress() to commit composed
949 // character during a GDK_KEY_PRESS event dispatching.
950 if (!gdk_keyval_to_unicode(aEvent->keyval) &&
951 !aEvent->hardware_keycode) {
952 isUnexpectedAsyncEvent = true;
953 break;
957 // fcitx handles key events asynchronously even if focused
958 // editor cannot use IME actually.
960 if (isHandlingAsyncEvent) {
961 MOZ_LOG(gIMELog, LogLevel::Info,
962 ("0x%p OnKeyEvent(), aEvent->state has "
963 "FcitxKeyState_IgnoredMask or aEvent is in "
964 "the posting event queue, so, it won't be handled "
965 "asynchronously anymore. "
966 "Removing the posted events from the queue",
967 this));
968 probablyHandledAsynchronously = false;
969 mPostingKeyEvents.RemoveEvent(aEvent);
970 break;
972 break;
974 default:
975 MOZ_ASSERT_UNREACHABLE(
976 "IME may handle key event "
977 "asyncrhonously, but not yet confirmed if it comes agian "
978 "actually");
982 if (!isUnexpectedAsyncEvent) {
983 mKeyboardEventWasDispatched = aKeyboardEventWasDispatched;
984 mKeyboardEventWasConsumed = false;
985 } else {
986 // If we didn't expect this event, we've alreday dispatched eKeyDown
987 // event or eKeyUp event for that.
988 mKeyboardEventWasDispatched = true;
989 // And in this case, we need to assume that another key event hasn't
990 // been receivied and mKeyboardEventWasConsumed keeps storing the
991 // dispatched eKeyDown or eKeyUp event's state.
993 mFallbackToKeyEvent = false;
994 mProcessingKeyEvent = aEvent;
995 gboolean isFiltered = gtk_im_context_filter_keypress(currentContext, aEvent);
997 // If we're not sure whether the event is handled by IME asynchronously or
998 // synchronously, we need to trust the result of
999 // gtk_im_context_filter_keypress(). If it consumed and but did nothing,
1000 // we can assume that another event will be synthesized.
1001 if (!isHandlingAsyncEvent && maybeHandledAsynchronously) {
1002 probablyHandledAsynchronously |=
1003 isFiltered && !mFallbackToKeyEvent && !mKeyboardEventWasDispatched;
1006 if (aEvent->type == GDK_KEY_PRESS) {
1007 if (isFiltered && probablyHandledAsynchronously) {
1008 sWaitingSynthesizedKeyPressHardwareKeyCode = aEvent->hardware_keycode;
1009 } else {
1010 sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
1014 // The caller of this shouldn't handle aEvent anymore if we've dispatched
1015 // composition events or modified content with other events.
1016 bool filterThisEvent = isFiltered && !mFallbackToKeyEvent;
1018 if (IsComposingOnCurrentContext() && !isFiltered &&
1019 aEvent->type == GDK_KEY_PRESS && mDispatchedCompositionString.IsEmpty()) {
1020 // A Hangul input engine for SCIM doesn't emit preedit_end
1021 // signal even when composition string becomes empty. On the
1022 // other hand, we should allow to make composition with empty
1023 // string for other languages because there *might* be such
1024 // IM. For compromising this issue, we should dispatch
1025 // compositionend event, however, we don't need to reset IM
1026 // actually.
1027 // NOTE: Don't dispatch key events as "processed by IME" since
1028 // we need to dispatch keyboard events as IME wasn't handled it.
1029 mProcessingKeyEvent = nullptr;
1030 DispatchCompositionCommitEvent(currentContext, &EmptyString());
1031 mProcessingKeyEvent = aEvent;
1032 // In this case, even though we handle the keyboard event here,
1033 // but we should dispatch keydown event as
1034 filterThisEvent = false;
1037 if (filterThisEvent && !mKeyboardEventWasDispatched) {
1038 // If IME handled the key event but we've not dispatched eKeyDown nor
1039 // eKeyUp event yet, we need to dispatch here unless the key event is
1040 // now being handled by other IME process.
1041 if (!probablyHandledAsynchronously) {
1042 MaybeDispatchKeyEventAsProcessedByIME(eVoidEvent);
1043 // Be aware, the widget might have been gone here.
1045 // If we need to wait reply from IM, IM may send some signals to us
1046 // without sending the key event again. In such case, we need to
1047 // dispatch keyboard events with a copy of aEvent. Therefore, we
1048 // need to use information of this key event to dispatch an KeyDown
1049 // or eKeyUp event later.
1050 else {
1051 MOZ_LOG(gIMELog, LogLevel::Info,
1052 ("0x%p OnKeyEvent(), putting aEvent into the queue...", this));
1053 mPostingKeyEvents.PutEvent(aEvent);
1057 mProcessingKeyEvent = nullptr;
1059 if (aEvent->type == GDK_KEY_PRESS && !filterThisEvent) {
1060 // If the key event hasn't been handled by active IME nor keyboard
1061 // layout, we can assume that the dead key sequence has been or was
1062 // ended. Note that we should not reset it when the key event is
1063 // GDK_KEY_RELEASE since it may not be filtered by active keyboard
1064 // layout even in composition.
1065 mMaybeInDeadKeySequence = false;
1068 if (aEvent->type == GDK_KEY_RELEASE) {
1069 if (const GdkEventKey* pendingKeyPressEvent =
1070 mPostingKeyEvents.GetCorrespondingKeyPressEvent(aEvent)) {
1071 MOZ_LOG(gIMELog, LogLevel::Warning,
1072 ("0x%p OnKeyEvent(), forgetting a pending GDK_KEY_PRESS event "
1073 "because GDK_KEY_RELEASE for the event is handled",
1074 this));
1075 mPostingKeyEvents.RemoveEvent(pendingKeyPressEvent);
1079 MOZ_LOG(
1080 gIMELog, LogLevel::Debug,
1081 ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s "
1082 "(isFiltered=%s, mFallbackToKeyEvent=%s, "
1083 "probablyHandledAsynchronously=%s, maybeHandledAsynchronously=%s), "
1084 "mPostingKeyEvents.Length()=%zu, mCompositionState=%s, "
1085 "mMaybeInDeadKeySequence=%s, mKeyboardEventWasDispatched=%s, "
1086 "mKeyboardEventWasConsumed=%s",
1087 this, ToChar(filterThisEvent), ToChar(isFiltered),
1088 ToChar(mFallbackToKeyEvent), ToChar(probablyHandledAsynchronously),
1089 ToChar(maybeHandledAsynchronously), mPostingKeyEvents.Length(),
1090 GetCompositionStateName(), ToChar(mMaybeInDeadKeySequence),
1091 ToChar(mKeyboardEventWasDispatched), ToChar(mKeyboardEventWasConsumed)));
1092 MOZ_LOG(gIMELog, LogLevel::Info, ("<<<<<<<<<<<<<<<<\n\n"));
1094 if (filterThisEvent) {
1095 return KeyHandlingState::eHandled;
1097 // If another call of this method has already dispatched eKeyDown event,
1098 // we should return KeyHandlingState::eNotHandledButEventDispatched because
1099 // the caller should've stopped handling the event if preceding eKeyDown
1100 // event was consumed.
1101 if (aKeyboardEventWasDispatched) {
1102 return KeyHandlingState::eNotHandledButEventDispatched;
1104 if (!mKeyboardEventWasDispatched) {
1105 return KeyHandlingState::eNotHandled;
1107 return mKeyboardEventWasConsumed
1108 ? KeyHandlingState::eNotHandledButEventConsumed
1109 : KeyHandlingState::eNotHandledButEventDispatched;
1112 void IMContextWrapper::OnFocusChangeInGecko(bool aFocus) {
1113 MOZ_LOG(gIMELog, LogLevel::Info,
1114 ("0x%p OnFocusChangeInGecko(aFocus=%s),mCompositionState=%s, "
1115 "mIMEFocusState=%s, mSetInputPurposeAndInputHints=%s",
1116 this, ToChar(aFocus), GetCompositionStateName(),
1117 ToString(mIMEFocusState).c_str(),
1118 ToChar(mSetInputPurposeAndInputHints)));
1120 // We shouldn't carry over the removed string to another editor.
1121 mSelectedStringRemovedByComposition.Truncate();
1122 mContentSelection.reset();
1124 if (aFocus) {
1125 if (mSetInputPurposeAndInputHints) {
1126 mSetInputPurposeAndInputHints = false;
1127 SetInputPurposeAndInputHints();
1129 NotifyIMEOfFocusChange(IMEFocusState::Focused);
1130 } else {
1131 NotifyIMEOfFocusChange(IMEFocusState::Blurred);
1134 // When the focus changes, we need to inform IM about the new cursor
1135 // position. Chinese input methods generally rely on this because they
1136 // usually don't start composition until a character is picked.
1137 if (aFocus && EnsureToCacheContentSelection()) {
1138 SetCursorPosition(GetActiveContext());
1142 void IMContextWrapper::ResetIME() {
1143 MOZ_LOG(gIMELog, LogLevel::Info,
1144 ("0x%p ResetIME(), mCompositionState=%s, mIMEFocusState=%s", this,
1145 GetCompositionStateName(), ToString(mIMEFocusState).c_str()));
1147 GtkIMContext* activeContext = GetActiveContext();
1148 if (MOZ_UNLIKELY(!activeContext)) {
1149 MOZ_LOG(gIMELog, LogLevel::Error,
1150 ("0x%p ResetIME(), FAILED, there are no context", this));
1151 return;
1154 RefPtr<IMContextWrapper> kungFuDeathGrip(this);
1155 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
1157 mPendingResettingIMContext = false;
1158 gtk_im_context_reset(activeContext);
1160 // The last focused window might have been destroyed by a DOM event handler
1161 // which was called by us during a call of gtk_im_context_reset().
1162 if (!lastFocusedWindow ||
1163 NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) ||
1164 lastFocusedWindow->Destroyed()) {
1165 return;
1168 nsAutoString compositionString;
1169 GetCompositionString(activeContext, compositionString);
1171 MOZ_LOG(gIMELog, LogLevel::Debug,
1172 ("0x%p ResetIME() called gtk_im_context_reset(), "
1173 "activeContext=0x%p, mCompositionState=%s, compositionString=%s, "
1174 "mIMEFocusState=%s",
1175 this, activeContext, GetCompositionStateName(),
1176 NS_ConvertUTF16toUTF8(compositionString).get(),
1177 ToString(mIMEFocusState).c_str()));
1179 // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still
1180 // used in Japan!) sends only "preedit_changed" signal with empty
1181 // composition string synchronously. Therefore, if composition string
1182 // is now empty string, we should assume that the IME won't send
1183 // "commit" signal.
1184 if (IsComposing() && compositionString.IsEmpty()) {
1185 // WARNING: The widget might have been gone after this.
1186 DispatchCompositionCommitEvent(activeContext, &EmptyString());
1190 nsresult IMContextWrapper::EndIMEComposition(nsWindow* aCaller) {
1191 if (MOZ_UNLIKELY(IsDestroyed())) {
1192 return NS_OK;
1195 MOZ_LOG(gIMELog, LogLevel::Info,
1196 ("0x%p EndIMEComposition(aCaller=0x%p), "
1197 "mCompositionState=%s",
1198 this, aCaller, GetCompositionStateName()));
1200 if (aCaller != mLastFocusedWindow) {
1201 MOZ_LOG(gIMELog, LogLevel::Error,
1202 ("0x%p EndIMEComposition(), FAILED, the caller isn't "
1203 "focused window, mLastFocusedWindow=0x%p",
1204 this, mLastFocusedWindow));
1205 return NS_OK;
1208 if (!IsComposing()) {
1209 return NS_OK;
1212 // Currently, GTK has API neither to commit nor to cancel composition
1213 // forcibly. Therefore, TextComposition will recompute commit string for
1214 // the request even if native IME will cause unexpected commit string.
1215 // So, we don't need to emulate commit or cancel composition with
1216 // proper composition events.
1217 // XXX ResetIME() might not enough for finishing compositoin on some
1218 // environments. We should emulate focus change too because some IMEs
1219 // may commit or cancel composition at blur.
1220 ResetIME();
1222 return NS_OK;
1225 void IMContextWrapper::OnLayoutChange() {
1226 if (MOZ_UNLIKELY(IsDestroyed())) {
1227 return;
1230 if (IsComposing()) {
1231 SetCursorPosition(GetActiveContext());
1232 } else {
1233 // If not composing, candidate window position is updated before key
1234 // down
1235 mSetCursorPositionOnKeyEvent = true;
1237 mLayoutChanged = true;
1240 void IMContextWrapper::OnUpdateComposition() {
1241 if (MOZ_UNLIKELY(IsDestroyed())) {
1242 return;
1245 if (!IsComposing()) {
1246 // Composition has been committed. So we need update selection for
1247 // caret later
1248 mContentSelection.reset();
1249 EnsureToCacheContentSelection();
1250 mSetCursorPositionOnKeyEvent = true;
1253 // If we've already set candidate window position, we don't need to update
1254 // the position with update composition notification.
1255 if (!mLayoutChanged) {
1256 SetCursorPosition(GetActiveContext());
1260 void IMContextWrapper::SetInputContext(nsWindow* aCaller,
1261 const InputContext* aContext,
1262 const InputContextAction* aAction) {
1263 if (MOZ_UNLIKELY(IsDestroyed())) {
1264 return;
1267 MOZ_LOG(gIMELog, LogLevel::Info,
1268 ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ "
1269 "mEnabled=%s }, mHTMLInputType=%s })",
1270 this, aCaller, ToString(aContext->mIMEState.mEnabled).c_str(),
1271 NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));
1273 if (aCaller != mLastFocusedWindow) {
1274 MOZ_LOG(gIMELog, LogLevel::Error,
1275 ("0x%p SetInputContext(), FAILED, "
1276 "the caller isn't focused window, mLastFocusedWindow=0x%p",
1277 this, mLastFocusedWindow));
1278 return;
1281 if (!mContext) {
1282 MOZ_LOG(gIMELog, LogLevel::Error,
1283 ("0x%p SetInputContext(), FAILED, "
1284 "there are no context",
1285 this));
1286 return;
1289 if (sLastFocusedContext != this) {
1290 mInputContext = *aContext;
1291 MOZ_LOG(gIMELog, LogLevel::Debug,
1292 ("0x%p SetInputContext(), succeeded, "
1293 "but we're not active",
1294 this));
1295 return;
1298 const bool changingEnabledState =
1299 aContext->IsInputAttributeChanged(mInputContext);
1301 // Release current IME focus if IME is enabled.
1302 if (changingEnabledState && mInputContext.mIMEState.IsEditable()) {
1303 if (IsComposing()) {
1304 EndIMEComposition(mLastFocusedWindow);
1306 if (mIMEFocusState == IMEFocusState::Focused) {
1307 NotifyIMEOfFocusChange(IMEFocusState::BlurredWithoutFocusChange);
1311 mInputContext = *aContext;
1312 mSetInputPurposeAndInputHints = false;
1314 if (!changingEnabledState || !mInputContext.mIMEState.IsEditable()) {
1315 return;
1318 // If the input context was temporarily disabled without a focus change,
1319 // it must be ready to query content even if the focused content is in
1320 // a remote process. In this case, we should set IME focus right now.
1321 if (mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
1322 SetInputPurposeAndInputHints();
1323 NotifyIMEOfFocusChange(IMEFocusState::Focused);
1324 return;
1327 // Otherwise, we cannot set input-purpose and input-hints right now because
1328 // setting them may require to set focus immediately for IME own's UI.
1329 // However, at this moment, `ContentCacheInParent` does not have content
1330 // cache, it'll be available after `NOTIFY_IME_OF_FOCUS` notification.
1331 // Therefore, we set them at receiving the notification.
1332 mSetInputPurposeAndInputHints = true;
1335 void IMContextWrapper::SetInputPurposeAndInputHints() {
1336 GtkIMContext* currentContext = GetCurrentContext();
1337 if (!currentContext) {
1338 return;
1341 GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM;
1342 const nsString& inputType = mInputContext.mHTMLInputType;
1343 // Password case has difficult issue. Desktop IMEs disable composition if
1344 // input-purpose is password. For disabling IME on |ime-mode: disabled;|, we
1345 // need to check mEnabled value instead of inputType value. This hack also
1346 // enables composition on <input type="password" style="ime-mode: enabled;">.
1347 // This is right behavior of ime-mode on desktop.
1349 // On the other hand, IME for tablet devices may provide a specific software
1350 // keyboard for password field. If so, the behavior might look strange on
1351 // both:
1352 // <input type="text" style="ime-mode: disabled;">
1353 // <input type="password" style="ime-mode: enabled;">
1355 // Temporarily, we should focus on desktop environment for now. I.e., let's
1356 // ignore tablet devices for now. When somebody reports actual trouble on
1357 // tablet devices, we should try to look for a way to solve actual problem.
1358 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
1359 purpose = GTK_INPUT_PURPOSE_PASSWORD;
1360 } else if (inputType.EqualsLiteral("email")) {
1361 purpose = GTK_INPUT_PURPOSE_EMAIL;
1362 } else if (inputType.EqualsLiteral("url")) {
1363 purpose = GTK_INPUT_PURPOSE_URL;
1364 } else if (inputType.EqualsLiteral("tel")) {
1365 purpose = GTK_INPUT_PURPOSE_PHONE;
1366 } else if (inputType.EqualsLiteral("number")) {
1367 purpose = GTK_INPUT_PURPOSE_NUMBER;
1368 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("decimal")) {
1369 purpose = GTK_INPUT_PURPOSE_NUMBER;
1370 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("email")) {
1371 purpose = GTK_INPUT_PURPOSE_EMAIL;
1372 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("numeric")) {
1373 purpose = GTK_INPUT_PURPOSE_DIGITS;
1374 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("tel")) {
1375 purpose = GTK_INPUT_PURPOSE_PHONE;
1376 } else if (mInputContext.mHTMLInputMode.EqualsLiteral("url")) {
1377 purpose = GTK_INPUT_PURPOSE_URL;
1379 // Search by type and inputmode isn't supported on GTK.
1381 g_object_set(currentContext, "input-purpose", purpose, nullptr);
1383 // Although GtkInputHints is enum type, value is bit field.
1384 gint hints = GTK_INPUT_HINT_NONE;
1385 if (mInputContext.mHTMLInputMode.EqualsLiteral("none")) {
1386 hints |= GTK_INPUT_HINT_INHIBIT_OSK;
1389 if (mInputContext.mAutocapitalize.EqualsLiteral("characters")) {
1390 hints |= GTK_INPUT_HINT_UPPERCASE_CHARS;
1391 } else if (mInputContext.mAutocapitalize.EqualsLiteral("sentences")) {
1392 hints |= GTK_INPUT_HINT_UPPERCASE_SENTENCES;
1393 } else if (mInputContext.mAutocapitalize.EqualsLiteral("words")) {
1394 hints |= GTK_INPUT_HINT_UPPERCASE_WORDS;
1397 g_object_set(currentContext, "input-hints", hints, nullptr);
1400 InputContext IMContextWrapper::GetInputContext() {
1401 mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
1402 return mInputContext;
1405 GtkIMContext* IMContextWrapper::GetCurrentContext() const {
1406 if (IsEnabled()) {
1407 return mContext;
1409 if (mInputContext.mIMEState.mEnabled == IMEEnabled::Password) {
1410 return mSimpleContext;
1412 return mDummyContext;
1415 bool IMContextWrapper::IsValidContext(GtkIMContext* aContext) const {
1416 if (!aContext) {
1417 return false;
1419 return aContext == mContext || aContext == mSimpleContext ||
1420 aContext == mDummyContext;
1423 bool IMContextWrapper::IsEnabled() const {
1424 return mInputContext.mIMEState.mEnabled == IMEEnabled::Enabled ||
1425 (!sUseSimpleContext &&
1426 mInputContext.mIMEState.mEnabled == IMEEnabled::Password);
1429 void IMContextWrapper::NotifyIMEOfFocusChange(IMEFocusState aIMEFocusState) {
1430 MOZ_ASSERT_IF(aIMEFocusState == IMEFocusState::BlurredWithoutFocusChange,
1431 mIMEFocusState != IMEFocusState::Blurred);
1432 if (mIMEFocusState == aIMEFocusState) {
1433 return;
1436 MOZ_LOG(gIMELog, LogLevel::Info,
1437 ("0x%p NotifyIMEOfFocusChange(aIMEFocusState=%s), mIMEFocusState=%s, "
1438 "sLastFocusedContext=0x%p",
1439 this, ToString(aIMEFocusState).c_str(),
1440 ToString(mIMEFocusState).c_str(), sLastFocusedContext));
1441 MOZ_ASSERT(!mSetInputPurposeAndInputHints);
1443 // If we've already made IME blurred at setting the input context disabled
1444 // and it's now completely blurred by a focus move, we need only to update
1445 // mIMEFocusState and when the input context gets enabled, we cannot set
1446 // IME focus immediately.
1447 if (aIMEFocusState == IMEFocusState::Blurred &&
1448 mIMEFocusState == IMEFocusState::BlurredWithoutFocusChange) {
1449 mIMEFocusState = IMEFocusState::Blurred;
1450 return;
1453 auto Blur = [&](IMEFocusState aInternalState) {
1454 GtkIMContext* currentContext = GetCurrentContext();
1455 if (MOZ_UNLIKELY(!currentContext)) {
1456 MOZ_LOG(gIMELog, LogLevel::Error,
1457 ("0x%p NotifyIMEOfFocusChange()::Blur(), FAILED, "
1458 "there is no context",
1459 this));
1460 return;
1462 gtk_im_context_focus_out(currentContext);
1463 mIMEFocusState = aInternalState;
1466 if (aIMEFocusState != IMEFocusState::Focused) {
1467 return Blur(aIMEFocusState);
1470 GtkIMContext* currentContext = GetCurrentContext();
1471 if (MOZ_UNLIKELY(!currentContext)) {
1472 MOZ_LOG(gIMELog, LogLevel::Error,
1473 ("0x%p NotifyIMEOfFocusChange(), FAILED, "
1474 "there is no context",
1475 this));
1476 return;
1479 if (sLastFocusedContext && sLastFocusedContext != this) {
1480 sLastFocusedContext->NotifyIMEOfFocusChange(IMEFocusState::Blurred);
1483 sLastFocusedContext = this;
1485 // Forget all posted key events when focus is moved since they shouldn't
1486 // be fired in different editor.
1487 sWaitingSynthesizedKeyPressHardwareKeyCode = 0;
1488 mPostingKeyEvents.Clear();
1490 gtk_im_context_focus_in(currentContext);
1491 mIMEFocusState = aIMEFocusState;
1492 mSetCursorPositionOnKeyEvent = true;
1494 if (!IsEnabled()) {
1495 // We should release IME focus for uim and scim.
1496 // These IMs are using snooper that is released at losing focus.
1497 Blur(IMEFocusState::BlurredWithoutFocusChange);
1501 void IMContextWrapper::OnSelectionChange(
1502 nsWindow* aCaller, const IMENotification& aIMENotification) {
1503 const bool isSelectionRangeChanged =
1504 mContentSelection.isNothing() ||
1505 !aIMENotification.mSelectionChangeData.EqualsRange(
1506 mContentSelection.ref());
1507 mContentSelection =
1508 Some(ContentSelection(aIMENotification.mSelectionChangeData));
1509 const bool retrievedSurroundingSignalReceived =
1510 mRetrieveSurroundingSignalReceived;
1511 mRetrieveSurroundingSignalReceived = false;
1513 if (MOZ_UNLIKELY(IsDestroyed())) {
1514 return;
1517 const IMENotification::SelectionChangeDataBase& selectionChangeData =
1518 aIMENotification.mSelectionChangeData;
1520 MOZ_LOG(gIMELog, LogLevel::Info,
1521 ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
1522 "mSelectionChangeData=%s }), "
1523 "mCompositionState=%s, mIsDeletingSurrounding=%s, "
1524 "mRetrieveSurroundingSignalReceived=%s, isSelectionRangeChanged=%s",
1525 this, aCaller, ToString(selectionChangeData).c_str(),
1526 GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
1527 ToChar(retrievedSurroundingSignalReceived),
1528 ToChar(isSelectionRangeChanged)));
1530 if (aCaller != mLastFocusedWindow) {
1531 MOZ_LOG(gIMELog, LogLevel::Error,
1532 ("0x%p OnSelectionChange(), FAILED, "
1533 "the caller isn't focused window, mLastFocusedWindow=0x%p",
1534 this, mLastFocusedWindow));
1535 return;
1538 if (!IsComposing()) {
1539 // Now we have no composition (mostly situation on calling this method)
1540 // If we have it, it will set by
1541 // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED.
1542 mSetCursorPositionOnKeyEvent = true;
1545 // The focused editor might have placeholder text with normal text node.
1546 // In such case, the text node must be removed from a compositionstart
1547 // event handler. So, we're dispatching eCompositionStart,
1548 // we should ignore selection change notification.
1549 if (mCompositionState == eCompositionState_CompositionStartDispatched) {
1550 if (NS_WARN_IF(mContentSelection.isNothing())) {
1551 MOZ_LOG(gIMELog, LogLevel::Error,
1552 ("0x%p OnSelectionChange(), FAILED, "
1553 "new offset is too large, cannot keep composing",
1554 this));
1555 } else if (mContentSelection->HasRange()) {
1556 // Modify the selection start offset with new offset.
1557 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
1558 // XXX We should modify mSelectedStringRemovedByComposition?
1559 // But how?
1560 MOZ_LOG(gIMELog, LogLevel::Debug,
1561 ("0x%p OnSelectionChange(), ignored, mCompositionStart "
1562 "is updated to %u, the selection change doesn't cause "
1563 "resetting IM context",
1564 this, mCompositionStart));
1565 // And don't reset the IM context.
1566 return;
1567 } else {
1568 MOZ_LOG(
1569 gIMELog, LogLevel::Debug,
1570 ("0x%p OnSelectionChange(), ignored, because of no selection range",
1571 this));
1572 return;
1574 // Otherwise, reset the IM context due to impossible to keep composing.
1577 // If the selection change is caused by deleting surrounding text,
1578 // we shouldn't need to notify IME of selection change.
1579 if (mIsDeletingSurrounding) {
1580 return;
1583 bool occurredBeforeComposition =
1584 IsComposing() && !selectionChangeData.mOccurredDuringComposition &&
1585 !selectionChangeData.mCausedByComposition;
1586 if (occurredBeforeComposition) {
1587 mPendingResettingIMContext = true;
1590 // When the selection change is caused by dispatching composition event,
1591 // selection set event and/or occurred before starting current composition,
1592 // we shouldn't notify IME of that and commit existing composition.
1593 // Don't do this even if selection is not changed actually. For example,
1594 // fcitx has direct input mode which does not insert composing string, but
1595 // inserts commited text for each key sequence (i.e., there is "invisible"
1596 // composition string). In the world after bug 1712269, we don't use a
1597 // set of composition events for this kind of IME. Therefore,
1598 // SelectionChangeData.mCausedByComposition is not expected value for here
1599 // if this call is caused by a preceding commit. And if the preceding commit
1600 // is triggered by a key type for next word, resetting IME state makes fcitx
1601 // discard the pending input for the next word. Thus, we need to check
1602 // whether the selection range is actually changed here.
1603 if (!selectionChangeData.mCausedByComposition &&
1604 !selectionChangeData.mCausedBySelectionEvent && isSelectionRangeChanged &&
1605 !occurredBeforeComposition) {
1606 // Hack for ibus-pinyin. ibus-pinyin will synthesize a set of
1607 // composition which commits with empty string after calling
1608 // gtk_im_context_reset(). Therefore, selecting text causes
1609 // unexpectedly removing it. For preventing it but not breaking the
1610 // other IMEs which use surrounding text, we should call it only when
1611 // surrounding text has been retrieved after last selection range was
1612 // set. If it's not retrieved, that means that current IME doesn't
1613 // have any content cache, so, it must not need the notification of
1614 // selection change.
1615 if (IsComposing() || retrievedSurroundingSignalReceived) {
1616 ResetIME();
1621 /* static */
1622 void IMContextWrapper::OnThemeChanged() {
1623 if (auto* provider = SelectionStyleProvider::GetExistingInstance()) {
1624 provider->OnThemeChanged();
1628 /* static */
1629 void IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
1630 IMContextWrapper* aModule) {
1631 aModule->OnStartCompositionNative(aContext);
1634 void IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext) {
1635 // IME may synthesize composition asynchronously after filtering a
1636 // GDK_KEY_PRESS event. In that case, we should handle composition with
1637 // emulating the usual case, i.e., this is called in the stack of
1638 // OnKeyEvent().
1639 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1640 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1641 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1642 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1643 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1644 KEY_NAME_INDEX_USE_STRING) {
1645 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1646 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1650 MOZ_LOG(gIMELog, LogLevel::Info,
1651 ("0x%p OnStartCompositionNative(aContext=0x%p), "
1652 "current context=0x%p, mComposingContext=0x%p",
1653 this, aContext, GetCurrentContext(), mComposingContext));
1655 // See bug 472635, we should do nothing if IM context doesn't match.
1656 if (GetCurrentContext() != aContext) {
1657 MOZ_LOG(gIMELog, LogLevel::Error,
1658 ("0x%p OnStartCompositionNative(), FAILED, "
1659 "given context doesn't match",
1660 this));
1661 return;
1664 if (mComposingContext && aContext != mComposingContext) {
1665 // XXX For now, we should ignore this odd case, just logging.
1666 MOZ_LOG(gIMELog, LogLevel::Warning,
1667 ("0x%p OnStartCompositionNative(), Warning, "
1668 "there is already a composing context but starting new "
1669 "composition with different context",
1670 this));
1673 // IME may start composition without "preedit_start" signal. Therefore,
1674 // mComposingContext will be initialized in DispatchCompositionStart().
1676 if (!DispatchCompositionStart(aContext)) {
1677 return;
1679 mCompositionTargetRange.mOffset = mCompositionStart;
1680 mCompositionTargetRange.mLength = 0;
1683 /* static */
1684 void IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext,
1685 IMContextWrapper* aModule) {
1686 aModule->OnEndCompositionNative(aContext);
1689 void IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext) {
1690 MOZ_LOG(gIMELog, LogLevel::Info,
1691 ("0x%p OnEndCompositionNative(aContext=0x%p), mComposingContext=0x%p",
1692 this, aContext, mComposingContext));
1694 // See bug 472635, we should do nothing if IM context doesn't match.
1695 // Note that if this is called after focus move, the context may different
1696 // from any our owning context.
1697 if (!IsValidContext(aContext)) {
1698 MOZ_LOG(gIMELog, LogLevel::Error,
1699 ("0x%p OnEndCompositionNative(), FAILED, "
1700 "given context doesn't match with any context",
1701 this));
1702 return;
1705 // If we've not started composition with aContext, we should ignore it.
1706 if (aContext != mComposingContext) {
1707 MOZ_LOG(gIMELog, LogLevel::Warning,
1708 ("0x%p OnEndCompositionNative(), Warning, "
1709 "given context doesn't match with mComposingContext",
1710 this));
1711 return;
1714 g_object_unref(mComposingContext);
1715 mComposingContext = nullptr;
1717 // If we already handled the commit event, we should do nothing here.
1718 if (IsComposing()) {
1719 if (!DispatchCompositionCommitEvent(aContext)) {
1720 // If the widget is destroyed, we should do nothing anymore.
1721 return;
1725 if (mPendingResettingIMContext) {
1726 ResetIME();
1730 /* static */
1731 void IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext,
1732 IMContextWrapper* aModule) {
1733 RefPtr module = aModule;
1734 module->OnChangeCompositionNative(aContext);
1736 if (module->IsDestroyed()) {
1737 // A strong reference is already held during "preedit-changed" emission,
1738 // but _ibus_context_destroy_cb() in ibus 1.5.28 and
1739 // _fcitx_im_context_close_im_cb() in fcitx 4.2.9.9 want their
1740 // GtkIMContexts to live a little longer. See bug 1824634.
1741 NS_DispatchToMainThread(
1742 NS_NewRunnableFunction(__func__, [context = RefPtr{aContext}]() {}));
1746 void IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext) {
1747 // IME may synthesize composition asynchronously after filtering a
1748 // GDK_KEY_PRESS event. In that case, we should handle composition with
1749 // emulating the usual case, i.e., this is called in the stack of
1750 // OnKeyEvent().
1751 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1752 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1753 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1754 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1755 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1756 KEY_NAME_INDEX_USE_STRING) {
1757 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1758 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1762 MOZ_LOG(gIMELog, LogLevel::Info,
1763 ("0x%p OnChangeCompositionNative(aContext=0x%p), "
1764 "mComposingContext=0x%p",
1765 this, aContext, mComposingContext));
1767 // See bug 472635, we should do nothing if IM context doesn't match.
1768 // Note that if this is called after focus move, the context may different
1769 // from any our owning context.
1770 if (!IsValidContext(aContext)) {
1771 MOZ_LOG(gIMELog, LogLevel::Error,
1772 ("0x%p OnChangeCompositionNative(), FAILED, "
1773 "given context doesn't match with any context",
1774 this));
1775 return;
1778 if (mComposingContext && aContext != mComposingContext) {
1779 // XXX For now, we should ignore this odd case, just logging.
1780 MOZ_LOG(gIMELog, LogLevel::Warning,
1781 ("0x%p OnChangeCompositionNative(), Warning, "
1782 "given context doesn't match with composing context",
1783 this));
1786 nsAutoString compositionString;
1787 GetCompositionString(aContext, compositionString);
1788 if (!IsComposing() && compositionString.IsEmpty()) {
1789 MOZ_LOG(gIMELog, LogLevel::Warning,
1790 ("0x%p OnChangeCompositionNative(), Warning, does nothing "
1791 "because has not started composition and composing string is "
1792 "empty",
1793 this));
1794 mDispatchedCompositionString.Truncate();
1795 return; // Don't start the composition with empty string.
1798 // Be aware, widget can be gone
1799 DispatchCompositionChangeEvent(aContext, compositionString);
1802 /* static */
1803 gboolean IMContextWrapper::OnRetrieveSurroundingCallback(
1804 GtkIMContext* aContext, IMContextWrapper* aModule) {
1805 return aModule->OnRetrieveSurroundingNative(aContext);
1808 gboolean IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext) {
1809 MOZ_LOG(gIMELog, LogLevel::Info,
1810 ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), "
1811 "current context=0x%p",
1812 this, aContext, GetCurrentContext()));
1814 // See bug 472635, we should do nothing if IM context doesn't match.
1815 if (GetCurrentContext() != aContext) {
1816 MOZ_LOG(gIMELog, LogLevel::Error,
1817 ("0x%p OnRetrieveSurroundingNative(), FAILED, "
1818 "given context doesn't match",
1819 this));
1820 return FALSE;
1823 nsAutoString uniStr;
1824 uint32_t cursorPos;
1825 if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) {
1826 return FALSE;
1829 // Despite taking a pointer and a length, IBus wants the string to be
1830 // zero-terminated and doesn't like U+0000 within the string.
1831 uniStr.ReplaceChar(char16_t(0), char16_t(0xFFFD));
1833 NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
1834 uint32_t cursorPosInUTF8 = utf8Str.Length();
1835 AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
1836 gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
1837 cursorPosInUTF8);
1838 mRetrieveSurroundingSignalReceived = true;
1839 return TRUE;
1842 /* static */
1843 gboolean IMContextWrapper::OnDeleteSurroundingCallback(
1844 GtkIMContext* aContext, gint aOffset, gint aNChars,
1845 IMContextWrapper* aModule) {
1846 return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
1849 gboolean IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext,
1850 gint aOffset,
1851 gint aNChars) {
1852 MOZ_LOG(gIMELog, LogLevel::Info,
1853 ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, "
1854 "aNChar=%d), current context=0x%p",
1855 this, aContext, aOffset, aNChars, GetCurrentContext()));
1857 // See bug 472635, we should do nothing if IM context doesn't match.
1858 if (GetCurrentContext() != aContext) {
1859 MOZ_LOG(gIMELog, LogLevel::Error,
1860 ("0x%p OnDeleteSurroundingNative(), FAILED, "
1861 "given context doesn't match",
1862 this));
1863 return FALSE;
1866 AutoRestore<bool> saveDeletingSurrounding(mIsDeletingSurrounding);
1867 mIsDeletingSurrounding = true;
1868 if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) {
1869 return TRUE;
1872 // failed
1873 MOZ_LOG(gIMELog, LogLevel::Error,
1874 ("0x%p OnDeleteSurroundingNative(), FAILED, "
1875 "cannot delete text",
1876 this));
1877 return FALSE;
1880 /* static */
1881 void IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext,
1882 const gchar* aString,
1883 IMContextWrapper* aModule) {
1884 aModule->OnCommitCompositionNative(aContext, aString);
1887 void IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext,
1888 const gchar* aUTF8Char) {
1889 const gchar emptyStr = 0;
1890 const gchar* commitString = aUTF8Char ? aUTF8Char : &emptyStr;
1891 NS_ConvertUTF8toUTF16 utf16CommitString(commitString);
1893 // IME may synthesize composition asynchronously after filtering a
1894 // GDK_KEY_PRESS event. In that case, we should handle composition with
1895 // emulating the usual case, i.e., this is called in the stack of
1896 // OnKeyEvent().
1897 Maybe<AutoRestore<GdkEventKey*>> maybeRestoreProcessingKeyEvent;
1898 if (!mProcessingKeyEvent && !mPostingKeyEvents.IsEmpty()) {
1899 GdkEventKey* keyEvent = mPostingKeyEvents.GetFirstEvent();
1900 if (keyEvent && keyEvent->type == GDK_KEY_PRESS &&
1901 KeymapWrapper::ComputeDOMKeyNameIndex(keyEvent) ==
1902 KEY_NAME_INDEX_USE_STRING) {
1903 maybeRestoreProcessingKeyEvent.emplace(mProcessingKeyEvent);
1904 mProcessingKeyEvent = mPostingKeyEvents.GetFirstEvent();
1908 MOZ_LOG(gIMELog, LogLevel::Info,
1909 ("0x%p OnCommitCompositionNative(aContext=0x%p), "
1910 "current context=0x%p, active context=0x%p, commitString=\"%s\", "
1911 "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s",
1912 this, aContext, GetCurrentContext(), GetActiveContext(),
1913 commitString, mProcessingKeyEvent, ToChar(IsComposingOn(aContext))));
1915 // See bug 472635, we should do nothing if IM context doesn't match.
1916 if (!IsValidContext(aContext)) {
1917 MOZ_LOG(gIMELog, LogLevel::Error,
1918 ("0x%p OnCommitCompositionNative(), FAILED, "
1919 "given context doesn't match",
1920 this));
1921 return;
1924 // If we are not in composition and committing with empty string,
1925 // we need to do nothing because if we continued to handle this
1926 // signal, we would dispatch compositionstart, text, compositionend
1927 // events with empty string. Of course, they are unnecessary events
1928 // for Web applications and our editor.
1929 if (!IsComposingOn(aContext) && utf16CommitString.IsEmpty()) {
1930 MOZ_LOG(gIMELog, LogLevel::Warning,
1931 ("0x%p OnCommitCompositionNative(), Warning, does nothing "
1932 "because has not started composition and commit string is empty",
1933 this));
1934 return;
1937 // If IME doesn't change their keyevent that generated this commit,
1938 // we should treat that IME didn't handle the key event because
1939 // web applications want to receive "keydown" and "keypress" event
1940 // in such case.
1941 // NOTE: While a key event is being handled, this might be caused on
1942 // current context. Otherwise, this may be caused on active context.
1943 if (!IsComposingOn(aContext) && mProcessingKeyEvent &&
1944 mProcessingKeyEvent->type == GDK_KEY_PRESS &&
1945 aContext == GetCurrentContext()) {
1946 char keyval_utf8[8]; /* should have at least 6 bytes of space */
1947 gint keyval_utf8_len;
1948 guint32 keyval_unicode;
1950 keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval);
1951 keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8);
1952 keyval_utf8[keyval_utf8_len] = '\0';
1954 // If committing string is exactly same as a character which is
1955 // produced by the key, eKeyDown and eKeyPress event should be
1956 // dispatched by the caller of OnKeyEvent() normally. Note that
1957 // mMaybeInDeadKeySequence will be set to false by OnKeyEvent()
1958 // since we set mFallbackToKeyEvent to true here.
1959 if (!strcmp(commitString, keyval_utf8)) {
1960 MOZ_LOG(gIMELog, LogLevel::Info,
1961 ("0x%p OnCommitCompositionNative(), "
1962 "we'll send normal key event",
1963 this));
1964 mFallbackToKeyEvent = true;
1965 return;
1968 // If we're in a dead key sequence, commit string is a character in
1969 // the BMP and mProcessingKeyEvent produces some characters but it's
1970 // not same as committing string, we should dispatch an eKeyPress
1971 // event from here.
1972 WidgetKeyboardEvent keyDownEvent(true, eKeyDown, mLastFocusedWindow);
1973 KeymapWrapper::InitKeyEvent(keyDownEvent, mProcessingKeyEvent, false);
1974 if (mMaybeInDeadKeySequence && utf16CommitString.Length() == 1 &&
1975 keyDownEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
1976 mKeyboardEventWasDispatched = true;
1977 // Anyway, we're not in dead key sequence anymore.
1978 mMaybeInDeadKeySequence = false;
1980 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
1981 nsresult rv = dispatcher->BeginNativeInputTransaction();
1982 if (NS_WARN_IF(NS_FAILED(rv))) {
1983 MOZ_LOG(gIMELog, LogLevel::Error,
1984 ("0x%p OnCommitCompositionNative(), FAILED, "
1985 "due to BeginNativeInputTransaction() failure",
1986 this));
1987 return;
1990 // First, dispatch eKeyDown event.
1991 keyDownEvent.mKeyValue = utf16CommitString;
1992 nsEventStatus status = nsEventStatus_eIgnore;
1993 bool dispatched = dispatcher->DispatchKeyboardEvent(
1994 eKeyDown, keyDownEvent, status, mProcessingKeyEvent);
1995 if (!dispatched || status == nsEventStatus_eConsumeNoDefault) {
1996 mKeyboardEventWasConsumed = true;
1997 MOZ_LOG(gIMELog, LogLevel::Info,
1998 ("0x%p OnCommitCompositionNative(), "
1999 "doesn't dispatch eKeyPress event because the preceding "
2000 "eKeyDown event was not dispatched or was consumed",
2001 this));
2002 return;
2004 if (mLastFocusedWindow != keyDownEvent.mWidget ||
2005 mLastFocusedWindow->Destroyed()) {
2006 MOZ_LOG(gIMELog, LogLevel::Warning,
2007 ("0x%p OnCommitCompositionNative(), Warning, "
2008 "stop dispatching eKeyPress event because the preceding "
2009 "eKeyDown event caused changing focused widget or "
2010 "destroyed",
2011 this));
2012 return;
2014 MOZ_LOG(gIMELog, LogLevel::Info,
2015 ("0x%p OnCommitCompositionNative(), "
2016 "dispatched eKeyDown event for the committed character",
2017 this));
2019 // Next, dispatch eKeyPress event.
2020 dispatcher->MaybeDispatchKeypressEvents(keyDownEvent, status,
2021 mProcessingKeyEvent);
2022 MOZ_LOG(gIMELog, LogLevel::Info,
2023 ("0x%p OnCommitCompositionNative(), "
2024 "dispatched eKeyPress event for the committed character",
2025 this));
2026 return;
2030 NS_ConvertUTF8toUTF16 str(commitString);
2031 // Be aware, widget can be gone
2032 DispatchCompositionCommitEvent(aContext, &str);
2035 void IMContextWrapper::GetCompositionString(GtkIMContext* aContext,
2036 nsAString& aCompositionString) {
2037 gchar* preedit_string;
2038 gint cursor_pos;
2039 PangoAttrList* feedback_list;
2040 gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
2041 &cursor_pos);
2042 if (preedit_string && *preedit_string) {
2043 CopyUTF8toUTF16(MakeStringSpan(preedit_string), aCompositionString);
2044 } else {
2045 aCompositionString.Truncate();
2048 MOZ_LOG(gIMELog, LogLevel::Info,
2049 ("0x%p GetCompositionString(aContext=0x%p), "
2050 "aCompositionString=\"%s\"",
2051 this, aContext, preedit_string));
2053 pango_attr_list_unref(feedback_list);
2054 g_free(preedit_string);
2057 bool IMContextWrapper::MaybeDispatchKeyEventAsProcessedByIME(
2058 EventMessage aFollowingEvent) {
2059 if (!mLastFocusedWindow) {
2060 return false;
2063 if (!mIsKeySnooped &&
2064 ((!mProcessingKeyEvent && mPostingKeyEvents.IsEmpty()) ||
2065 (mProcessingKeyEvent && mKeyboardEventWasDispatched))) {
2066 return true;
2069 // A "keydown" or "keyup" event handler may change focus with the
2070 // following event. In such case, we need to cancel this composition.
2071 // So, we need to store IM context now because mComposingContext may be
2072 // overwritten with different context if calling this method recursively.
2073 // Note that we don't need to grab the context here because |context|
2074 // will be used only for checking if it's same as mComposingContext.
2075 GtkIMContext* oldCurrentContext = GetCurrentContext();
2076 GtkIMContext* oldComposingContext = mComposingContext;
2078 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2080 if (mProcessingKeyEvent || !mPostingKeyEvents.IsEmpty()) {
2081 if (mProcessingKeyEvent) {
2082 mKeyboardEventWasDispatched = true;
2084 // If we're not handling a key event synchronously, the signal may be
2085 // sent by IME without sending key event to us. In such case, we
2086 // should dispatch keyboard event for the last key event which was
2087 // posted to other IME process.
2088 GdkEventKey* sourceEvent = mProcessingKeyEvent
2089 ? mProcessingKeyEvent
2090 : mPostingKeyEvents.GetFirstEvent();
2092 MOZ_LOG(
2093 gIMELog, LogLevel::Info,
2094 ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
2095 "aFollowingEvent=%s), dispatch %s %s "
2096 "event: { type=%s, keyval=%s, unicode=0x%X, state=%s, "
2097 "time=%u, hardware_keycode=%u, group=%u }",
2098 this, ToChar(aFollowingEvent),
2099 ToChar(sourceEvent->type == GDK_KEY_PRESS ? eKeyDown : eKeyUp),
2100 mProcessingKeyEvent ? "processing" : "posted",
2101 GetEventType(sourceEvent), gdk_keyval_name(sourceEvent->keyval),
2102 gdk_keyval_to_unicode(sourceEvent->keyval),
2103 GetEventStateName(sourceEvent->state, mIMContextID).get(),
2104 sourceEvent->time, sourceEvent->hardware_keycode, sourceEvent->group));
2106 // Let's dispatch eKeyDown event or eKeyUp event now. Note that only
2107 // when we're not in a dead key composition, we should mark the
2108 // eKeyDown and eKeyUp event as "processed by IME" since we should
2109 // expose raw keyCode and key value to web apps the key event is a
2110 // part of a dead key sequence.
2111 // FYI: We should ignore if default of preceding keydown or keyup
2112 // event is prevented since even on the other browsers, web
2113 // applications cannot cancel the following composition event.
2114 // Spec bug: https://github.com/w3c/uievents/issues/180
2115 KeymapWrapper::DispatchKeyDownOrKeyUpEvent(lastFocusedWindow, sourceEvent,
2116 !mMaybeInDeadKeySequence,
2117 &mKeyboardEventWasConsumed);
2118 MOZ_LOG(gIMELog, LogLevel::Info,
2119 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), keydown or keyup "
2120 "event is dispatched",
2121 this));
2123 if (!mProcessingKeyEvent) {
2124 MOZ_LOG(gIMELog, LogLevel::Info,
2125 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), removing first "
2126 "event from the queue",
2127 this));
2128 mPostingKeyEvents.RemoveEvent(sourceEvent);
2130 } else {
2131 MOZ_ASSERT(mIsKeySnooped);
2132 // Currently, we support key snooper mode of uim and wayland only.
2133 MOZ_ASSERT(mIMContextID == IMContextID::Uim ||
2134 mIMContextID == IMContextID::Wayland);
2135 // uim sends "preedit_start" signal and "preedit_changed" separately
2136 // at starting composition, "commit" and "preedit_end" separately at
2137 // committing composition.
2139 // Currently, we should dispatch only fake eKeyDown event because
2140 // we cannot decide which is the last signal of each key operation
2141 // and Chromium also dispatches only "keydown" event in this case.
2142 bool dispatchFakeKeyDown = false;
2143 switch (aFollowingEvent) {
2144 case eCompositionStart:
2145 case eCompositionCommit:
2146 case eCompositionCommitAsIs:
2147 case eContentCommandInsertText:
2148 dispatchFakeKeyDown = true;
2149 break;
2150 // XXX Unfortunately, I don't have a good idea to prevent to
2151 // dispatch redundant eKeyDown event for eCompositionStart
2152 // immediately after "delete_surrounding" signal. However,
2153 // not dispatching eKeyDown event is worse than dispatching
2154 // redundant eKeyDown events.
2155 case eContentCommandDelete:
2156 dispatchFakeKeyDown = true;
2157 break;
2158 // We need to prevent to dispatch redundant eKeyDown event for
2159 // eCompositionChange immediately after eCompositionStart. So,
2160 // We should not dispatch eKeyDown event if dispatched composition
2161 // string is still empty string.
2162 case eCompositionChange:
2163 dispatchFakeKeyDown = !mDispatchedCompositionString.IsEmpty();
2164 break;
2165 default:
2166 MOZ_ASSERT_UNREACHABLE("Do you forget to handle the case?");
2167 break;
2170 if (dispatchFakeKeyDown) {
2171 WidgetKeyboardEvent fakeKeyDownEvent(true, eKeyDown, lastFocusedWindow);
2172 fakeKeyDownEvent.mKeyCode = NS_VK_PROCESSKEY;
2173 fakeKeyDownEvent.mKeyNameIndex = KEY_NAME_INDEX_Process;
2174 // It's impossible to get physical key information in this case but
2175 // this should be okay since web apps shouldn't do anything with
2176 // physical key information during composition.
2177 fakeKeyDownEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN;
2179 MOZ_LOG(gIMELog, LogLevel::Info,
2180 ("0x%p MaybeDispatchKeyEventAsProcessedByIME("
2181 "aFollowingEvent=%s), dispatch fake eKeyDown event",
2182 this, ToChar(aFollowingEvent)));
2184 KeymapWrapper::DispatchKeyDownOrKeyUpEvent(
2185 lastFocusedWindow, fakeKeyDownEvent, &mKeyboardEventWasConsumed);
2186 MOZ_LOG(gIMELog, LogLevel::Info,
2187 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), "
2188 "fake keydown event is dispatched",
2189 this));
2193 if (lastFocusedWindow->IsDestroyed() ||
2194 lastFocusedWindow != mLastFocusedWindow) {
2195 MOZ_LOG(gIMELog, LogLevel::Warning,
2196 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the "
2197 "focused widget was destroyed/changed by a key event",
2198 this));
2199 return false;
2202 // If the dispatched keydown event caused moving focus and that also
2203 // caused changing active context, we need to cancel composition here.
2204 if (GetCurrentContext() != oldCurrentContext) {
2205 MOZ_LOG(gIMELog, LogLevel::Warning,
2206 ("0x%p MaybeDispatchKeyEventAsProcessedByIME(), Warning, the key "
2207 "event causes changing active IM context",
2208 this));
2209 if (mComposingContext == oldComposingContext) {
2210 // Only when the context is still composing, we should call
2211 // ResetIME() here. Otherwise, it should've already been
2212 // cleaned up.
2213 ResetIME();
2215 return false;
2218 return true;
2221 bool IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext) {
2222 MOZ_LOG(gIMELog, LogLevel::Info,
2223 ("0x%p DispatchCompositionStart(aContext=0x%p)", this, aContext));
2225 if (IsComposing()) {
2226 MOZ_LOG(gIMELog, LogLevel::Error,
2227 ("0x%p DispatchCompositionStart(), FAILED, "
2228 "we're already in composition",
2229 this));
2230 return true;
2233 if (!mLastFocusedWindow) {
2234 MOZ_LOG(gIMELog, LogLevel::Error,
2235 ("0x%p DispatchCompositionStart(), FAILED, "
2236 "there are no focused window in this module",
2237 this));
2238 return false;
2241 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
2242 MOZ_LOG(gIMELog, LogLevel::Error,
2243 ("0x%p DispatchCompositionStart(), FAILED, "
2244 "cannot query the selection offset",
2245 this));
2246 return false;
2249 if (NS_WARN_IF(!mContentSelection->HasRange())) {
2250 MOZ_LOG(gIMELog, LogLevel::Error,
2251 ("0x%p DispatchCompositionStart(), FAILED, "
2252 "due to no selection",
2253 this));
2254 return false;
2257 mComposingContext = static_cast<GtkIMContext*>(g_object_ref(aContext));
2258 MOZ_ASSERT(mComposingContext);
2260 // Keep the last focused window alive
2261 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2263 // XXX The composition start point might be changed by composition events
2264 // even though we strongly hope it doesn't happen.
2265 // Every composition event should have the start offset for the result
2266 // because it may high cost if we query the offset every time.
2267 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
2268 mDispatchedCompositionString.Truncate();
2270 // If this composition is started by a key press, we need to dispatch
2271 // eKeyDown or eKeyUp event before dispatching eCompositionStart event.
2272 // Note that dispatching a keyboard event which is marked as "processed
2273 // by IME" is okay since Chromium also dispatches keyboard event as so.
2274 if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionStart)) {
2275 MOZ_LOG(gIMELog, LogLevel::Warning,
2276 ("0x%p DispatchCompositionStart(), Warning, "
2277 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2278 this));
2279 return false;
2282 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
2283 nsresult rv = dispatcher->BeginNativeInputTransaction();
2284 if (NS_WARN_IF(NS_FAILED(rv))) {
2285 MOZ_LOG(gIMELog, LogLevel::Error,
2286 ("0x%p DispatchCompositionStart(), FAILED, "
2287 "due to BeginNativeInputTransaction() failure",
2288 this));
2289 return false;
2292 static bool sHasSetTelemetry = false;
2293 if (!sHasSetTelemetry) {
2294 sHasSetTelemetry = true;
2295 NS_ConvertUTF8toUTF16 im(GetIMName());
2296 // 72 is kMaximumKeyStringLength in TelemetryScalar.cpp
2297 if (im.Length() > 72) {
2298 if (NS_IS_SURROGATE_PAIR(im[72 - 2], im[72 - 1])) {
2299 im.Truncate(72 - 2);
2300 } else {
2301 im.Truncate(72 - 1);
2303 // U+2026 is "..."
2304 im.Append(char16_t(0x2026));
2306 glean::widget::ime_name_on_linux.Get(NS_ConvertUTF16toUTF8(im)).Set(true);
2309 MOZ_LOG(gIMELog, LogLevel::Debug,
2310 ("0x%p DispatchCompositionStart(), dispatching "
2311 "compositionstart... (mCompositionStart=%u)",
2312 this, mCompositionStart));
2313 mCompositionState = eCompositionState_CompositionStartDispatched;
2314 nsEventStatus status;
2315 dispatcher->StartComposition(status);
2316 if (lastFocusedWindow->IsDestroyed() ||
2317 lastFocusedWindow != mLastFocusedWindow) {
2318 MOZ_LOG(gIMELog, LogLevel::Error,
2319 ("0x%p DispatchCompositionStart(), FAILED, the focused "
2320 "widget was destroyed/changed by compositionstart event",
2321 this));
2322 return false;
2325 return true;
2328 bool IMContextWrapper::DispatchCompositionChangeEvent(
2329 GtkIMContext* aContext, const nsAString& aCompositionString) {
2330 MOZ_LOG(
2331 gIMELog, LogLevel::Info,
2332 ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)", this, aContext));
2334 if (!mLastFocusedWindow) {
2335 MOZ_LOG(gIMELog, LogLevel::Error,
2336 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2337 "there are no focused window in this module",
2338 this));
2339 return false;
2342 if (!IsComposing()) {
2343 MOZ_LOG(gIMELog, LogLevel::Debug,
2344 ("0x%p DispatchCompositionChangeEvent(), the composition "
2345 "wasn't started, force starting...",
2346 this));
2347 if (!DispatchCompositionStart(aContext)) {
2348 return false;
2351 // If this composition string change caused by a key press, we need to
2352 // dispatch eKeyDown or eKeyUp before dispatching eCompositionChange event.
2353 else if (!MaybeDispatchKeyEventAsProcessedByIME(eCompositionChange)) {
2354 MOZ_LOG(gIMELog, LogLevel::Warning,
2355 ("0x%p DispatchCompositionChangeEvent(), Warning, "
2356 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2357 this));
2358 return false;
2361 RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
2362 nsresult rv = dispatcher->BeginNativeInputTransaction();
2363 if (NS_WARN_IF(NS_FAILED(rv))) {
2364 MOZ_LOG(gIMELog, LogLevel::Error,
2365 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2366 "due to BeginNativeInputTransaction() failure",
2367 this));
2368 return false;
2371 // Store the selected string which will be removed by following
2372 // compositionchange event.
2373 if (mCompositionState == eCompositionState_CompositionStartDispatched) {
2374 if (NS_WARN_IF(!EnsureToCacheContentSelection(
2375 &mSelectedStringRemovedByComposition))) {
2376 // XXX How should we behave in this case??
2377 } else if (mContentSelection->HasRange()) {
2378 // XXX We should assume, for now, any web applications don't change
2379 // selection at handling this compositionchange event.
2380 mCompositionStart = mContentSelection->OffsetAndDataRef().StartOffset();
2381 } else {
2382 // If there is no selection range, we should keep previously storing
2383 // mCompositionStart.
2387 RefPtr<TextRangeArray> rangeArray =
2388 CreateTextRangeArray(aContext, aCompositionString);
2390 rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray);
2391 if (NS_WARN_IF(NS_FAILED(rv))) {
2392 MOZ_LOG(gIMELog, LogLevel::Error,
2393 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2394 "due to SetPendingComposition() failure",
2395 this));
2396 return false;
2399 mCompositionState = eCompositionState_CompositionChangeEventDispatched;
2401 // We cannot call SetCursorPosition for e10s-aware.
2402 // DispatchEvent is async on e10s, so composition rect isn't updated now
2403 // on tab parent.
2404 mDispatchedCompositionString = aCompositionString;
2405 mLayoutChanged = false;
2406 mCompositionTargetRange.mOffset =
2407 mCompositionStart + rangeArray->TargetClauseOffset();
2408 mCompositionTargetRange.mLength = rangeArray->TargetClauseLength();
2410 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2411 nsEventStatus status;
2412 rv = dispatcher->FlushPendingComposition(status);
2413 if (NS_WARN_IF(NS_FAILED(rv))) {
2414 MOZ_LOG(gIMELog, LogLevel::Error,
2415 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2416 "due to FlushPendingComposition() failure",
2417 this));
2418 return false;
2421 if (lastFocusedWindow->IsDestroyed() ||
2422 lastFocusedWindow != mLastFocusedWindow) {
2423 MOZ_LOG(gIMELog, LogLevel::Error,
2424 ("0x%p DispatchCompositionChangeEvent(), FAILED, the "
2425 "focused widget was destroyed/changed by "
2426 "compositionchange event",
2427 this));
2428 return false;
2430 return true;
2433 bool IMContextWrapper::DispatchCompositionCommitEvent(
2434 GtkIMContext* aContext, const nsAString* aCommitString) {
2435 MOZ_LOG(gIMELog, LogLevel::Info,
2436 ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, "
2437 "aCommitString=0x%p, (\"%s\"))",
2438 this, aContext, aCommitString,
2439 aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));
2441 if (!mLastFocusedWindow) {
2442 MOZ_LOG(gIMELog, LogLevel::Error,
2443 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2444 "there are no focused window in this module",
2445 this));
2446 return false;
2449 // TODO: We need special care to handle request to commit composition
2450 // by content while we're committing composition because we have
2451 // commit string information now but IME may not have composition
2452 // anymore. Therefore, we may not be able to handle commit as
2453 // expected. However, this is rare case because this situation
2454 // never occurs with remote content. So, it's okay to fix this
2455 // issue later. (Perhaps, TextEventDisptcher should do it for
2456 // all platforms. E.g., creating WillCommitComposition()?)
2457 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
2458 RefPtr<TextEventDispatcher> dispatcher;
2459 if (!IsComposing() &&
2460 !StaticPrefs::intl_ime_use_composition_events_for_insert_text()) {
2461 if (!aCommitString || aCommitString->IsEmpty()) {
2462 MOZ_LOG(gIMELog, LogLevel::Error,
2463 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2464 "did nothing due to inserting empty string without composition",
2465 this));
2466 return true;
2468 if (MOZ_UNLIKELY(!EnsureToCacheContentSelection())) {
2469 MOZ_LOG(gIMELog, LogLevel::Warning,
2470 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2471 "Failed to cache selection before dispatching "
2472 "eContentCommandInsertText event",
2473 this));
2475 if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandInsertText)) {
2476 MOZ_LOG(gIMELog, LogLevel::Warning,
2477 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2478 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2479 this));
2480 return false;
2482 // Emulate selection until receiving actual selection range. This is
2483 // important for OnSelectionChange. If selection is not changed by web
2484 // apps, i.e., selection range is same as what selection expects, we
2485 // shouldn't reset IME because the trigger of causing this commit may be an
2486 // input for next composition and we shouldn't cancel it.
2487 if (mContentSelection.isSome()) {
2488 mContentSelection->Collapse(
2489 (mContentSelection->HasRange()
2490 ? mContentSelection->OffsetAndDataRef().StartOffset()
2491 : mCompositionStart) +
2492 aCommitString->Length());
2493 MOZ_LOG(gIMELog, LogLevel::Info,
2494 ("0x%p DispatchCompositionCommitEvent(), mContentSelection=%s",
2495 this, ToString(mContentSelection).c_str()));
2497 MOZ_ASSERT(!dispatcher);
2498 } else {
2499 if (!IsComposing()) {
2500 if (!aCommitString || aCommitString->IsEmpty()) {
2501 MOZ_LOG(gIMELog, LogLevel::Error,
2502 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2503 "there is no composition and empty commit string",
2504 this));
2505 return true;
2507 MOZ_LOG(gIMELog, LogLevel::Debug,
2508 ("0x%p DispatchCompositionCommitEvent(), "
2509 "the composition wasn't started, force starting...",
2510 this));
2511 if (!DispatchCompositionStart(aContext)) {
2512 return false;
2515 // If this commit caused by a key press, we need to dispatch eKeyDown or
2516 // eKeyUp before dispatching composition events.
2517 else if (!MaybeDispatchKeyEventAsProcessedByIME(
2518 aCommitString ? eCompositionCommit : eCompositionCommitAsIs)) {
2519 MOZ_LOG(gIMELog, LogLevel::Warning,
2520 ("0x%p DispatchCompositionCommitEvent(), Warning, "
2521 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
2522 this));
2523 mCompositionState = eCompositionState_NotComposing;
2524 return false;
2527 dispatcher = GetTextEventDispatcher();
2528 MOZ_ASSERT(dispatcher);
2529 nsresult rv = dispatcher->BeginNativeInputTransaction();
2530 if (NS_WARN_IF(NS_FAILED(rv))) {
2531 MOZ_LOG(gIMELog, LogLevel::Error,
2532 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2533 "due to BeginNativeInputTransaction() failure",
2534 this));
2535 return false;
2538 // Emulate selection until receiving actual selection range.
2539 const uint32_t offsetToPutCaret =
2540 mCompositionStart + (aCommitString
2541 ? aCommitString->Length()
2542 : mDispatchedCompositionString.Length());
2543 if (mContentSelection.isSome()) {
2544 mContentSelection->Collapse(offsetToPutCaret);
2545 } else {
2546 // TODO: We should guarantee that there should be at least fake selection
2547 // for IME at here. Then, we can keep the last writing mode.
2548 mContentSelection.emplace(offsetToPutCaret, WritingMode());
2552 mCompositionState = eCompositionState_NotComposing;
2553 // Reset dead key sequence too because GTK doesn't support dead key chain
2554 // (i.e., a key press doesn't cause both producing some characters and
2555 // restarting new dead key sequence at one time). So, committing
2556 // composition means end of a dead key sequence.
2557 mMaybeInDeadKeySequence = false;
2558 mCompositionStart = UINT32_MAX;
2559 mCompositionTargetRange.Clear();
2560 mDispatchedCompositionString.Truncate();
2561 mSelectedStringRemovedByComposition.Truncate();
2563 if (!dispatcher) {
2564 MOZ_ASSERT(aCommitString);
2565 MOZ_ASSERT(!aCommitString->IsEmpty());
2566 nsEventStatus status = nsEventStatus_eIgnore;
2567 WidgetContentCommandEvent insertTextEvent(true, eContentCommandInsertText,
2568 lastFocusedWindow);
2569 insertTextEvent.mString.emplace(*aCommitString);
2570 lastFocusedWindow->DispatchEvent(&insertTextEvent, status);
2572 if (!insertTextEvent.mSucceeded) {
2573 MOZ_LOG(gIMELog, LogLevel::Error,
2574 ("0x%p DispatchCompositionChangeEvent(), FAILED, inserting "
2575 "text failed",
2576 this));
2577 return false;
2579 } else {
2580 nsEventStatus status = nsEventStatus_eIgnore;
2581 nsresult rv = dispatcher->CommitComposition(status, aCommitString);
2582 if (NS_WARN_IF(NS_FAILED(rv))) {
2583 MOZ_LOG(gIMELog, LogLevel::Error,
2584 ("0x%p DispatchCompositionChangeEvent(), FAILED, "
2585 "due to CommitComposition() failure",
2586 this));
2587 return false;
2591 if (lastFocusedWindow->IsDestroyed() ||
2592 lastFocusedWindow != mLastFocusedWindow) {
2593 MOZ_LOG(gIMELog, LogLevel::Error,
2594 ("0x%p DispatchCompositionCommitEvent(), FAILED, "
2595 "the focused widget was destroyed/changed by "
2596 "compositioncommit event",
2597 this));
2598 return false;
2601 return true;
2604 already_AddRefed<TextRangeArray> IMContextWrapper::CreateTextRangeArray(
2605 GtkIMContext* aContext, const nsAString& aCompositionString) {
2606 MOZ_LOG(gIMELog, LogLevel::Info,
2607 ("0x%p CreateTextRangeArray(aContext=0x%p, "
2608 "aCompositionString=\"%s\" (Length()=%zu))",
2609 this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(),
2610 aCompositionString.Length()));
2612 RefPtr<TextRangeArray> textRangeArray = new TextRangeArray();
2614 gchar* preedit_string;
2615 gint cursor_pos_in_chars;
2616 PangoAttrList* feedback_list;
2617 gtk_im_context_get_preedit_string(aContext, &preedit_string, &feedback_list,
2618 &cursor_pos_in_chars);
2619 if (!preedit_string || !*preedit_string) {
2620 if (!aCompositionString.IsEmpty()) {
2621 MOZ_LOG(gIMELog, LogLevel::Error,
2622 ("0x%p CreateTextRangeArray(), FAILED, due to "
2623 "preedit_string is null",
2624 this));
2626 pango_attr_list_unref(feedback_list);
2627 g_free(preedit_string);
2628 return textRangeArray.forget();
2631 // Convert caret offset from offset in characters to offset in UTF-16
2632 // string. If we couldn't proper offset in UTF-16 string, we should
2633 // assume that the caret is at the end of the composition string.
2634 uint32_t caretOffsetInUTF16 = aCompositionString.Length();
2635 if (NS_WARN_IF(cursor_pos_in_chars < 0)) {
2636 // Note that this case is undocumented. We should assume that the
2637 // caret is at the end of the composition string.
2638 } else if (cursor_pos_in_chars == 0) {
2639 caretOffsetInUTF16 = 0;
2640 } else {
2641 gchar* charAfterCaret =
2642 g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars);
2643 if (NS_WARN_IF(!charAfterCaret)) {
2644 MOZ_LOG(gIMELog, LogLevel::Warning,
2645 ("0x%p CreateTextRangeArray(), failed to get UTF-8 "
2646 "string before the caret (cursor_pos_in_chars=%d)",
2647 this, cursor_pos_in_chars));
2648 } else {
2649 glong caretOffset = 0;
2650 gunichar2* utf16StrBeforeCaret =
2651 g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string,
2652 nullptr, &caretOffset, nullptr);
2653 if (NS_WARN_IF(!utf16StrBeforeCaret) || NS_WARN_IF(caretOffset < 0)) {
2654 MOZ_LOG(gIMELog, LogLevel::Warning,
2655 ("0x%p CreateTextRangeArray(), WARNING, failed to "
2656 "convert to UTF-16 string before the caret "
2657 "(cursor_pos_in_chars=%d, caretOffset=%ld)",
2658 this, cursor_pos_in_chars, caretOffset));
2659 } else {
2660 caretOffsetInUTF16 = static_cast<uint32_t>(caretOffset);
2661 uint32_t compositionStringLength = aCompositionString.Length();
2662 if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) {
2663 MOZ_LOG(gIMELog, LogLevel::Warning,
2664 ("0x%p CreateTextRangeArray(), WARNING, "
2665 "caretOffsetInUTF16=%u is larger than "
2666 "compositionStringLength=%u",
2667 this, caretOffsetInUTF16, compositionStringLength));
2668 caretOffsetInUTF16 = compositionStringLength;
2671 if (utf16StrBeforeCaret) {
2672 g_free(utf16StrBeforeCaret);
2677 PangoAttrIterator* iter;
2678 iter = pango_attr_list_get_iterator(feedback_list);
2679 if (!iter) {
2680 MOZ_LOG(gIMELog, LogLevel::Error,
2681 ("0x%p CreateTextRangeArray(), FAILED, iterator couldn't "
2682 "be allocated",
2683 this));
2684 pango_attr_list_unref(feedback_list);
2685 g_free(preedit_string);
2686 return textRangeArray.forget();
2689 uint32_t minOffsetOfClauses = aCompositionString.Length();
2690 uint32_t maxOffsetOfClauses = 0;
2691 do {
2692 TextRange range;
2693 if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) {
2694 continue;
2696 MOZ_ASSERT(range.Length());
2697 minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset);
2698 maxOffsetOfClauses = std::max(maxOffsetOfClauses, range.mEndOffset);
2699 textRangeArray->AppendElement(range);
2700 } while (pango_attr_iterator_next(iter));
2702 // If the IME doesn't define clause from the start of the composition,
2703 // we should insert dummy clause information since TextRangeArray assumes
2704 // that there must be a clause whose start is 0 when there is one or
2705 // more clauses.
2706 if (minOffsetOfClauses) {
2707 TextRange dummyClause;
2708 dummyClause.mStartOffset = 0;
2709 dummyClause.mEndOffset = minOffsetOfClauses;
2710 dummyClause.mRangeType = TextRangeType::eRawClause;
2711 textRangeArray->InsertElementAt(0, dummyClause);
2712 maxOffsetOfClauses = std::max(maxOffsetOfClauses, dummyClause.mEndOffset);
2713 MOZ_LOG(gIMELog, LogLevel::Warning,
2714 ("0x%p CreateTextRangeArray(), inserting a dummy clause "
2715 "at the beginning of the composition string mStartOffset=%u, "
2716 "mEndOffset=%u, mRangeType=%s",
2717 this, dummyClause.mStartOffset, dummyClause.mEndOffset,
2718 ToChar(dummyClause.mRangeType)));
2721 // If the IME doesn't define clause at end of the composition, we should
2722 // insert dummy clause information since TextRangeArray assumes that there
2723 // must be a clase whose end is the length of the composition string when
2724 // there is one or more clauses.
2725 if (!textRangeArray->IsEmpty() &&
2726 maxOffsetOfClauses < aCompositionString.Length()) {
2727 TextRange dummyClause;
2728 dummyClause.mStartOffset = maxOffsetOfClauses;
2729 dummyClause.mEndOffset = aCompositionString.Length();
2730 dummyClause.mRangeType = TextRangeType::eRawClause;
2731 textRangeArray->AppendElement(dummyClause);
2732 MOZ_LOG(gIMELog, LogLevel::Warning,
2733 ("0x%p CreateTextRangeArray(), inserting a dummy clause "
2734 "at the end of the composition string mStartOffset=%u, "
2735 "mEndOffset=%u, mRangeType=%s",
2736 this, dummyClause.mStartOffset, dummyClause.mEndOffset,
2737 ToChar(dummyClause.mRangeType)));
2740 TextRange range;
2741 range.mStartOffset = range.mEndOffset = caretOffsetInUTF16;
2742 range.mRangeType = TextRangeType::eCaret;
2743 textRangeArray->AppendElement(range);
2744 MOZ_LOG(
2745 gIMELog, LogLevel::Debug,
2746 ("0x%p CreateTextRangeArray(), mStartOffset=%u, "
2747 "mEndOffset=%u, mRangeType=%s",
2748 this, range.mStartOffset, range.mEndOffset, ToChar(range.mRangeType)));
2750 pango_attr_iterator_destroy(iter);
2751 pango_attr_list_unref(feedback_list);
2752 g_free(preedit_string);
2754 return textRangeArray.forget();
2757 /* static */
2758 nscolor IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor) {
2759 PangoColor& pangoColor = aPangoAttrColor->color;
2760 uint8_t r = pangoColor.red / 0x100;
2761 uint8_t g = pangoColor.green / 0x100;
2762 uint8_t b = pangoColor.blue / 0x100;
2763 return NS_RGB(r, g, b);
2766 bool IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter,
2767 const gchar* aUTF8CompositionString,
2768 uint32_t aUTF16CaretOffset,
2769 TextRange& aTextRange) const {
2770 // Set the range offsets in UTF-16 string.
2771 gint utf8ClauseStart, utf8ClauseEnd;
2772 pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd);
2773 if (utf8ClauseStart == utf8ClauseEnd) {
2774 MOZ_LOG(gIMELog, LogLevel::Error,
2775 ("0x%p SetTextRange(), FAILED, due to collapsed range", this));
2776 return false;
2779 if (!utf8ClauseStart) {
2780 aTextRange.mStartOffset = 0;
2781 } else {
2782 glong utf16PreviousClausesLength;
2783 gunichar2* utf16PreviousClausesString =
2784 g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr,
2785 &utf16PreviousClausesLength, nullptr);
2787 if (NS_WARN_IF(!utf16PreviousClausesString)) {
2788 MOZ_LOG(gIMELog, LogLevel::Error,
2789 ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
2790 "failure (retrieving previous string of current clause)",
2791 this));
2792 return false;
2795 aTextRange.mStartOffset = utf16PreviousClausesLength;
2796 g_free(utf16PreviousClausesString);
2799 glong utf16CurrentClauseLength;
2800 gunichar2* utf16CurrentClauseString = g_utf8_to_utf16(
2801 aUTF8CompositionString + utf8ClauseStart, utf8ClauseEnd - utf8ClauseStart,
2802 nullptr, &utf16CurrentClauseLength, nullptr);
2804 if (NS_WARN_IF(!utf16CurrentClauseString)) {
2805 MOZ_LOG(gIMELog, LogLevel::Error,
2806 ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
2807 "failure (retrieving current clause)",
2808 this));
2809 return false;
2812 // iBus Chewing IME tells us that there is an empty clause at the end of
2813 // the composition string but we should ignore it since our code doesn't
2814 // assume that there is an empty clause.
2815 if (!utf16CurrentClauseLength) {
2816 MOZ_LOG(gIMELog, LogLevel::Warning,
2817 ("0x%p SetTextRange(), FAILED, due to current clause length "
2818 "is 0",
2819 this));
2820 return false;
2823 aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength;
2824 g_free(utf16CurrentClauseString);
2825 utf16CurrentClauseString = nullptr;
2827 // Set styles
2828 TextRangeStyle& style = aTextRange.mRangeStyle;
2830 // Underline
2831 PangoAttrInt* attrUnderline = reinterpret_cast<PangoAttrInt*>(
2832 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE));
2833 if (attrUnderline) {
2834 switch (attrUnderline->value) {
2835 case PANGO_UNDERLINE_NONE:
2836 style.mLineStyle = TextRangeStyle::LineStyle::None;
2837 break;
2838 case PANGO_UNDERLINE_DOUBLE:
2839 style.mLineStyle = TextRangeStyle::LineStyle::Double;
2840 break;
2841 case PANGO_UNDERLINE_ERROR:
2842 style.mLineStyle = TextRangeStyle::LineStyle::Wavy;
2843 break;
2844 case PANGO_UNDERLINE_SINGLE:
2845 case PANGO_UNDERLINE_LOW:
2846 style.mLineStyle = TextRangeStyle::LineStyle::Solid;
2847 break;
2848 default:
2849 MOZ_LOG(gIMELog, LogLevel::Warning,
2850 ("0x%p SetTextRange(), retrieved unknown underline "
2851 "style: %d",
2852 this, attrUnderline->value));
2853 style.mLineStyle = TextRangeStyle::LineStyle::Solid;
2854 break;
2856 style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
2858 // Underline color
2859 PangoAttrColor* attrUnderlineColor = reinterpret_cast<PangoAttrColor*>(
2860 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE_COLOR));
2861 if (attrUnderlineColor) {
2862 style.mUnderlineColor = ToNscolor(attrUnderlineColor);
2863 style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR;
2865 } else {
2866 style.mLineStyle = TextRangeStyle::LineStyle::None;
2867 style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
2870 // Don't set colors if they are not specified. They should be computed by
2871 // textframe if only one of the colors are specified.
2873 // Foreground color (text color)
2874 PangoAttrColor* attrForeground = reinterpret_cast<PangoAttrColor*>(
2875 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND));
2876 if (attrForeground) {
2877 style.mForegroundColor = ToNscolor(attrForeground);
2878 style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR;
2881 // Background color
2882 PangoAttrColor* attrBackground = reinterpret_cast<PangoAttrColor*>(
2883 pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND));
2884 if (attrBackground) {
2885 style.mBackgroundColor = ToNscolor(attrBackground);
2886 style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR;
2890 * We need to judge the meaning of the clause for a11y. Before we support
2891 * IME specific composition string style, we used following rules:
2893 * 1: If attrUnderline and attrForground are specified, we assumed the
2894 * clause is TextRangeType::eSelectedClause.
2895 * 2: If only attrUnderline is specified, we assumed the clause is
2896 * TextRangeType::eConvertedClause.
2897 * 3: If only attrForground is specified, we assumed the clause is
2898 * TextRangeType::eSelectedRawClause.
2899 * 4: If neither attrUnderline nor attrForeground is specified, we assumed
2900 * the clause is TextRangeType::eRawClause.
2902 * However, this rules are odd since there can be two or more selected
2903 * clauses. Additionally, our old rules caused that IME developers/users
2904 * cannot specify composition string style as they want.
2906 * So, we shouldn't guess the meaning from its visual style.
2909 // If the range covers whole of composition string and the caret is at
2910 // the end of the composition string, the range is probably not converted.
2911 if (!utf8ClauseStart &&
2912 utf8ClauseEnd == static_cast<gint>(strlen(aUTF8CompositionString)) &&
2913 aTextRange.mEndOffset == aUTF16CaretOffset) {
2914 aTextRange.mRangeType = TextRangeType::eRawClause;
2916 // Typically, the caret is set at the start of the selected clause.
2917 // So, if the caret is in the clause, we can assume that the clause is
2918 // selected.
2919 else if (aTextRange.mStartOffset <= aUTF16CaretOffset &&
2920 aTextRange.mEndOffset > aUTF16CaretOffset) {
2921 aTextRange.mRangeType = TextRangeType::eSelectedClause;
2923 // Otherwise, we should assume that the clause is converted but not
2924 // selected.
2925 else {
2926 aTextRange.mRangeType = TextRangeType::eConvertedClause;
2929 MOZ_LOG(gIMELog, LogLevel::Debug,
2930 ("0x%p SetTextRange(), succeeded, aTextRange= { "
2931 "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }",
2932 this, aTextRange.mStartOffset, aTextRange.mEndOffset,
2933 ToChar(aTextRange.mRangeType),
2934 GetTextRangeStyleText(aTextRange.mRangeStyle).get()));
2936 return true;
2939 void IMContextWrapper::SetCursorPosition(GtkIMContext* aContext) {
2940 MOZ_LOG(
2941 gIMELog, LogLevel::Info,
2942 ("0x%p SetCursorPosition(aContext=0x%p), "
2943 "mCompositionTargetRange={ mOffset=%u, mLength=%u }, "
2944 "mContentSelection=%s",
2945 this, aContext, mCompositionTargetRange.mOffset,
2946 mCompositionTargetRange.mLength, ToString(mContentSelection).c_str()));
2948 bool useCaret = false;
2949 if (!mCompositionTargetRange.IsValid()) {
2950 if (mContentSelection.isNothing()) {
2951 MOZ_LOG(gIMELog, LogLevel::Error,
2952 ("0x%p SetCursorPosition(), FAILED, "
2953 "mCompositionTargetRange and mContentSelection are invalid",
2954 this));
2955 return;
2957 if (!mContentSelection->HasRange()) {
2958 MOZ_LOG(gIMELog, LogLevel::Warning,
2959 ("0x%p SetCursorPosition(), FAILED, "
2960 "mCompositionTargetRange is invalid and there is no selection",
2961 this));
2962 return;
2964 useCaret = true;
2967 if (!mLastFocusedWindow) {
2968 MOZ_LOG(gIMELog, LogLevel::Error,
2969 ("0x%p SetCursorPosition(), FAILED, due to no focused "
2970 "window",
2971 this));
2972 return;
2975 if (MOZ_UNLIKELY(!aContext)) {
2976 MOZ_LOG(gIMELog, LogLevel::Error,
2977 ("0x%p SetCursorPosition(), FAILED, due to no context", this));
2978 return;
2981 WidgetQueryContentEvent queryCaretOrTextRectEvent(
2982 true, useCaret ? eQueryCaretRect : eQueryTextRect, mLastFocusedWindow);
2983 if (useCaret) {
2984 queryCaretOrTextRectEvent.InitForQueryCaretRect(
2985 mContentSelection->OffsetAndDataRef().StartOffset());
2986 } else {
2987 if (mContentSelection->WritingModeRef().IsVertical()) {
2988 // For preventing the candidate window to overlap the target
2989 // clause, we should set fake (typically, very tall) caret rect.
2990 uint32_t length =
2991 mCompositionTargetRange.mLength ? mCompositionTargetRange.mLength : 1;
2992 queryCaretOrTextRectEvent.InitForQueryTextRect(
2993 mCompositionTargetRange.mOffset, length);
2994 } else {
2995 queryCaretOrTextRectEvent.InitForQueryTextRect(
2996 mCompositionTargetRange.mOffset, 1);
2999 nsEventStatus status;
3000 mLastFocusedWindow->DispatchEvent(&queryCaretOrTextRectEvent, status);
3001 if (queryCaretOrTextRectEvent.Failed()) {
3002 MOZ_LOG(gIMELog, LogLevel::Error,
3003 ("0x%p SetCursorPosition(), FAILED, %s was failed", this,
3004 useCaret ? "eQueryCaretRect" : "eQueryTextRect"));
3005 return;
3008 nsWindow* rootWindow =
3009 static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());
3011 // Get the position of the rootWindow in screen.
3012 LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset();
3014 // Get the position of IM context owner window in screen.
3015 LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset();
3017 // Compute the caret position in the IM owner window.
3018 LayoutDeviceIntRect rect =
3019 queryCaretOrTextRectEvent.mReply->mRect + root - owner;
3020 rect.width = 0;
3021 GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect);
3023 gtk_im_context_set_cursor_location(aContext, &area);
3026 nsresult IMContextWrapper::GetCurrentParagraph(nsAString& aText,
3027 uint32_t& aCursorPos) {
3028 MOZ_LOG(gIMELog, LogLevel::Info,
3029 ("0x%p GetCurrentParagraph(), mCompositionState=%s", this,
3030 GetCompositionStateName()));
3032 if (!mLastFocusedWindow) {
3033 MOZ_LOG(gIMELog, LogLevel::Error,
3034 ("0x%p GetCurrentParagraph(), FAILED, there are no "
3035 "focused window in this module",
3036 this));
3037 return NS_ERROR_NULL_POINTER;
3040 nsEventStatus status;
3042 uint32_t selOffset = mCompositionStart;
3043 uint32_t selLength = mSelectedStringRemovedByComposition.Length();
3045 // If focused editor doesn't have composition string, we should use
3046 // current selection.
3047 if (!EditorHasCompositionString()) {
3048 // Query cursor position & selection
3049 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
3050 MOZ_LOG(gIMELog, LogLevel::Error,
3051 ("0x%p GetCurrentParagraph(), FAILED, due to no "
3052 "valid selection information",
3053 this));
3054 return NS_ERROR_FAILURE;
3057 if (mContentSelection.isSome() && mContentSelection->HasRange()) {
3058 selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
3059 selLength = mContentSelection->OffsetAndDataRef().Length();
3060 } else {
3061 // If there is no range, let's get all text instead...
3062 selOffset = 0u;
3063 selLength = INT32_MAX; // TODO: Change to UINT32_MAX, but see below
3067 MOZ_LOG(gIMELog, LogLevel::Debug,
3068 ("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u", this,
3069 selOffset, selLength));
3071 // XXX nsString::Find and nsString::RFind take int32_t for offset, so,
3072 // we cannot support this request when the current offset is larger
3073 // than INT32_MAX.
3074 if (selOffset > INT32_MAX || selLength > INT32_MAX ||
3075 selOffset + selLength > INT32_MAX) {
3076 MOZ_LOG(gIMELog, LogLevel::Error,
3077 ("0x%p GetCurrentParagraph(), FAILED, The selection is "
3078 "out of range",
3079 this));
3080 return NS_ERROR_FAILURE;
3083 // Get all text contents of the focused editor
3084 WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
3085 mLastFocusedWindow);
3086 queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
3087 mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
3088 if (NS_WARN_IF(queryTextContentEvent.Failed())) {
3089 return NS_ERROR_FAILURE;
3092 if (selOffset + selLength > queryTextContentEvent.mReply->DataLength()) {
3093 MOZ_LOG(gIMELog, LogLevel::Error,
3094 ("0x%p GetCurrentParagraph(), FAILED, The selection is "
3095 "invalid, queryTextContentEvent={ mReply=%s }",
3096 this, ToString(queryTextContentEvent.mReply).c_str()));
3097 return NS_ERROR_FAILURE;
3100 // Remove composing string and restore the selected string because
3101 // GtkEntry doesn't remove selected string until committing, however,
3102 // our editor does it. We should emulate the behavior for IME.
3103 nsAutoString textContent(queryTextContentEvent.mReply->DataRef());
3104 if (EditorHasCompositionString() &&
3105 mDispatchedCompositionString != mSelectedStringRemovedByComposition) {
3106 textContent.Replace(mCompositionStart,
3107 mDispatchedCompositionString.Length(),
3108 mSelectedStringRemovedByComposition);
3111 // Get only the focused paragraph, by looking for newlines
3112 int32_t parStart = 0;
3113 if (selOffset > 0) {
3114 parStart = Substring(textContent, 0, selOffset - 1).RFind(u"\n") + 1;
3116 int32_t parEnd = textContent.Find(u"\n", selOffset + selLength);
3117 if (parEnd < 0) {
3118 parEnd = textContent.Length();
3120 aText = nsDependentSubstring(textContent, parStart, parEnd - parStart);
3121 aCursorPos = selOffset - uint32_t(parStart);
3123 MOZ_LOG(
3124 gIMELog, LogLevel::Debug,
3125 ("0x%p GetCurrentParagraph(), succeeded, aText=%s, "
3126 "aText.Length()=%zu, aCursorPos=%u",
3127 this, NS_ConvertUTF16toUTF8(aText).get(), aText.Length(), aCursorPos));
3129 return NS_OK;
3132 nsresult IMContextWrapper::DeleteText(GtkIMContext* aContext, int32_t aOffset,
3133 uint32_t aNChars) {
3134 MOZ_LOG(gIMELog, LogLevel::Info,
3135 ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), "
3136 "mCompositionState=%s",
3137 this, aContext, aOffset, aNChars, GetCompositionStateName()));
3139 if (!mLastFocusedWindow) {
3140 MOZ_LOG(gIMELog, LogLevel::Error,
3141 ("0x%p DeleteText(), FAILED, there are no focused window "
3142 "in this module",
3143 this));
3144 return NS_ERROR_NULL_POINTER;
3147 if (!aNChars) {
3148 MOZ_LOG(gIMELog, LogLevel::Error,
3149 ("0x%p DeleteText(), FAILED, aNChars must not be zero", this));
3150 return NS_ERROR_INVALID_ARG;
3153 RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
3154 nsEventStatus status;
3156 // First, we should cancel current composition because editor cannot
3157 // handle changing selection and deleting text.
3158 uint32_t selOffset;
3159 bool wasComposing = IsComposing();
3160 bool editorHadCompositionString = EditorHasCompositionString();
3161 if (wasComposing) {
3162 selOffset = mCompositionStart;
3163 if (!DispatchCompositionCommitEvent(aContext,
3164 &mSelectedStringRemovedByComposition)) {
3165 MOZ_LOG(gIMELog, LogLevel::Error,
3166 ("0x%p DeleteText(), FAILED, quitting from DeletText", this));
3167 return NS_ERROR_FAILURE;
3169 } else {
3170 if (NS_WARN_IF(!EnsureToCacheContentSelection())) {
3171 MOZ_LOG(gIMELog, LogLevel::Error,
3172 ("0x%p DeleteText(), FAILED, due to no valid selection "
3173 "information",
3174 this));
3175 return NS_ERROR_FAILURE;
3177 if (!mContentSelection->HasRange()) {
3178 MOZ_LOG(gIMELog, LogLevel::Debug,
3179 ("0x%p DeleteText(), does nothing, due to no selection range",
3180 this));
3181 return NS_OK;
3183 selOffset = mContentSelection->OffsetAndDataRef().StartOffset();
3186 // Get all text contents of the focused editor
3187 WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
3188 mLastFocusedWindow);
3189 queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
3190 mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
3191 if (NS_WARN_IF(queryTextContentEvent.Failed())) {
3192 return NS_ERROR_FAILURE;
3194 if (queryTextContentEvent.mReply->IsDataEmpty()) {
3195 MOZ_LOG(gIMELog, LogLevel::Error,
3196 ("0x%p DeleteText(), FAILED, there is no contents", this));
3197 return NS_ERROR_FAILURE;
3200 NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(
3201 queryTextContentEvent.mReply->DataRef(), 0, selOffset));
3202 glong offsetInUTF8Characters =
3203 g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset;
3204 if (offsetInUTF8Characters < 0) {
3205 MOZ_LOG(gIMELog, LogLevel::Error,
3206 ("0x%p DeleteText(), FAILED, aOffset is too small for "
3207 "current cursor pos (computed offset: %ld)",
3208 this, offsetInUTF8Characters));
3209 return NS_ERROR_FAILURE;
3212 AppendUTF16toUTF8(
3213 nsDependentSubstring(queryTextContentEvent.mReply->DataRef(), selOffset),
3214 utf8Str);
3215 glong countOfCharactersInUTF8 =
3216 g_utf8_strlen(utf8Str.get(), utf8Str.Length());
3217 glong endInUTF8Characters = offsetInUTF8Characters + aNChars;
3218 if (countOfCharactersInUTF8 < endInUTF8Characters) {
3219 MOZ_LOG(gIMELog, LogLevel::Error,
3220 ("0x%p DeleteText(), FAILED, aNChars is too large for "
3221 "current contents (content length: %ld, computed end offset: %ld)",
3222 this, countOfCharactersInUTF8, endInUTF8Characters));
3223 return NS_ERROR_FAILURE;
3226 gchar* charAtOffset =
3227 g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters);
3228 gchar* charAtEnd =
3229 g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters);
3231 // Set selection to delete
3232 WidgetSelectionEvent selectionEvent(true, eSetSelection, mLastFocusedWindow);
3234 nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0,
3235 charAtOffset - utf8Str.get());
3236 selectionEvent.mOffset = NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length();
3238 nsDependentCSubstring utf8DeletingStr(utf8Str, utf8StrBeforeOffset.Length(),
3239 charAtEnd - charAtOffset);
3240 selectionEvent.mLength = NS_ConvertUTF8toUTF16(utf8DeletingStr).Length();
3242 selectionEvent.mReversed = false;
3243 selectionEvent.mExpandToClusterBoundary = false;
3244 lastFocusedWindow->DispatchEvent(&selectionEvent, status);
3246 if (!selectionEvent.mSucceeded || lastFocusedWindow != mLastFocusedWindow ||
3247 lastFocusedWindow->Destroyed()) {
3248 MOZ_LOG(gIMELog, LogLevel::Error,
3249 ("0x%p DeleteText(), FAILED, setting selection caused "
3250 "focus change or window destroyed",
3251 this));
3252 return NS_ERROR_FAILURE;
3255 // If this deleting text caused by a key press, we need to dispatch
3256 // eKeyDown or eKeyUp before dispatching eContentCommandDelete event.
3257 if (!MaybeDispatchKeyEventAsProcessedByIME(eContentCommandDelete)) {
3258 MOZ_LOG(gIMELog, LogLevel::Warning,
3259 ("0x%p DeleteText(), Warning, "
3260 "MaybeDispatchKeyEventAsProcessedByIME() returned false",
3261 this));
3262 return NS_ERROR_FAILURE;
3265 // Delete the selection
3266 WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete,
3267 mLastFocusedWindow);
3268 mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
3270 if (!contentCommandEvent.mSucceeded ||
3271 lastFocusedWindow != mLastFocusedWindow ||
3272 lastFocusedWindow->Destroyed()) {
3273 MOZ_LOG(gIMELog, LogLevel::Error,
3274 ("0x%p DeleteText(), FAILED, deleting the selection caused "
3275 "focus change or window destroyed",
3276 this));
3277 return NS_ERROR_FAILURE;
3280 if (!wasComposing) {
3281 return NS_OK;
3284 // Restore the composition at new caret position.
3285 if (!DispatchCompositionStart(aContext)) {
3286 MOZ_LOG(
3287 gIMELog, LogLevel::Error,
3288 ("0x%p DeleteText(), FAILED, resterting composition start", this));
3289 return NS_ERROR_FAILURE;
3292 if (!editorHadCompositionString) {
3293 return NS_OK;
3296 nsAutoString compositionString;
3297 GetCompositionString(aContext, compositionString);
3298 if (!DispatchCompositionChangeEvent(aContext, compositionString)) {
3299 MOZ_LOG(
3300 gIMELog, LogLevel::Error,
3301 ("0x%p DeleteText(), FAILED, restoring composition string", this));
3302 return NS_ERROR_FAILURE;
3305 return NS_OK;
3308 bool IMContextWrapper::EnsureToCacheContentSelection(
3309 nsAString* aSelectedString) {
3310 if (aSelectedString) {
3311 aSelectedString->Truncate();
3314 if (mContentSelection.isSome()) {
3315 if (mContentSelection->HasRange() && aSelectedString) {
3316 aSelectedString->Assign(mContentSelection->OffsetAndDataRef().DataRef());
3318 return true;
3321 RefPtr<nsWindow> dispatcherWindow =
3322 mLastFocusedWindow ? mLastFocusedWindow : mOwnerWindow;
3323 if (NS_WARN_IF(!dispatcherWindow)) {
3324 MOZ_LOG(gIMELog, LogLevel::Error,
3325 ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
3326 "no focused window",
3327 this));
3328 return false;
3331 nsEventStatus status;
3332 WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
3333 dispatcherWindow);
3334 dispatcherWindow->DispatchEvent(&querySelectedTextEvent, status);
3335 if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
3336 MOZ_LOG(gIMELog, LogLevel::Error,
3337 ("0x%p EnsureToCacheContentSelection(), FAILED, due to "
3338 "failure of query selection event",
3339 this));
3340 return false;
3343 mContentSelection = Some(ContentSelection(querySelectedTextEvent));
3344 if (mContentSelection->HasRange()) {
3345 if (!mContentSelection->OffsetAndDataRef().IsDataEmpty() &&
3346 aSelectedString) {
3347 aSelectedString->Assign(querySelectedTextEvent.mReply->DataRef());
3351 MOZ_LOG(
3352 gIMELog, LogLevel::Debug,
3353 ("0x%p EnsureToCacheContentSelection(), Succeeded, mContentSelection=%s",
3354 this, ToString(mContentSelection).c_str()));
3355 return true;
3358 } // namespace widget
3359 } // namespace mozilla