diff options
Diffstat (limited to 'widget/gtk/IMContextWrapper.cpp')
-rw-r--r-- | widget/gtk/IMContextWrapper.cpp | 2359 |
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 |