summaryrefslogtreecommitdiffstats
path: root/widget/gtk/IMContextWrapper.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/gtk/IMContextWrapper.cpp')
-rw-r--r--widget/gtk/IMContextWrapper.cpp2359
1 files changed, 2359 insertions, 0 deletions
diff --git a/widget/gtk/IMContextWrapper.cpp b/widget/gtk/IMContextWrapper.cpp
new file mode 100644
index 000000000..58d7a3681
--- /dev/null
+++ b/widget/gtk/IMContextWrapper.cpp
@@ -0,0 +1,2359 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=4 et sw=4 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Logging.h"
+#include "prtime.h"
+
+#include "IMContextWrapper.h"
+#include "nsGtkKeyUtils.h"
+#include "nsWindow.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MiscEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "WritingModes.h"
+
+namespace mozilla {
+namespace widget {
+
+LazyLogModule gGtkIMLog("nsGtkIMModuleWidgets");
+
+static inline const char*
+ToChar(bool aBool)
+{
+ return aBool ? "true" : "false";
+}
+
+static const char*
+GetEnabledStateName(uint32_t aState)
+{
+ switch (aState) {
+ case IMEState::DISABLED:
+ return "DISABLED";
+ case IMEState::ENABLED:
+ return "ENABLED";
+ case IMEState::PASSWORD:
+ return "PASSWORD";
+ case IMEState::PLUGIN:
+ return "PLUG_IN";
+ default:
+ return "UNKNOWN ENABLED STATUS!!";
+ }
+}
+
+static const char*
+GetEventType(GdkEventKey* aKeyEvent)
+{
+ switch (aKeyEvent->type) {
+ case GDK_KEY_PRESS:
+ return "GDK_KEY_PRESS";
+ case GDK_KEY_RELEASE:
+ return "GDK_KEY_RELEASE";
+ default:
+ return "Unknown";
+ }
+}
+
+class GetWritingModeName : public nsAutoCString
+{
+public:
+ explicit GetWritingModeName(const WritingMode& aWritingMode)
+ {
+ if (!aWritingMode.IsVertical()) {
+ AssignLiteral("Horizontal");
+ return;
+ }
+ if (aWritingMode.IsVerticalLR()) {
+ AssignLiteral("Vertical (LTR)");
+ return;
+ }
+ AssignLiteral("Vertical (RTL)");
+ }
+ virtual ~GetWritingModeName() {}
+};
+
+class GetTextRangeStyleText final : public nsAutoCString
+{
+public:
+ explicit GetTextRangeStyleText(const TextRangeStyle& aStyle)
+ {
+ if (!aStyle.IsDefined()) {
+ AssignLiteral("{ IsDefined()=false }");
+ return;
+ }
+
+ if (aStyle.IsLineStyleDefined()) {
+ AppendLiteral("{ mLineStyle=");
+ AppendLineStyle(aStyle.mLineStyle);
+ if (aStyle.IsUnderlineColorDefined()) {
+ AppendLiteral(", mUnderlineColor=");
+ AppendColor(aStyle.mUnderlineColor);
+ } else {
+ AppendLiteral(", IsUnderlineColorDefined=false");
+ }
+ } else {
+ AppendLiteral("{ IsLineStyleDefined()=false");
+ }
+
+ if (aStyle.IsForegroundColorDefined()) {
+ AppendLiteral(", mForegroundColor=");
+ AppendColor(aStyle.mForegroundColor);
+ } else {
+ AppendLiteral(", IsForegroundColorDefined()=false");
+ }
+
+ if (aStyle.IsBackgroundColorDefined()) {
+ AppendLiteral(", mBackgroundColor=");
+ AppendColor(aStyle.mBackgroundColor);
+ } else {
+ AppendLiteral(", IsBackgroundColorDefined()=false");
+ }
+
+ AppendLiteral(" }");
+ }
+ void AppendLineStyle(uint8_t aLineStyle)
+ {
+ switch (aLineStyle) {
+ case TextRangeStyle::LINESTYLE_NONE:
+ AppendLiteral("LINESTYLE_NONE");
+ break;
+ case TextRangeStyle::LINESTYLE_SOLID:
+ AppendLiteral("LINESTYLE_SOLID");
+ break;
+ case TextRangeStyle::LINESTYLE_DOTTED:
+ AppendLiteral("LINESTYLE_DOTTED");
+ break;
+ case TextRangeStyle::LINESTYLE_DASHED:
+ AppendLiteral("LINESTYLE_DASHED");
+ break;
+ case TextRangeStyle::LINESTYLE_DOUBLE:
+ AppendLiteral("LINESTYLE_DOUBLE");
+ break;
+ case TextRangeStyle::LINESTYLE_WAVY:
+ AppendLiteral("LINESTYLE_WAVY");
+ break;
+ default:
+ AppendPrintf("Invalid(0x%02X)", aLineStyle);
+ break;
+ }
+ }
+ void AppendColor(nscolor aColor)
+ {
+ AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }",
+ NS_GET_R(aColor), NS_GET_G(aColor), NS_GET_B(aColor),
+ NS_GET_A(aColor));
+ }
+ virtual ~GetTextRangeStyleText() {};
+};
+
+const static bool kUseSimpleContextDefault = MOZ_WIDGET_GTK == 2;
+
+/******************************************************************************
+ * IMContextWrapper
+ ******************************************************************************/
+
+IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr;
+bool IMContextWrapper::sUseSimpleContext;
+
+NS_IMPL_ISUPPORTS(IMContextWrapper,
+ TextEventDispatcherListener,
+ nsISupportsWeakReference)
+
+IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow)
+ : mOwnerWindow(aOwnerWindow)
+ , mLastFocusedWindow(nullptr)
+ , mContext(nullptr)
+ , mSimpleContext(nullptr)
+ , mDummyContext(nullptr)
+ , mComposingContext(nullptr)
+ , mCompositionStart(UINT32_MAX)
+ , mProcessingKeyEvent(nullptr)
+ , mCompositionState(eCompositionState_NotComposing)
+ , mIsIMFocused(false)
+ , mIsDeletingSurrounding(false)
+ , mLayoutChanged(false)
+ , mSetCursorPositionOnKeyEvent(true)
+ , mPendingResettingIMContext(false)
+ , mRetrieveSurroundingSignalReceived(false)
+{
+ static bool sFirstInstance = true;
+ if (sFirstInstance) {
+ sFirstInstance = false;
+ sUseSimpleContext =
+ Preferences::GetBool(
+ "intl.ime.use_simple_context_on_password_field",
+ kUseSimpleContextDefault);
+ }
+ Init();
+}
+
+void
+IMContextWrapper::Init()
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p Init(), mOwnerWindow=0x%p",
+ this, mOwnerWindow));
+
+ MozContainer* container = mOwnerWindow->GetMozContainer();
+ NS_PRECONDITION(container, "container is null");
+ GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container));
+
+ // NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
+ // So, we don't need to check the result.
+
+ // Normal context.
+ mContext = gtk_im_multicontext_new();
+ gtk_im_context_set_client_window(mContext, gdkWindow);
+ g_signal_connect(mContext, "preedit_changed",
+ G_CALLBACK(IMContextWrapper::OnChangeCompositionCallback), this);
+ g_signal_connect(mContext, "retrieve_surrounding",
+ G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback), this);
+ g_signal_connect(mContext, "delete_surrounding",
+ G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback), this);
+ g_signal_connect(mContext, "commit",
+ G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback), this);
+ g_signal_connect(mContext, "preedit_start",
+ G_CALLBACK(IMContextWrapper::OnStartCompositionCallback), this);
+ g_signal_connect(mContext, "preedit_end",
+ G_CALLBACK(IMContextWrapper::OnEndCompositionCallback), this);
+
+ // Simple context
+ if (sUseSimpleContext) {
+ mSimpleContext = gtk_im_context_simple_new();
+ gtk_im_context_set_client_window(mSimpleContext, gdkWindow);
+ g_signal_connect(mSimpleContext, "preedit_changed",
+ G_CALLBACK(&IMContextWrapper::OnChangeCompositionCallback),
+ this);
+ g_signal_connect(mSimpleContext, "retrieve_surrounding",
+ G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback),
+ this);
+ g_signal_connect(mSimpleContext, "delete_surrounding",
+ G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback),
+ this);
+ g_signal_connect(mSimpleContext, "commit",
+ G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback),
+ this);
+ g_signal_connect(mSimpleContext, "preedit_start",
+ G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
+ this);
+ g_signal_connect(mSimpleContext, "preedit_end",
+ G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
+ this);
+ }
+
+ // Dummy context
+ mDummyContext = gtk_im_multicontext_new();
+ gtk_im_context_set_client_window(mDummyContext, gdkWindow);
+}
+
+IMContextWrapper::~IMContextWrapper()
+{
+ if (this == sLastFocusedContext) {
+ sLastFocusedContext = nullptr;
+ }
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p ~IMContextWrapper()", this));
+}
+
+NS_IMETHODIMP
+IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+ const IMENotification& aNotification)
+{
+ switch (aNotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ case REQUEST_TO_CANCEL_COMPOSITION: {
+ nsWindow* window =
+ static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
+ return EndIMEComposition(window);
+ }
+ case NOTIFY_IME_OF_FOCUS:
+ OnFocusChangeInGecko(true);
+ return NS_OK;
+ case NOTIFY_IME_OF_BLUR:
+ OnFocusChangeInGecko(false);
+ return NS_OK;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ OnLayoutChange();
+ return NS_OK;
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ OnUpdateComposition();
+ return NS_OK;
+ case NOTIFY_IME_OF_SELECTION_CHANGE: {
+ nsWindow* window =
+ static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
+ OnSelectionChange(window, aNotification);
+ return NS_OK;
+ }
+ default:
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+NS_IMETHODIMP_(void)
+IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher)
+{
+ // XXX When input transaction is being stolen by add-on, what should we do?
+}
+
+NS_IMETHODIMP_(void)
+IMContextWrapper::WillDispatchKeyboardEvent(
+ TextEventDispatcher* aTextEventDispatcher,
+ WidgetKeyboardEvent& aKeyboardEvent,
+ uint32_t aIndexOfKeypress,
+ void* aData)
+{
+ KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent,
+ static_cast<GdkEventKey*>(aData));
+}
+
+TextEventDispatcher*
+IMContextWrapper::GetTextEventDispatcher()
+{
+ if (NS_WARN_IF(!mLastFocusedWindow)) {
+ return nullptr;
+ }
+ TextEventDispatcher* dispatcher =
+ mLastFocusedWindow->GetTextEventDispatcher();
+ // nsIWidget::GetTextEventDispatcher() shouldn't return nullptr.
+ MOZ_RELEASE_ASSERT(dispatcher);
+ return dispatcher;
+}
+
+nsIMEUpdatePreference
+IMContextWrapper::GetIMEUpdatePreference() const
+{
+ // While a plugin has focus, IMContextWrapper doesn't need any
+ // notifications.
+ if (mInputContext.mIMEState.mEnabled == IMEState::PLUGIN) {
+ return nsIMEUpdatePreference();
+ }
+
+ nsIMEUpdatePreference::Notifications notifications =
+ nsIMEUpdatePreference::NOTIFY_NOTHING;
+ // If it's not enabled, we don't need position change notification.
+ if (IsEnabled()) {
+ notifications |= nsIMEUpdatePreference::NOTIFY_POSITION_CHANGE;
+ }
+ nsIMEUpdatePreference updatePreference(notifications);
+ return updatePreference;
+}
+
+void
+IMContextWrapper::OnDestroyWindow(nsWindow* aWindow)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
+ "mOwnerWindow=0x%p, mLastFocusedModule=0x%p",
+ this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext));
+
+ NS_PRECONDITION(aWindow, "aWindow must not be null");
+
+ if (mLastFocusedWindow == aWindow) {
+ EndIMEComposition(aWindow);
+ if (mIsIMFocused) {
+ Blur();
+ }
+ mLastFocusedWindow = nullptr;
+ }
+
+ if (mOwnerWindow != aWindow) {
+ return;
+ }
+
+ if (sLastFocusedContext == this) {
+ sLastFocusedContext = nullptr;
+ }
+
+ /**
+ * NOTE:
+ * The given window is the owner of this, so, we must release the
+ * contexts now. But that might be referred from other nsWindows
+ * (they are children of this. But we don't know why there are the
+ * cases). So, we need to clear the pointers that refers to contexts
+ * and this if the other referrers are still alive. See bug 349727.
+ */
+ if (mContext) {
+ PrepareToDestroyContext(mContext);
+ gtk_im_context_set_client_window(mContext, nullptr);
+ g_object_unref(mContext);
+ mContext = nullptr;
+ }
+
+ if (mSimpleContext) {
+ gtk_im_context_set_client_window(mSimpleContext, nullptr);
+ g_object_unref(mSimpleContext);
+ mSimpleContext = nullptr;
+ }
+
+ if (mDummyContext) {
+ // mContext and mDummyContext have the same slaveType and signal_data
+ // so no need for another workaround_gtk_im_display_closed.
+ gtk_im_context_set_client_window(mDummyContext, nullptr);
+ g_object_unref(mDummyContext);
+ mDummyContext = nullptr;
+ }
+
+ if (NS_WARN_IF(mComposingContext)) {
+ g_object_unref(mComposingContext);
+ mComposingContext = nullptr;
+ }
+
+ mOwnerWindow = nullptr;
+ mLastFocusedWindow = nullptr;
+ mInputContext.mIMEState.mEnabled = IMEState::DISABLED;
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p OnDestroyWindow(), succeeded, Completely destroyed",
+ this));
+}
+
+// Work around gtk bug http://bugzilla.gnome.org/show_bug.cgi?id=483223:
+// (and the similar issue of GTK+ IIIM)
+// The GTK+ XIM and IIIM modules register handlers for the "closed" signal
+// on the display, but:
+// * The signal handlers are not disconnected when the module is unloaded.
+//
+// The GTK+ XIM module has another problem:
+// * When the signal handler is run (with the module loaded) it tries
+// XFree (and fails) on a pointer that did not come from Xmalloc.
+//
+// To prevent these modules from being unloaded, use static variables to
+// hold ref of GtkIMContext class.
+// For GTK+ XIM module, to prevent the signal handler from being run,
+// find the signal handlers and remove them.
+//
+// GtkIMContextXIMs share XOpenIM connections and display closed signal
+// handlers (where possible).
+
+void
+IMContextWrapper::PrepareToDestroyContext(GtkIMContext* aContext)
+{
+#if (MOZ_WIDGET_GTK == 2)
+ GtkIMMulticontext *multicontext = GTK_IM_MULTICONTEXT(aContext);
+ GtkIMContext *slave = multicontext->slave;
+#else
+ GtkIMContext *slave = nullptr; //TODO GTK3
+#endif
+ if (!slave) {
+ return;
+ }
+
+ GType slaveType = G_TYPE_FROM_INSTANCE(slave);
+ const gchar *im_type_name = g_type_name(slaveType);
+ if (strcmp(im_type_name, "GtkIMContextIIIM") == 0) {
+ // Add a reference to prevent the IIIM module from being unloaded
+ static gpointer gtk_iiim_context_class =
+ g_type_class_ref(slaveType);
+ // Mute unused variable warning:
+ (void)gtk_iiim_context_class;
+ }
+}
+
+void
+IMContextWrapper::OnFocusWindow(nsWindow* aWindow)
+{
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p",
+ this, aWindow, mLastFocusedWindow));
+ mLastFocusedWindow = aWindow;
+ Focus();
+}
+
+void
+IMContextWrapper::OnBlurWindow(nsWindow* aWindow)
+{
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
+ "mIsIMFocused=%s",
+ this, aWindow, mLastFocusedWindow, ToChar(mIsIMFocused)));
+
+ if (!mIsIMFocused || mLastFocusedWindow != aWindow) {
+ return;
+ }
+
+ Blur();
+}
+
+bool
+IMContextWrapper::OnKeyEvent(nsWindow* aCaller,
+ GdkEventKey* aEvent,
+ bool aKeyDownEventWasSent /* = false */)
+{
+ NS_PRECONDITION(aEvent, "aEvent must be non-null");
+
+ if (!mInputContext.mIMEState.MaybeEditable() ||
+ MOZ_UNLIKELY(IsDestroyed())) {
+ return false;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnKeyEvent(aCaller=0x%p, aKeyDownEventWasSent=%s), "
+ "mCompositionState=%s, current context=0x%p, active context=0x%p, "
+ "aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X }",
+ this, aCaller, ToChar(aKeyDownEventWasSent),
+ GetCompositionStateName(), GetCurrentContext(), GetActiveContext(),
+ aEvent, GetEventType(aEvent), gdk_keyval_name(aEvent->keyval),
+ gdk_keyval_to_unicode(aEvent->keyval)));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
+ "window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return false;
+ }
+
+ // Even if old IM context has composition, key event should be sent to
+ // current context since the user expects so.
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (MOZ_UNLIKELY(!currentContext)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnKeyEvent(), FAILED, there are no context",
+ this));
+ return false;
+ }
+
+ if (mSetCursorPositionOnKeyEvent) {
+ SetCursorPosition(currentContext);
+ mSetCursorPositionOnKeyEvent = false;
+ }
+
+ mKeyDownEventWasSent = aKeyDownEventWasSent;
+ mFilterKeyEvent = true;
+ mProcessingKeyEvent = aEvent;
+ gboolean isFiltered =
+ gtk_im_context_filter_keypress(currentContext, aEvent);
+ mProcessingKeyEvent = nullptr;
+
+ // We filter the key event if the event was not committed (because
+ // it's probably part of a composition) or if the key event was
+ // committed _and_ changed. This way we still let key press
+ // events go through as simple key press events instead of
+ // composed characters.
+ bool filterThisEvent = isFiltered && mFilterKeyEvent;
+
+ if (IsComposingOnCurrentContext() && !isFiltered) {
+ if (aEvent->type == GDK_KEY_PRESS) {
+ if (!mDispatchedCompositionString.IsEmpty()) {
+ // If there is composition string, we shouldn't dispatch
+ // any keydown events during composition.
+ filterThisEvent = true;
+ } else {
+ // A Hangul input engine for SCIM doesn't emit preedit_end
+ // signal even when composition string becomes empty. On the
+ // other hand, we should allow to make composition with empty
+ // string for other languages because there *might* be such
+ // IM. For compromising this issue, we should dispatch
+ // compositionend event, however, we don't need to reset IM
+ // actually.
+ DispatchCompositionCommitEvent(currentContext, &EmptyString());
+ filterThisEvent = false;
+ }
+ } else {
+ // Key release event may not be consumed by IM, however, we
+ // shouldn't dispatch any keyup event during composition.
+ filterThisEvent = true;
+ }
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s "
+ "(isFiltered=%s, mFilterKeyEvent=%s), mCompositionState=%s",
+ this, ToChar(filterThisEvent), ToChar(isFiltered),
+ ToChar(mFilterKeyEvent), GetCompositionStateName()));
+
+ return filterThisEvent;
+}
+
+void
+IMContextWrapper::OnFocusChangeInGecko(bool aFocus)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnFocusChangeInGecko(aFocus=%s), "
+ "mCompositionState=%s, mIsIMFocused=%s",
+ this, ToChar(aFocus), GetCompositionStateName(),
+ ToChar(mIsIMFocused)));
+
+ // We shouldn't carry over the removed string to another editor.
+ mSelectedString.Truncate();
+ mSelection.Clear();
+}
+
+void
+IMContextWrapper::ResetIME()
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p ResetIME(), mCompositionState=%s, mIsIMFocused=%s",
+ this, GetCompositionStateName(), ToChar(mIsIMFocused)));
+
+ GtkIMContext* activeContext = GetActiveContext();
+ if (MOZ_UNLIKELY(!activeContext)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p ResetIME(), FAILED, there are no context",
+ this));
+ return;
+ }
+
+ RefPtr<IMContextWrapper> kungFuDeathGrip(this);
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+
+ mPendingResettingIMContext = false;
+ gtk_im_context_reset(activeContext);
+
+ // The last focused window might have been destroyed by a DOM event handler
+ // which was called by us during a call of gtk_im_context_reset().
+ if (!lastFocusedWindow ||
+ NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) ||
+ lastFocusedWindow->Destroyed()) {
+ return;
+ }
+
+ nsAutoString compositionString;
+ GetCompositionString(activeContext, compositionString);
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p ResetIME() called gtk_im_context_reset(), "
+ "activeContext=0x%p, mCompositionState=%s, compositionString=%s, "
+ "mIsIMFocused=%s",
+ this, activeContext, GetCompositionStateName(),
+ NS_ConvertUTF16toUTF8(compositionString).get(),
+ ToChar(mIsIMFocused)));
+
+ // XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still
+ // used in Japan!) sends only "preedit_changed" signal with empty
+ // composition string synchronously. Therefore, if composition string
+ // is now empty string, we should assume that the IME won't send
+ // "commit" signal.
+ if (IsComposing() && compositionString.IsEmpty()) {
+ // WARNING: The widget might have been gone after this.
+ DispatchCompositionCommitEvent(activeContext, &EmptyString());
+ }
+}
+
+nsresult
+IMContextWrapper::EndIMEComposition(nsWindow* aCaller)
+{
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return NS_OK;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p EndIMEComposition(aCaller=0x%p), "
+ "mCompositionState=%s",
+ this, aCaller, GetCompositionStateName()));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p EndIMEComposition(), FAILED, the caller isn't "
+ "focused window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return NS_OK;
+ }
+
+ if (!IsComposing()) {
+ return NS_OK;
+ }
+
+ // Currently, GTK has API neither to commit nor to cancel composition
+ // forcibly. Therefore, TextComposition will recompute commit string for
+ // the request even if native IME will cause unexpected commit string.
+ // So, we don't need to emulate commit or cancel composition with
+ // proper composition events.
+ // XXX ResetIME() might not enough for finishing compositoin on some
+ // environments. We should emulate focus change too because some IMEs
+ // may commit or cancel composition at blur.
+ ResetIME();
+
+ return NS_OK;
+}
+
+void
+IMContextWrapper::OnLayoutChange()
+{
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ if (IsComposing()) {
+ SetCursorPosition(GetActiveContext());
+ } else {
+ // If not composing, candidate window position is updated before key
+ // down
+ mSetCursorPositionOnKeyEvent = true;
+ }
+ mLayoutChanged = true;
+}
+
+void
+IMContextWrapper::OnUpdateComposition()
+{
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ if (!IsComposing()) {
+ // Composition has been committed. So we need update selection for
+ // caret later
+ mSelection.Clear();
+ EnsureToCacheSelection();
+ mSetCursorPositionOnKeyEvent = true;
+ }
+
+ // If we've already set candidate window position, we don't need to update
+ // the position with update composition notification.
+ if (!mLayoutChanged) {
+ SetCursorPosition(GetActiveContext());
+ }
+}
+
+void
+IMContextWrapper::SetInputContext(nsWindow* aCaller,
+ const InputContext* aContext,
+ const InputContextAction* aAction)
+{
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ "
+ "mEnabled=%s }, mHTMLInputType=%s })",
+ this, aCaller, GetEnabledStateName(aContext->mIMEState.mEnabled),
+ NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetInputContext(), FAILED, "
+ "the caller isn't focused window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return;
+ }
+
+ if (!mContext) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetInputContext(), FAILED, "
+ "there are no context",
+ this));
+ return;
+ }
+
+
+ if (sLastFocusedContext != this) {
+ mInputContext = *aContext;
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p SetInputContext(), succeeded, "
+ "but we're not active",
+ this));
+ return;
+ }
+
+ bool changingEnabledState =
+ aContext->mIMEState.mEnabled != mInputContext.mIMEState.mEnabled ||
+ aContext->mHTMLInputType != mInputContext.mHTMLInputType;
+
+ // Release current IME focus if IME is enabled.
+ if (changingEnabledState && mInputContext.mIMEState.MaybeEditable()) {
+ EndIMEComposition(mLastFocusedWindow);
+ Blur();
+ }
+
+ mInputContext = *aContext;
+
+ if (changingEnabledState) {
+#if (MOZ_WIDGET_GTK == 3)
+ static bool sInputPurposeSupported = !gtk_check_version(3, 6, 0);
+ if (sInputPurposeSupported && mInputContext.mIMEState.MaybeEditable()) {
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (currentContext) {
+ GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM;
+ const nsString& inputType = mInputContext.mHTMLInputType;
+ // Password case has difficult issue. Desktop IMEs disable
+ // composition if input-purpose is password.
+ // For disabling IME on |ime-mode: disabled;|, we need to check
+ // mEnabled value instead of inputType value. This hack also
+ // enables composition on
+ // <input type="password" style="ime-mode: enabled;">.
+ // This is right behavior of ime-mode on desktop.
+ //
+ // On the other hand, IME for tablet devices may provide a
+ // specific software keyboard for password field. If so,
+ // the behavior might look strange on both:
+ // <input type="text" style="ime-mode: disabled;">
+ // <input type="password" style="ime-mode: enabled;">
+ //
+ // Temporarily, we should focus on desktop environment for now.
+ // I.e., let's ignore tablet devices for now. When somebody
+ // reports actual trouble on tablet devices, we should try to
+ // look for a way to solve actual problem.
+ if (mInputContext.mIMEState.mEnabled == IMEState::PASSWORD) {
+ purpose = GTK_INPUT_PURPOSE_PASSWORD;
+ } else if (inputType.EqualsLiteral("email")) {
+ purpose = GTK_INPUT_PURPOSE_EMAIL;
+ } else if (inputType.EqualsLiteral("url")) {
+ purpose = GTK_INPUT_PURPOSE_URL;
+ } else if (inputType.EqualsLiteral("tel")) {
+ purpose = GTK_INPUT_PURPOSE_PHONE;
+ } else if (inputType.EqualsLiteral("number")) {
+ purpose = GTK_INPUT_PURPOSE_NUMBER;
+ }
+
+ g_object_set(currentContext, "input-purpose", purpose, nullptr);
+ }
+ }
+#endif // #if (MOZ_WIDGET_GTK == 3)
+
+ // Even when aState is not enabled state, we need to set IME focus.
+ // Because some IMs are updating the status bar of them at this time.
+ // Be aware, don't use aWindow here because this method shouldn't move
+ // focus actually.
+ Focus();
+
+ // XXX Should we call Blur() when it's not editable? E.g., it might be
+ // better to close VKB automatically.
+ }
+}
+
+InputContext
+IMContextWrapper::GetInputContext()
+{
+ mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
+ return mInputContext;
+}
+
+GtkIMContext*
+IMContextWrapper::GetCurrentContext() const
+{
+ if (IsEnabled()) {
+ return mContext;
+ }
+ if (mInputContext.mIMEState.mEnabled == IMEState::PASSWORD) {
+ return mSimpleContext;
+ }
+ return mDummyContext;
+}
+
+bool
+IMContextWrapper::IsValidContext(GtkIMContext* aContext) const
+{
+ if (!aContext) {
+ return false;
+ }
+ return aContext == mContext ||
+ aContext == mSimpleContext ||
+ aContext == mDummyContext;
+}
+
+bool
+IMContextWrapper::IsEnabled() const
+{
+ return mInputContext.mIMEState.mEnabled == IMEState::ENABLED ||
+ mInputContext.mIMEState.mEnabled == IMEState::PLUGIN ||
+ (!sUseSimpleContext &&
+ mInputContext.mIMEState.mEnabled == IMEState::PASSWORD);
+}
+
+void
+IMContextWrapper::Focus()
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p Focus(), sLastFocusedContext=0x%p",
+ this, sLastFocusedContext));
+
+ if (mIsIMFocused) {
+ NS_ASSERTION(sLastFocusedContext == this,
+ "We're not active, but the IM was focused?");
+ return;
+ }
+
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (!currentContext) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p Focus(), FAILED, there are no context",
+ this));
+ return;
+ }
+
+ if (sLastFocusedContext && sLastFocusedContext != this) {
+ sLastFocusedContext->Blur();
+ }
+
+ sLastFocusedContext = this;
+
+ gtk_im_context_focus_in(currentContext);
+ mIsIMFocused = true;
+ mSetCursorPositionOnKeyEvent = true;
+
+ if (!IsEnabled()) {
+ // We should release IME focus for uim and scim.
+ // These IMs are using snooper that is released at losing focus.
+ Blur();
+ }
+}
+
+void
+IMContextWrapper::Blur()
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p Blur(), mIsIMFocused=%s",
+ this, ToChar(mIsIMFocused)));
+
+ if (!mIsIMFocused) {
+ return;
+ }
+
+ GtkIMContext* currentContext = GetCurrentContext();
+ if (!currentContext) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p Blur(), FAILED, there are no context",
+ this));
+ return;
+ }
+
+ gtk_im_context_focus_out(currentContext);
+ mIsIMFocused = false;
+}
+
+void
+IMContextWrapper::OnSelectionChange(nsWindow* aCaller,
+ const IMENotification& aIMENotification)
+{
+ mSelection.Assign(aIMENotification);
+ bool retrievedSurroundingSignalReceived =
+ mRetrieveSurroundingSignalReceived;
+ mRetrieveSurroundingSignalReceived = false;
+
+ if (MOZ_UNLIKELY(IsDestroyed())) {
+ return;
+ }
+
+ const IMENotification::SelectionChangeDataBase& selectionChangeData =
+ aIMENotification.mSelectionChangeData;
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
+ "mSelectionChangeData={ mOffset=%u, Length()=%u, mReversed=%s, "
+ "mWritingMode=%s, mCausedByComposition=%s, "
+ "mCausedBySelectionEvent=%s, mOccurredDuringComposition=%s "
+ "} }), mCompositionState=%s, mIsDeletingSurrounding=%s, "
+ "mRetrieveSurroundingSignalReceived=%s",
+ this, aCaller, selectionChangeData.mOffset,
+ selectionChangeData.Length(),
+ ToChar(selectionChangeData.mReversed),
+ GetWritingModeName(selectionChangeData.GetWritingMode()).get(),
+ ToChar(selectionChangeData.mCausedByComposition),
+ ToChar(selectionChangeData.mCausedBySelectionEvent),
+ ToChar(selectionChangeData.mOccurredDuringComposition),
+ GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
+ ToChar(retrievedSurroundingSignalReceived)));
+
+ if (aCaller != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnSelectionChange(), FAILED, "
+ "the caller isn't focused window, mLastFocusedWindow=0x%p",
+ this, mLastFocusedWindow));
+ return;
+ }
+
+ if (!IsComposing()) {
+ // Now we have no composition (mostly situation on calling this method)
+ // If we have it, it will set by
+ // NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED.
+ mSetCursorPositionOnKeyEvent = true;
+ }
+
+ // The focused editor might have placeholder text with normal text node.
+ // In such case, the text node must be removed from a compositionstart
+ // event handler. So, we're dispatching eCompositionStart,
+ // we should ignore selection change notification.
+ if (mCompositionState == eCompositionState_CompositionStartDispatched) {
+ if (NS_WARN_IF(!mSelection.IsValid())) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnSelectionChange(), FAILED, "
+ "new offset is too large, cannot keep composing",
+ this));
+ } else {
+ // Modify the selection start offset with new offset.
+ mCompositionStart = mSelection.mOffset;
+ // XXX We should modify mSelectedString? But how?
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p OnSelectionChange(), ignored, mCompositionStart "
+ "is updated to %u, the selection change doesn't cause "
+ "resetting IM context",
+ this, mCompositionStart));
+ // And don't reset the IM context.
+ return;
+ }
+ // Otherwise, reset the IM context due to impossible to keep composing.
+ }
+
+ // If the selection change is caused by deleting surrounding text,
+ // we shouldn't need to notify IME of selection change.
+ if (mIsDeletingSurrounding) {
+ return;
+ }
+
+ bool occurredBeforeComposition =
+ IsComposing() && !selectionChangeData.mOccurredDuringComposition &&
+ !selectionChangeData.mCausedByComposition;
+ if (occurredBeforeComposition) {
+ mPendingResettingIMContext = true;
+ }
+
+ // When the selection change is caused by dispatching composition event,
+ // selection set event and/or occurred before starting current composition,
+ // we shouldn't notify IME of that and commit existing composition.
+ if (!selectionChangeData.mCausedByComposition &&
+ !selectionChangeData.mCausedBySelectionEvent &&
+ !occurredBeforeComposition) {
+ // Hack for ibus-pinyin. ibus-pinyin will synthesize a set of
+ // composition which commits with empty string after calling
+ // gtk_im_context_reset(). Therefore, selecting text causes
+ // unexpectedly removing it. For preventing it but not breaking the
+ // other IMEs which use surrounding text, we should call it only when
+ // surrounding text has been retrieved after last selection range was
+ // set. If it's not retrieved, that means that current IME doesn't
+ // have any content cache, so, it must not need the notification of
+ // selection change.
+ if (IsComposing() || retrievedSurroundingSignalReceived) {
+ ResetIME();
+ }
+ }
+}
+
+/* static */
+void
+IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule)
+{
+ aModule->OnStartCompositionNative(aContext);
+}
+
+void
+IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnStartCompositionNative(aContext=0x%p), "
+ "current context=0x%p",
+ this, aContext, GetCurrentContext()));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (GetCurrentContext() != aContext) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnStartCompositionNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return;
+ }
+
+ mComposingContext = static_cast<GtkIMContext*>(g_object_ref(aContext));
+
+ if (!DispatchCompositionStart(aContext)) {
+ return;
+ }
+ mCompositionTargetRange.mOffset = mCompositionStart;
+ mCompositionTargetRange.mLength = 0;
+}
+
+/* static */
+void
+IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule)
+{
+ aModule->OnEndCompositionNative(aContext);
+}
+
+void
+IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnEndCompositionNative(aContext=0x%p)",
+ this, aContext));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ // Note that if this is called after focus move, the context may different
+ // from any our owning context.
+ if (!IsValidContext(aContext)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnEndCompositionNative(), FAILED, "
+ "given context doesn't match with any context",
+ this));
+ return;
+ }
+
+ g_object_unref(mComposingContext);
+ mComposingContext = nullptr;
+
+ // If we already handled the commit event, we should do nothing here.
+ if (IsComposing()) {
+ if (!DispatchCompositionCommitEvent(aContext)) {
+ // If the widget is destroyed, we should do nothing anymore.
+ return;
+ }
+ }
+
+ if (mPendingResettingIMContext) {
+ ResetIME();
+ }
+}
+
+/* static */
+void
+IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule)
+{
+ aModule->OnChangeCompositionNative(aContext);
+}
+
+void
+IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnChangeCompositionNative(aContext=0x%p)",
+ this, aContext));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ // Note that if this is called after focus move, the context may different
+ // from any our owning context.
+ if (!IsValidContext(aContext)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnChangeCompositionNative(), FAILED, "
+ "given context doesn't match with any context",
+ this));
+ return;
+ }
+
+ nsAutoString compositionString;
+ GetCompositionString(aContext, compositionString);
+ if (!IsComposing() && compositionString.IsEmpty()) {
+ mDispatchedCompositionString.Truncate();
+ return; // Don't start the composition with empty string.
+ }
+
+ // Be aware, widget can be gone
+ DispatchCompositionChangeEvent(aContext, compositionString);
+}
+
+/* static */
+gboolean
+IMContextWrapper::OnRetrieveSurroundingCallback(GtkIMContext* aContext,
+ IMContextWrapper* aModule)
+{
+ return aModule->OnRetrieveSurroundingNative(aContext);
+}
+
+gboolean
+IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnRetrieveSurroundingNative(aContext=0x%p), "
+ "current context=0x%p",
+ this, aContext, GetCurrentContext()));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (GetCurrentContext() != aContext) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnRetrieveSurroundingNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return FALSE;
+ }
+
+ nsAutoString uniStr;
+ uint32_t cursorPos;
+ if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) {
+ return FALSE;
+ }
+
+ NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
+ uint32_t cursorPosInUTF8 = utf8Str.Length();
+ AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
+ gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
+ cursorPosInUTF8);
+ mRetrieveSurroundingSignalReceived = true;
+ return TRUE;
+}
+
+/* static */
+gboolean
+IMContextWrapper::OnDeleteSurroundingCallback(GtkIMContext* aContext,
+ gint aOffset,
+ gint aNChars,
+ IMContextWrapper* aModule)
+{
+ return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
+}
+
+gboolean
+IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext,
+ gint aOffset,
+ gint aNChars)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, "
+ "aNChar=%d), current context=0x%p",
+ this, aContext, aOffset, aNChars, GetCurrentContext()));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (GetCurrentContext() != aContext) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnDeleteSurroundingNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return FALSE;
+ }
+
+ AutoRestore<bool> saveDeletingSurrounding(mIsDeletingSurrounding);
+ mIsDeletingSurrounding = true;
+ if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) {
+ return TRUE;
+ }
+
+ // failed
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnDeleteSurroundingNative(), FAILED, "
+ "cannot delete text",
+ this));
+ return FALSE;
+}
+
+/* static */
+void
+IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext,
+ const gchar* aString,
+ IMContextWrapper* aModule)
+{
+ aModule->OnCommitCompositionNative(aContext, aString);
+}
+
+void
+IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext,
+ const gchar* aUTF8Char)
+{
+ const gchar emptyStr = 0;
+ const gchar *commitString = aUTF8Char ? aUTF8Char : &emptyStr;
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnCommitCompositionNative(aContext=0x%p), "
+ "current context=0x%p, active context=0x%p, commitString=\"%s\", "
+ "mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s",
+ this, aContext, GetCurrentContext(), GetActiveContext(), commitString,
+ mProcessingKeyEvent, ToChar(IsComposingOn(aContext))));
+
+ // See bug 472635, we should do nothing if IM context doesn't match.
+ if (!IsValidContext(aContext)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p OnCommitCompositionNative(), FAILED, "
+ "given context doesn't match",
+ this));
+ return;
+ }
+
+ // If we are not in composition and committing with empty string,
+ // we need to do nothing because if we continued to handle this
+ // signal, we would dispatch compositionstart, text, compositionend
+ // events with empty string. Of course, they are unnecessary events
+ // for Web applications and our editor.
+ if (!IsComposingOn(aContext) && !commitString[0]) {
+ return;
+ }
+
+ // If IME doesn't change their keyevent that generated this commit,
+ // don't send it through XIM - just send it as a normal key press
+ // event.
+ // NOTE: While a key event is being handled, this might be caused on
+ // current context. Otherwise, this may be caused on active context.
+ if (!IsComposingOn(aContext) && mProcessingKeyEvent &&
+ aContext == GetCurrentContext()) {
+ char keyval_utf8[8]; /* should have at least 6 bytes of space */
+ gint keyval_utf8_len;
+ guint32 keyval_unicode;
+
+ keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval);
+ keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8);
+ keyval_utf8[keyval_utf8_len] = '\0';
+
+ if (!strcmp(commitString, keyval_utf8)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p OnCommitCompositionNative(), "
+ "we'll send normal key event",
+ this));
+ mFilterKeyEvent = false;
+ return;
+ }
+ }
+
+ NS_ConvertUTF8toUTF16 str(commitString);
+ // Be aware, widget can be gone
+ DispatchCompositionCommitEvent(aContext, &str);
+}
+
+void
+IMContextWrapper::GetCompositionString(GtkIMContext* aContext,
+ nsAString& aCompositionString)
+{
+ gchar *preedit_string;
+ gint cursor_pos;
+ PangoAttrList *feedback_list;
+ gtk_im_context_get_preedit_string(aContext, &preedit_string,
+ &feedback_list, &cursor_pos);
+ if (preedit_string && *preedit_string) {
+ CopyUTF8toUTF16(preedit_string, aCompositionString);
+ } else {
+ aCompositionString.Truncate();
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p GetCompositionString(aContext=0x%p), "
+ "aCompositionString=\"%s\"",
+ this, aContext, preedit_string));
+
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+}
+
+bool
+IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p DispatchCompositionStart(aContext=0x%p)",
+ this, aContext));
+
+ if (IsComposing()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "we're already in composition",
+ this));
+ return true;
+ }
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "there are no focused window in this module",
+ this));
+ return false;
+ }
+
+ if (NS_WARN_IF(!EnsureToCacheSelection())) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "cannot query the selection offset",
+ this));
+ return false;
+ }
+
+ // Keep the last focused window alive
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+
+ // XXX The composition start point might be changed by composition events
+ // even though we strongly hope it doesn't happen.
+ // Every composition event should have the start offset for the result
+ // because it may high cost if we query the offset every time.
+ mCompositionStart = mSelection.mOffset;
+ mDispatchedCompositionString.Truncate();
+
+ if (mProcessingKeyEvent && !mKeyDownEventWasSent &&
+ mProcessingKeyEvent->type == GDK_KEY_PRESS) {
+ // If this composition is started by a native keydown event, we need to
+ // dispatch our keydown event here (before composition start).
+ bool isCancelled;
+ mLastFocusedWindow->DispatchKeyDownEvent(mProcessingKeyEvent,
+ &isCancelled);
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p DispatchCompositionStart(), FAILED, keydown event "
+ "is dispatched",
+ this));
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, the focused "
+ "widget was destroyed/changed by keydown event",
+ this));
+ return false;
+ }
+ }
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, "
+ "due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p DispatchCompositionStart(), dispatching "
+ "compositionstart... (mCompositionStart=%u)",
+ this, mCompositionStart));
+ mCompositionState = eCompositionState_CompositionStartDispatched;
+ nsEventStatus status;
+ dispatcher->StartComposition(status);
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionStart(), FAILED, the focused "
+ "widget was destroyed/changed by compositionstart event",
+ this));
+ return false;
+ }
+
+ return true;
+}
+
+bool
+IMContextWrapper::DispatchCompositionChangeEvent(
+ GtkIMContext* aContext,
+ const nsAString& aCompositionString)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p DispatchCompositionChangeEvent(aContext=0x%p)",
+ this, aContext));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "there are no focused window in this module",
+ this));
+ return false;
+ }
+
+ if (!IsComposing()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p DispatchCompositionChangeEvent(), the composition "
+ "wasn't started, force starting...",
+ this));
+ if (!DispatchCompositionStart(aContext)) {
+ return false;
+ }
+ }
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ // Store the selected string which will be removed by following
+ // compositionchange event.
+ if (mCompositionState == eCompositionState_CompositionStartDispatched) {
+ if (NS_WARN_IF(!EnsureToCacheSelection(&mSelectedString))) {
+ // XXX How should we behave in this case??
+ } else {
+ // XXX We should assume, for now, any web applications don't change
+ // selection at handling this compositionchange event.
+ mCompositionStart = mSelection.mOffset;
+ }
+ }
+
+ RefPtr<TextRangeArray> rangeArray =
+ CreateTextRangeArray(aContext, aCompositionString);
+
+ rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to SetPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ mCompositionState = eCompositionState_CompositionChangeEventDispatched;
+
+ // We cannot call SetCursorPosition for e10s-aware.
+ // DispatchEvent is async on e10s, so composition rect isn't updated now
+ // on tab parent.
+ mDispatchedCompositionString = aCompositionString;
+ mLayoutChanged = false;
+ mCompositionTargetRange.mOffset =
+ mCompositionStart + rangeArray->TargetClauseOffset();
+ mCompositionTargetRange.mLength = rangeArray->TargetClauseLength();
+
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+ nsEventStatus status;
+ rv = dispatcher->FlushPendingComposition(status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to FlushPendingComposition() failure",
+ this));
+ return false;
+ }
+
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, the "
+ "focused widget was destroyed/changed by "
+ "compositionchange event",
+ this));
+ return false;
+ }
+ return true;
+}
+
+bool
+IMContextWrapper::DispatchCompositionCommitEvent(
+ GtkIMContext* aContext,
+ const nsAString* aCommitString)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p DispatchCompositionCommitEvent(aContext=0x%p, "
+ "aCommitString=0x%p, (\"%s\"))",
+ this, aContext, aCommitString,
+ aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "there are no focused window in this module",
+ this));
+ return false;
+ }
+
+ if (!IsComposing()) {
+ if (!aCommitString || aCommitString->IsEmpty()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "there is no composition and empty commit string",
+ this));
+ return true;
+ }
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p DispatchCompositionCommitEvent(), "
+ "the composition wasn't started, force starting...",
+ this));
+ if (!DispatchCompositionStart(aContext)) {
+ return false;
+ }
+ }
+
+ RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
+ nsresult rv = dispatcher->BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "due to BeginNativeInputTransaction() failure",
+ this));
+ return false;
+ }
+
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+
+ mCompositionState = eCompositionState_NotComposing;
+ mCompositionStart = UINT32_MAX;
+ mCompositionTargetRange.Clear();
+ mDispatchedCompositionString.Truncate();
+
+ nsEventStatus status;
+ rv = dispatcher->CommitComposition(status, aCommitString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionChangeEvent(), FAILED, "
+ "due to CommitComposition() failure",
+ this));
+ return false;
+ }
+
+ if (lastFocusedWindow->IsDestroyed() ||
+ lastFocusedWindow != mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DispatchCompositionCommitEvent(), FAILED, "
+ "the focused widget was destroyed/changed by "
+ "compositioncommit event",
+ this));
+ return false;
+ }
+
+ return true;
+}
+
+already_AddRefed<TextRangeArray>
+IMContextWrapper::CreateTextRangeArray(GtkIMContext* aContext,
+ const nsAString& aCompositionString)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p CreateTextRangeArray(aContext=0x%p, "
+ "aCompositionString=\"%s\" (Length()=%u))",
+ this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(),
+ aCompositionString.Length()));
+
+ RefPtr<TextRangeArray> textRangeArray = new TextRangeArray();
+
+ gchar *preedit_string;
+ gint cursor_pos_in_chars;
+ PangoAttrList *feedback_list;
+ gtk_im_context_get_preedit_string(aContext, &preedit_string,
+ &feedback_list, &cursor_pos_in_chars);
+ if (!preedit_string || !*preedit_string) {
+ if (!aCompositionString.IsEmpty()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p CreateTextRangeArray(), FAILED, due to "
+ "preedit_string is null",
+ this));
+ }
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+ return textRangeArray.forget();
+ }
+
+ // Convert caret offset from offset in characters to offset in UTF-16
+ // string. If we couldn't proper offset in UTF-16 string, we should
+ // assume that the caret is at the end of the composition string.
+ uint32_t caretOffsetInUTF16 = aCompositionString.Length();
+ if (NS_WARN_IF(cursor_pos_in_chars < 0)) {
+ // Note that this case is undocumented. We should assume that the
+ // caret is at the end of the composition string.
+ } else if (cursor_pos_in_chars == 0) {
+ caretOffsetInUTF16 = 0;
+ } else {
+ gchar* charAfterCaret =
+ g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars);
+ if (NS_WARN_IF(!charAfterCaret)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), failed to get UTF-8 "
+ "string before the caret (cursor_pos_in_chars=%d)",
+ this, cursor_pos_in_chars));
+ } else {
+ glong caretOffset = 0;
+ gunichar2* utf16StrBeforeCaret =
+ g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string,
+ nullptr, &caretOffset, nullptr);
+ if (NS_WARN_IF(!utf16StrBeforeCaret) ||
+ NS_WARN_IF(caretOffset < 0)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), WARNING, failed to "
+ "convert to UTF-16 string before the caret "
+ "(cursor_pos_in_chars=%d, caretOffset=%d)",
+ this, cursor_pos_in_chars, caretOffset));
+ } else {
+ caretOffsetInUTF16 = static_cast<uint32_t>(caretOffset);
+ uint32_t compositionStringLength = aCompositionString.Length();
+ if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), WARNING, "
+ "caretOffsetInUTF16=%u is larger than "
+ "compositionStringLength=%u",
+ this, caretOffsetInUTF16, compositionStringLength));
+ caretOffsetInUTF16 = compositionStringLength;
+ }
+ }
+ if (utf16StrBeforeCaret) {
+ g_free(utf16StrBeforeCaret);
+ }
+ }
+ }
+
+ PangoAttrIterator* iter;
+ iter = pango_attr_list_get_iterator(feedback_list);
+ if (!iter) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p CreateTextRangeArray(), FAILED, iterator couldn't "
+ "be allocated",
+ this));
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+ return textRangeArray.forget();
+ }
+
+ uint32_t minOffsetOfClauses = aCompositionString.Length();
+ do {
+ TextRange range;
+ if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) {
+ continue;
+ }
+ MOZ_ASSERT(range.Length());
+ minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset);
+ textRangeArray->AppendElement(range);
+ } while (pango_attr_iterator_next(iter));
+
+ // If the IME doesn't define clause from the start of the composition,
+ // we should insert dummy clause information since TextRangeArray assumes
+ // that there must be a clause whose start is 0 when there is one or
+ // more clauses.
+ if (minOffsetOfClauses) {
+ TextRange dummyClause;
+ dummyClause.mStartOffset = 0;
+ dummyClause.mEndOffset = minOffsetOfClauses;
+ dummyClause.mRangeType = TextRangeType::eRawClause;
+ textRangeArray->InsertElementAt(0, dummyClause);
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p CreateTextRangeArray(), inserting a dummy clause "
+ "at the beginning of the composition string mStartOffset=%u, "
+ "mEndOffset=%u, mRangeType=%s",
+ this, dummyClause.mStartOffset, dummyClause.mEndOffset,
+ ToChar(dummyClause.mRangeType)));
+ }
+
+ TextRange range;
+ range.mStartOffset = range.mEndOffset = caretOffsetInUTF16;
+ range.mRangeType = TextRangeType::eCaret;
+ textRangeArray->AppendElement(range);
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p CreateTextRangeArray(), mStartOffset=%u, "
+ "mEndOffset=%u, mRangeType=%s",
+ this, range.mStartOffset, range.mEndOffset,
+ ToChar(range.mRangeType)));
+
+ pango_attr_iterator_destroy(iter);
+ pango_attr_list_unref(feedback_list);
+ g_free(preedit_string);
+
+ return textRangeArray.forget();
+}
+
+/* static */
+nscolor
+IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor)
+{
+ PangoColor& pangoColor = aPangoAttrColor->color;
+ uint8_t r = pangoColor.red / 0x100;
+ uint8_t g = pangoColor.green / 0x100;
+ uint8_t b = pangoColor.blue / 0x100;
+ return NS_RGB(r, g, b);
+}
+
+bool
+IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter,
+ const gchar* aUTF8CompositionString,
+ uint32_t aUTF16CaretOffset,
+ TextRange& aTextRange) const
+{
+ // Set the range offsets in UTF-16 string.
+ gint utf8ClauseStart, utf8ClauseEnd;
+ pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd);
+ if (utf8ClauseStart == utf8ClauseEnd) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetTextRange(), FAILED, due to collapsed range",
+ this));
+ return false;
+ }
+
+ if (!utf8ClauseStart) {
+ aTextRange.mStartOffset = 0;
+ } else {
+ glong utf16PreviousClausesLength;
+ gunichar2* utf16PreviousClausesString =
+ g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr,
+ &utf16PreviousClausesLength, nullptr);
+
+ if (NS_WARN_IF(!utf16PreviousClausesString)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
+ "failure (retrieving previous string of current clause)",
+ this));
+ return false;
+ }
+
+ aTextRange.mStartOffset = utf16PreviousClausesLength;
+ g_free(utf16PreviousClausesString);
+ }
+
+ glong utf16CurrentClauseLength;
+ gunichar2* utf16CurrentClauseString =
+ g_utf8_to_utf16(aUTF8CompositionString + utf8ClauseStart,
+ utf8ClauseEnd - utf8ClauseStart,
+ nullptr, &utf16CurrentClauseLength, nullptr);
+
+ if (NS_WARN_IF(!utf16CurrentClauseString)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
+ "failure (retrieving current clause)",
+ this));
+ return false;
+ }
+
+ // iBus Chewing IME tells us that there is an empty clause at the end of
+ // the composition string but we should ignore it since our code doesn't
+ // assume that there is an empty clause.
+ if (!utf16CurrentClauseLength) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p SetTextRange(), FAILED, due to current clause length "
+ "is 0",
+ this));
+ return false;
+ }
+
+ aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength;
+ g_free(utf16CurrentClauseString);
+ utf16CurrentClauseString = nullptr;
+
+ // Set styles
+ TextRangeStyle& style = aTextRange.mRangeStyle;
+
+ // Underline
+ PangoAttrInt* attrUnderline =
+ reinterpret_cast<PangoAttrInt*>(
+ pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE));
+ if (attrUnderline) {
+ switch (attrUnderline->value) {
+ case PANGO_UNDERLINE_NONE:
+ style.mLineStyle = TextRangeStyle::LINESTYLE_NONE;
+ break;
+ case PANGO_UNDERLINE_DOUBLE:
+ style.mLineStyle = TextRangeStyle::LINESTYLE_DOUBLE;
+ break;
+ case PANGO_UNDERLINE_ERROR:
+ style.mLineStyle = TextRangeStyle::LINESTYLE_WAVY;
+ break;
+ case PANGO_UNDERLINE_SINGLE:
+ case PANGO_UNDERLINE_LOW:
+ style.mLineStyle = TextRangeStyle::LINESTYLE_SOLID;
+ break;
+ default:
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p SetTextRange(), retrieved unknown underline "
+ "style: %d",
+ this, attrUnderline->value));
+ style.mLineStyle = TextRangeStyle::LINESTYLE_SOLID;
+ break;
+ }
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
+
+ // Underline color
+ PangoAttrColor* attrUnderlineColor =
+ reinterpret_cast<PangoAttrColor*>(
+ pango_attr_iterator_get(aPangoAttrIter,
+ PANGO_ATTR_UNDERLINE_COLOR));
+ if (attrUnderlineColor) {
+ style.mUnderlineColor = ToNscolor(attrUnderlineColor);
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR;
+ }
+ } else {
+ style.mLineStyle = TextRangeStyle::LINESTYLE_NONE;
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
+ }
+
+ // Don't set colors if they are not specified. They should be computed by
+ // textframe if only one of the colors are specified.
+
+ // Foreground color (text color)
+ PangoAttrColor* attrForeground =
+ reinterpret_cast<PangoAttrColor*>(
+ pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND));
+ if (attrForeground) {
+ style.mForegroundColor = ToNscolor(attrForeground);
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR;
+ }
+
+ // Background color
+ PangoAttrColor* attrBackground =
+ reinterpret_cast<PangoAttrColor*>(
+ pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND));
+ if (attrBackground) {
+ style.mBackgroundColor = ToNscolor(attrBackground);
+ style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR;
+ }
+
+ /**
+ * We need to judge the meaning of the clause for a11y. Before we support
+ * IME specific composition string style, we used following rules:
+ *
+ * 1: If attrUnderline and attrForground are specified, we assumed the
+ * clause is TextRangeType::eSelectedClause.
+ * 2: If only attrUnderline is specified, we assumed the clause is
+ * TextRangeType::eConvertedClause.
+ * 3: If only attrForground is specified, we assumed the clause is
+ * TextRangeType::eSelectedRawClause.
+ * 4: If neither attrUnderline nor attrForeground is specified, we assumed
+ * the clause is TextRangeType::eRawClause.
+ *
+ * However, this rules are odd since there can be two or more selected
+ * clauses. Additionally, our old rules caused that IME developers/users
+ * cannot specify composition string style as they want.
+ *
+ * So, we shouldn't guess the meaning from its visual style.
+ */
+
+ if (!attrUnderline && !attrForeground && !attrBackground) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Warning,
+ ("0x%p SetTextRange(), FAILED, due to no attr, "
+ "aTextRange= { mStartOffset=%u, mEndOffset=%u }",
+ this, aTextRange.mStartOffset, aTextRange.mEndOffset));
+ return false;
+ }
+
+ // If the range covers whole of composition string and the caret is at
+ // the end of the composition string, the range is probably not converted.
+ if (!utf8ClauseStart &&
+ utf8ClauseEnd == static_cast<gint>(strlen(aUTF8CompositionString)) &&
+ aTextRange.mEndOffset == aUTF16CaretOffset) {
+ aTextRange.mRangeType = TextRangeType::eRawClause;
+ }
+ // Typically, the caret is set at the start of the selected clause.
+ // So, if the caret is in the clause, we can assume that the clause is
+ // selected.
+ else if (aTextRange.mStartOffset <= aUTF16CaretOffset &&
+ aTextRange.mEndOffset > aUTF16CaretOffset) {
+ aTextRange.mRangeType = TextRangeType::eSelectedClause;
+ }
+ // Otherwise, we should assume that the clause is converted but not
+ // selected.
+ else {
+ aTextRange.mRangeType = TextRangeType::eConvertedClause;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p SetTextRange(), succeeded, aTextRange= { "
+ "mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }",
+ this, aTextRange.mStartOffset, aTextRange.mEndOffset,
+ ToChar(aTextRange.mRangeType),
+ GetTextRangeStyleText(aTextRange.mRangeStyle).get()));
+
+ return true;
+}
+
+void
+IMContextWrapper::SetCursorPosition(GtkIMContext* aContext)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p SetCursorPosition(aContext=0x%p), "
+ "mCompositionTargetRange={ mOffset=%u, mLength=%u }"
+ "mSelection={ mOffset=%u, mLength=%u, mWritingMode=%s }",
+ this, aContext, mCompositionTargetRange.mOffset,
+ mCompositionTargetRange.mLength,
+ mSelection.mOffset, mSelection.mLength,
+ GetWritingModeName(mSelection.mWritingMode).get()));
+
+ bool useCaret = false;
+ if (!mCompositionTargetRange.IsValid()) {
+ if (!mSelection.IsValid()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, "
+ "mCompositionTargetRange and mSelection are invalid",
+ this));
+ return;
+ }
+ useCaret = true;
+ }
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, due to no focused "
+ "window",
+ this));
+ return;
+ }
+
+ if (MOZ_UNLIKELY(!aContext)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, due to no context",
+ this));
+ return;
+ }
+
+ WidgetQueryContentEvent charRect(true,
+ useCaret ? eQueryCaretRect :
+ eQueryTextRect,
+ mLastFocusedWindow);
+ if (useCaret) {
+ charRect.InitForQueryCaretRect(mSelection.mOffset);
+ } else {
+ if (mSelection.mWritingMode.IsVertical()) {
+ // For preventing the candidate window to overlap the target
+ // clause, we should set fake (typically, very tall) caret rect.
+ uint32_t length = mCompositionTargetRange.mLength ?
+ mCompositionTargetRange.mLength : 1;
+ charRect.InitForQueryTextRect(mCompositionTargetRange.mOffset,
+ length);
+ } else {
+ charRect.InitForQueryTextRect(mCompositionTargetRange.mOffset, 1);
+ }
+ }
+ InitEvent(charRect);
+ nsEventStatus status;
+ mLastFocusedWindow->DispatchEvent(&charRect, status);
+ if (!charRect.mSucceeded) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p SetCursorPosition(), FAILED, %s was failed",
+ this, useCaret ? "eQueryCaretRect" : "eQueryTextRect"));
+ return;
+ }
+
+ nsWindow* rootWindow =
+ static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());
+
+ // Get the position of the rootWindow in screen.
+ LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset();
+
+ // Get the position of IM context owner window in screen.
+ LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset();
+
+ // Compute the caret position in the IM owner window.
+ LayoutDeviceIntRect rect = charRect.mReply.mRect + root - owner;
+ rect.width = 0;
+ GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect);
+
+ gtk_im_context_set_cursor_location(aContext, &area);
+}
+
+nsresult
+IMContextWrapper::GetCurrentParagraph(nsAString& aText,
+ uint32_t& aCursorPos)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p GetCurrentParagraph(), mCompositionState=%s",
+ this, GetCompositionStateName()));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, there are no "
+ "focused window in this module",
+ this));
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsEventStatus status;
+
+ uint32_t selOffset = mCompositionStart;
+ uint32_t selLength = mSelectedString.Length();
+
+ // If focused editor doesn't have composition string, we should use
+ // current selection.
+ if (!EditorHasCompositionString()) {
+ // Query cursor position & selection
+ if (NS_WARN_IF(!EnsureToCacheSelection())) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, due to no "
+ "valid selection information",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ selOffset = mSelection.mOffset;
+ selLength = mSelection.mLength;
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u",
+ this, selOffset, selLength));
+
+ // XXX nsString::Find and nsString::RFind take int32_t for offset, so,
+ // we cannot support this request when the current offset is larger
+ // than INT32_MAX.
+ if (selOffset > INT32_MAX || selLength > INT32_MAX ||
+ selOffset + selLength > INT32_MAX) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, The selection is "
+ "out of range",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get all text contents of the focused editor
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ mLastFocusedWindow);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
+ NS_ENSURE_TRUE(queryTextContentEvent.mSucceeded, NS_ERROR_FAILURE);
+
+ nsAutoString textContent(queryTextContentEvent.mReply.mString);
+ if (selOffset + selLength > textContent.Length()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p GetCurrentParagraph(), FAILED, The selection is "
+ "invalid, textContent.Length()=%u",
+ this, textContent.Length()));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Remove composing string and restore the selected string because
+ // GtkEntry doesn't remove selected string until committing, however,
+ // our editor does it. We should emulate the behavior for IME.
+ if (EditorHasCompositionString() &&
+ mDispatchedCompositionString != mSelectedString) {
+ textContent.Replace(mCompositionStart,
+ mDispatchedCompositionString.Length(), mSelectedString);
+ }
+
+ // Get only the focused paragraph, by looking for newlines
+ int32_t parStart = (selOffset == 0) ? 0 :
+ textContent.RFind("\n", false, selOffset - 1, -1) + 1;
+ int32_t parEnd = textContent.Find("\n", false, selOffset + selLength, -1);
+ if (parEnd < 0) {
+ parEnd = textContent.Length();
+ }
+ aText = nsDependentSubstring(textContent, parStart, parEnd - parStart);
+ aCursorPos = selOffset - uint32_t(parStart);
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p GetCurrentParagraph(), succeeded, aText=%s, "
+ "aText.Length()=%u, aCursorPos=%u",
+ this, NS_ConvertUTF16toUTF8(aText).get(),
+ aText.Length(), aCursorPos));
+
+ return NS_OK;
+}
+
+nsresult
+IMContextWrapper::DeleteText(GtkIMContext* aContext,
+ int32_t aOffset,
+ uint32_t aNChars)
+{
+ MOZ_LOG(gGtkIMLog, LogLevel::Info,
+ ("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), "
+ "mCompositionState=%s",
+ this, aContext, aOffset, aNChars, GetCompositionStateName()));
+
+ if (!mLastFocusedWindow) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, there are no focused window "
+ "in this module",
+ this));
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (!aNChars) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, aNChars must not be zero",
+ this));
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
+ nsEventStatus status;
+
+ // First, we should cancel current composition because editor cannot
+ // handle changing selection and deleting text.
+ uint32_t selOffset;
+ bool wasComposing = IsComposing();
+ bool editorHadCompositionString = EditorHasCompositionString();
+ if (wasComposing) {
+ selOffset = mCompositionStart;
+ if (!DispatchCompositionCommitEvent(aContext, &mSelectedString)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, quitting from DeletText",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ if (NS_WARN_IF(!EnsureToCacheSelection())) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, due to no valid selection "
+ "information",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+ selOffset = mSelection.mOffset;
+ }
+
+ // Get all text contents of the focused editor
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ mLastFocusedWindow);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
+ NS_ENSURE_TRUE(queryTextContentEvent.mSucceeded, NS_ERROR_FAILURE);
+ if (queryTextContentEvent.mReply.mString.IsEmpty()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, there is no contents",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ConvertUTF16toUTF8 utf8Str(
+ nsDependentSubstring(queryTextContentEvent.mReply.mString,
+ 0, selOffset));
+ glong offsetInUTF8Characters =
+ g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset;
+ if (offsetInUTF8Characters < 0) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, aOffset is too small for "
+ "current cursor pos (computed offset: %d)",
+ this, offsetInUTF8Characters));
+ return NS_ERROR_FAILURE;
+ }
+
+ AppendUTF16toUTF8(
+ nsDependentSubstring(queryTextContentEvent.mReply.mString, selOffset),
+ utf8Str);
+ glong countOfCharactersInUTF8 =
+ g_utf8_strlen(utf8Str.get(), utf8Str.Length());
+ glong endInUTF8Characters =
+ offsetInUTF8Characters + aNChars;
+ if (countOfCharactersInUTF8 < endInUTF8Characters) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, aNChars is too large for "
+ "current contents (content length: %d, computed end offset: %d)",
+ this, countOfCharactersInUTF8, endInUTF8Characters));
+ return NS_ERROR_FAILURE;
+ }
+
+ gchar* charAtOffset =
+ g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters);
+ gchar* charAtEnd =
+ g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters);
+
+ // Set selection to delete
+ WidgetSelectionEvent selectionEvent(true, eSetSelection,
+ mLastFocusedWindow);
+
+ nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0,
+ charAtOffset - utf8Str.get());
+ selectionEvent.mOffset =
+ NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length();
+
+ nsDependentCSubstring utf8DeletingStr(utf8Str,
+ utf8StrBeforeOffset.Length(),
+ charAtEnd - charAtOffset);
+ selectionEvent.mLength =
+ NS_ConvertUTF8toUTF16(utf8DeletingStr).Length();
+
+ selectionEvent.mReversed = false;
+ selectionEvent.mExpandToClusterBoundary = false;
+ lastFocusedWindow->DispatchEvent(&selectionEvent, status);
+
+ if (!selectionEvent.mSucceeded ||
+ lastFocusedWindow != mLastFocusedWindow ||
+ lastFocusedWindow->Destroyed()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, setting selection caused "
+ "focus change or window destroyed",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Delete the selection
+ WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete,
+ mLastFocusedWindow);
+ mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
+
+ if (!contentCommandEvent.mSucceeded ||
+ lastFocusedWindow != mLastFocusedWindow ||
+ lastFocusedWindow->Destroyed()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, deleting the selection caused "
+ "focus change or window destroyed",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!wasComposing) {
+ return NS_OK;
+ }
+
+ // Restore the composition at new caret position.
+ if (!DispatchCompositionStart(aContext)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, resterting composition start",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!editorHadCompositionString) {
+ return NS_OK;
+ }
+
+ nsAutoString compositionString;
+ GetCompositionString(aContext, compositionString);
+ if (!DispatchCompositionChangeEvent(aContext, compositionString)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p DeleteText(), FAILED, restoring composition string",
+ this));
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void
+IMContextWrapper::InitEvent(WidgetGUIEvent& aEvent)
+{
+ aEvent.mTime = PR_Now() / 1000;
+}
+
+bool
+IMContextWrapper::EnsureToCacheSelection(nsAString* aSelectedString)
+{
+ if (aSelectedString) {
+ aSelectedString->Truncate();
+ }
+
+ if (mSelection.IsValid() &&
+ (!mSelection.Collapsed() || !aSelectedString)) {
+ return true;
+ }
+
+ if (NS_WARN_IF(!mLastFocusedWindow)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p EnsureToCacheSelection(), FAILED, due to "
+ "no focused window",
+ this));
+ return false;
+ }
+
+ nsEventStatus status;
+ WidgetQueryContentEvent selection(true, eQuerySelectedText,
+ mLastFocusedWindow);
+ InitEvent(selection);
+ mLastFocusedWindow->DispatchEvent(&selection, status);
+ if (NS_WARN_IF(!selection.mSucceeded)) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p EnsureToCacheSelection(), FAILED, due to "
+ "failure of query selection event",
+ this));
+ return false;
+ }
+
+ mSelection.Assign(selection);
+ if (!mSelection.IsValid()) {
+ MOZ_LOG(gGtkIMLog, LogLevel::Error,
+ ("0x%p EnsureToCacheSelection(), FAILED, due to "
+ "failure of query selection event (invalid result)",
+ this));
+ return false;
+ }
+
+ if (!mSelection.Collapsed() && aSelectedString) {
+ aSelectedString->Assign(selection.mReply.mString);
+ }
+
+ MOZ_LOG(gGtkIMLog, LogLevel::Debug,
+ ("0x%p EnsureToCacheSelection(), Succeeded, mSelection="
+ "{ mOffset=%u, mLength=%u, mWritingMode=%s }",
+ this, mSelection.mOffset, mSelection.mLength,
+ GetWritingModeName(mSelection.mWritingMode).get()));
+ return true;
+}
+
+/******************************************************************************
+ * IMContextWrapper::Selection
+ ******************************************************************************/
+
+void
+IMContextWrapper::Selection::Assign(const IMENotification& aIMENotification)
+{
+ MOZ_ASSERT(aIMENotification.mMessage == NOTIFY_IME_OF_SELECTION_CHANGE);
+ mOffset = aIMENotification.mSelectionChangeData.mOffset;
+ mLength = aIMENotification.mSelectionChangeData.Length();
+ mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode();
+}
+
+void
+IMContextWrapper::Selection::Assign(const WidgetQueryContentEvent& aEvent)
+{
+ MOZ_ASSERT(aEvent.mMessage == eQuerySelectedText);
+ MOZ_ASSERT(aEvent.mSucceeded);
+ mOffset = aEvent.mReply.mOffset;
+ mLength = aEvent.mReply.mString.Length();
+ mWritingMode = aEvent.GetWritingMode();
+}
+
+} // namespace widget
+} // namespace mozilla