/* -*- 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 = false; /****************************************************************************** * 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(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(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(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; } NS_IMETHODIMP_(IMENotificationRequests) IMContextWrapper::GetIMENotificationRequests() { // While a plugin has focus, IMContextWrapper doesn't need any // notifications. if (mInputContext.mIMEState.mEnabled == IMEState::PLUGIN) { return IMENotificationRequests(); } IMENotificationRequests::Notifications notifications = IMENotificationRequests::NOTIFY_NOTHING; // If it's not enabled, we don't need position change notification. if (IsEnabled()) { notifications |= IMENotificationRequests::NOTIFY_POSITION_CHANGE; } return IMENotificationRequests(notifications); } 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) { GtkIMContext *slave = nullptr; //TODO GTK3 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. mSelectedStringRemovedByComposition.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 kungFuDeathGrip(this); RefPtr 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) { #ifdef MOZ_WIDGET_GTK 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 // . // 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: // // // // 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 // #ifdef MOZ_WIDGET_GTK // 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 mSelectedStringRemovedByComposition? // 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, mComposingContext=0x%p", this, aContext, GetCurrentContext(), mComposingContext)); // 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; } if (mComposingContext && aContext != mComposingContext) { // XXX For now, we should ignore this odd case, just logging. MOZ_LOG(gGtkIMLog, LogLevel::Warning, ("0x%p OnStartCompositionNative(), Warning, " "there is already a composing context but starting new " "composition with different context", this)); } // IME may start composition without "preedit_start" signal. Therefore, // mComposingContext will be initialized in DispatchCompositionStart(). 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), mComposingContext=0x%p", this, aContext, mComposingContext)); // 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; } // If we've not started composition with aContext, we should ignore it. if (aContext != mComposingContext) { MOZ_LOG(gGtkIMLog, LogLevel::Warning, ("0x%p OnEndCompositionNative(), Warning, " "given context doesn't match with mComposingContext", 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), " "mComposingContext=0x%p", this, aContext, mComposingContext)); // 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; } if (mComposingContext && aContext != mComposingContext) { // XXX For now, we should ignore this odd case, just logging. MOZ_LOG(gGtkIMLog, LogLevel::Warning, ("0x%p OnChangeCompositionNative(), Warning, " "given context doesn't match with composing context", this)); } nsAutoString compositionString; GetCompositionString(aContext, compositionString); if (!IsComposing() && compositionString.IsEmpty()) { MOZ_LOG(gGtkIMLog, LogLevel::Warning, ("0x%p OnChangeCompositionNative(), Warning, does nothing " "because has not started composition and composing string is " "empty", this)); 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 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]) { MOZ_LOG(gGtkIMLog, LogLevel::Warning, ("0x%p OnCommitCompositionNative(), Warning, does nothing " "because has not started composition and commit string is empty", this)); 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; } mComposingContext = static_cast(g_object_ref(aContext)); MOZ_ASSERT(mComposingContext); // Keep the last focused window alive RefPtr 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) { // A keydown event handler may change focus with the following keydown // event. In such case, we need to cancel this composition. So, we // need to store IM context now because mComposingContext may be // overwritten with different context if calling this method // recursively. // Note that we don't need to grab the context here because |context| // will be used only for checking if it's same as mComposingContext. GtkIMContext* context = mComposingContext; // 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(), preceding keydown event is " "dispatched", this)); if (lastFocusedWindow->IsDestroyed() || lastFocusedWindow != mLastFocusedWindow) { MOZ_LOG(gGtkIMLog, LogLevel::Warning, ("0x%p DispatchCompositionStart(), Warning, the focused " "widget was destroyed/changed by keydown event", this)); return false; } // If the dispatched keydown event caused moving focus and that also // caused changing active context, we need to cancel composition here. if (GetCurrentContext() != context) { MOZ_LOG(gGtkIMLog, LogLevel::Warning, ("0x%p DispatchCompositionStart(), Warning, the preceding " "keydown event causes changing active IM context", this)); if (mComposingContext == context) { // Only when the context is still composing, we should call // ResetIME() here. Otherwise, it should've already been // cleaned up. ResetIME(); } return false; } } RefPtr 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 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( &mSelectedStringRemovedByComposition))) { // 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 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 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 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 lastFocusedWindow(mLastFocusedWindow); // Emulate selection until receiving actual selection range. mSelection.CollapseTo( mCompositionStart + (aCommitString ? aCommitString->Length() : mDispatchedCompositionString.Length()), mSelection.mWritingMode); mCompositionState = eCompositionState_NotComposing; mCompositionStart = UINT32_MAX; mCompositionTargetRange.Clear(); mDispatchedCompositionString.Truncate(); mSelectedStringRemovedByComposition.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 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 = 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=%ld)", this, cursor_pos_in_chars, caretOffset)); } else { caretOffsetInUTF16 = static_cast(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( 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( 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( 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( 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(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, Length()=%u, mWritingMode=%s }", this, aContext, mCompositionTargetRange.mOffset, mCompositionTargetRange.mLength, mSelection.mOffset, mSelection.Length(), 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(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 = mSelectedStringRemovedByComposition.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.Length(); } 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 != mSelectedStringRemovedByComposition) { textContent.Replace(mCompositionStart, mDispatchedCompositionString.Length(), mSelectedStringRemovedByComposition); } // 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 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, &mSelectedStringRemovedByComposition)) { 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: %ld)", 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: %ld, computed end offset: %ld)", 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()) { if (aSelectedString) { *aSelectedString = mSelection.mString; } 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, Length()=%u, mWritingMode=%s }", this, mSelection.mOffset, mSelection.Length(), GetWritingModeName(mSelection.mWritingMode).get())); return true; } /****************************************************************************** * IMContextWrapper::Selection ******************************************************************************/ void IMContextWrapper::Selection::Assign(const IMENotification& aIMENotification) { MOZ_ASSERT(aIMENotification.mMessage == NOTIFY_IME_OF_SELECTION_CHANGE); mString = aIMENotification.mSelectionChangeData.String(); mOffset = aIMENotification.mSelectionChangeData.mOffset; mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode(); } void IMContextWrapper::Selection::Assign(const WidgetQueryContentEvent& aEvent) { MOZ_ASSERT(aEvent.mMessage == eQuerySelectedText); MOZ_ASSERT(aEvent.mSucceeded); mString = aEvent.mReply.mString.Length(); mOffset = aEvent.mReply.mOffset; mWritingMode = aEvent.GetWritingMode(); } } // namespace widget } // namespace mozilla