/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* vim:expandtab:shiftwidth=4:tabstop=4: */ /* 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/. */ #ifndef IMContextWrapper_h_ #define IMContextWrapper_h_ #include <gdk/gdk.h> #include <gtk/gtk.h> #include "nsString.h" #include "nsCOMPtr.h" #include "nsTArray.h" #include "nsIWidget.h" #include "mozilla/CheckedInt.h" #include "mozilla/EventForwards.h" #include "mozilla/TextEventDispatcherListener.h" #include "WritingModes.h" class nsWindow; namespace mozilla { namespace widget { class IMContextWrapper final : public TextEventDispatcherListener { public: // TextEventDispatcherListener implementation NS_DECL_ISUPPORTS NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher, const IMENotification& aNotification) override; NS_IMETHOD_(void) OnRemovedFrom( TextEventDispatcher* aTextEventDispatcher) override; NS_IMETHOD_(void) WillDispatchKeyboardEvent( TextEventDispatcher* aTextEventDispatcher, WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress, void* aData) override; public: // aOwnerWindow is a pointer of the owner window. When aOwnerWindow is // destroyed, the related IME contexts are released (i.e., IME cannot be // used with the instance after that). explicit IMContextWrapper(nsWindow* aOwnerWindow); // "Enabled" means the users can use all IMEs. // I.e., the focus is in the normal editors. bool IsEnabled() const; nsIMEUpdatePreference GetIMEUpdatePreference() const; // OnFocusWindow is a notification that aWindow is going to be focused. void OnFocusWindow(nsWindow* aWindow); // OnBlurWindow is a notification that aWindow is going to be unfocused. void OnBlurWindow(nsWindow* aWindow); // OnDestroyWindow is a notification that aWindow is going to be destroyed. void OnDestroyWindow(nsWindow* aWindow); // OnFocusChangeInGecko is a notification that an editor gets focus. void OnFocusChangeInGecko(bool aFocus); // OnSelectionChange is a notification that selection (caret) is changed // in the focused editor. void OnSelectionChange(nsWindow* aCaller, const IMENotification& aIMENotification); // OnKeyEvent is called when aWindow gets a native key press event or a // native key release event. If this returns TRUE, the key event was // filtered by IME. Otherwise, this returns FALSE. // NOTE: When the keypress event starts composition, this returns TRUE but // this dispatches keydown event before compositionstart event. bool OnKeyEvent(nsWindow* aWindow, GdkEventKey* aEvent, bool aKeyDownEventWasSent = false); // IME related nsIWidget methods. nsresult EndIMEComposition(nsWindow* aCaller); void SetInputContext(nsWindow* aCaller, const InputContext* aContext, const InputContextAction* aAction); InputContext GetInputContext(); void OnUpdateComposition(); void OnLayoutChange(); TextEventDispatcher* GetTextEventDispatcher(); protected: ~IMContextWrapper(); // Owner of an instance of this class. This should be top level window. // The owner window must release the contexts when it's destroyed because // the IME contexts need the native window. If OnDestroyWindow() is called // with the owner window, it'll release IME contexts. Otherwise, it'll // just clean up any existing composition if it's related to the destroying // child window. nsWindow* mOwnerWindow; // A last focused window in this class's context. nsWindow* mLastFocusedWindow; // Actual context. This is used for handling the user's input. GtkIMContext* mContext; // mSimpleContext is used for the password field and // the |ime-mode: disabled;| editors if sUseSimpleContext is true. // These editors disable IME. But dead keys should work. Fortunately, // the simple IM context of GTK2 support only them. GtkIMContext* mSimpleContext; // mDummyContext is a dummy context and will be used in Focus() // when the state of mEnabled means disabled. This context's IME state is // always "closed", so it closes IME forcedly. GtkIMContext* mDummyContext; // mComposingContext is not nullptr while one of mContext, mSimpleContext // and mDummyContext has composition. // XXX: We don't assume that two or more context have composition same time. GtkIMContext* mComposingContext; // IME enabled state and other things defined in InputContext. // Use following helper methods if you don't need the detail of the status. InputContext mInputContext; // mCompositionStart is the start offset of the composition string in the // current content. When <textarea> or <input> have focus, it means offset // from the first character of them. When a HTML editor has focus, it // means offset from the first character of the root element of the editor. uint32_t mCompositionStart; // mDispatchedCompositionString is the latest composition string which // was dispatched by compositionupdate event. nsString mDispatchedCompositionString; // mSelectedString is the selected string which was removed by first // compositionchange event. nsString mSelectedString; // OnKeyEvent() temporarily sets mProcessingKeyEvent to the given native // event. GdkEventKey* mProcessingKeyEvent; struct Range { uint32_t mOffset; uint32_t mLength; Range() : mOffset(UINT32_MAX) , mLength(UINT32_MAX) { } bool IsValid() const { return mOffset != UINT32_MAX; } void Clear() { mOffset = UINT32_MAX; mLength = UINT32_MAX; } }; // current target offset and length of IME composition Range mCompositionTargetRange; // mCompositionState indicates current status of composition. enum eCompositionState { eCompositionState_NotComposing, eCompositionState_CompositionStartDispatched, eCompositionState_CompositionChangeEventDispatched }; eCompositionState mCompositionState; bool IsComposing() const { return (mCompositionState != eCompositionState_NotComposing); } bool IsComposingOn(GtkIMContext* aContext) const { return IsComposing() && mComposingContext == aContext; } bool IsComposingOnCurrentContext() const { return IsComposingOn(GetCurrentContext()); } bool EditorHasCompositionString() { return (mCompositionState == eCompositionState_CompositionChangeEventDispatched); } /** * Checks if aContext is valid context for handling composition. * * @param aContext An IM context which is specified by native * composition events. * @return true if the context is valid context for * handling composition. Otherwise, false. */ bool IsValidContext(GtkIMContext* aContext) const; const char* GetCompositionStateName() { switch (mCompositionState) { case eCompositionState_NotComposing: return "NotComposing"; case eCompositionState_CompositionStartDispatched: return "CompositionStartDispatched"; case eCompositionState_CompositionChangeEventDispatched: return "CompositionChangeEventDispatched"; default: return "InvaildState"; } } struct Selection final { uint32_t mOffset; uint32_t mLength; WritingMode mWritingMode; Selection() : mOffset(UINT32_MAX) , mLength(UINT32_MAX) { } void Clear() { mOffset = UINT32_MAX; mLength = UINT32_MAX; mWritingMode = WritingMode(); } void Assign(const IMENotification& aIMENotification); void Assign(const WidgetQueryContentEvent& aSelectedTextEvent); bool IsValid() const { return mOffset != UINT32_MAX; } bool Collapsed() const { return !mLength; } uint32_t EndOffset() const { if (NS_WARN_IF(!IsValid())) { return UINT32_MAX; } CheckedInt<uint32_t> endOffset = CheckedInt<uint32_t>(mOffset) + mLength; if (NS_WARN_IF(!endOffset.isValid())) { return UINT32_MAX; } return endOffset.value(); } } mSelection; bool EnsureToCacheSelection(nsAString* aSelectedString = nullptr); // mIsIMFocused is set to TRUE when we call gtk_im_context_focus_in(). And // it's set to FALSE when we call gtk_im_context_focus_out(). bool mIsIMFocused; // mFilterKeyEvent is used by OnKeyEvent(). If the commit event should // be processed as simple key event, this is set to TRUE by the commit // handler. bool mFilterKeyEvent; // mKeyDownEventWasSent is used by OnKeyEvent() and // DispatchCompositionStart(). DispatchCompositionStart() dispatches // a keydown event if the composition start is caused by a native // keypress event. If this is true, the keydown event has been dispatched. // Then, DispatchCompositionStart() doesn't dispatch keydown event. bool mKeyDownEventWasSent; // mIsDeletingSurrounding is true while OnDeleteSurroundingNative() is // trying to delete the surrounding text. bool mIsDeletingSurrounding; // mLayoutChanged is true after OnLayoutChange() is called. This is reset // when eCompositionChange is being dispatched. bool mLayoutChanged; // mSetCursorPositionOnKeyEvent true when caret rect or position is updated // with no composition. If true, we update candidate window position // before key down bool mSetCursorPositionOnKeyEvent; // mPendingResettingIMContext becomes true if selection change notification // is received during composition but the selection change occurred before // starting the composition. In such case, we cannot notify IME of // selection change during composition because we don't want to commit // the composition in such case. However, we should notify IME of the // selection change after the composition is committed. bool mPendingResettingIMContext; // mRetrieveSurroundingSignalReceived is true after "retrieve_surrounding" // signal is received until selection is changed in Gecko. bool mRetrieveSurroundingSignalReceived; // sLastFocusedContext is a pointer to the last focused instance of this // class. When a instance is destroyed and sLastFocusedContext refers it, // this is cleared. So, this refers valid pointer always. static IMContextWrapper* sLastFocusedContext; // sUseSimpleContext indeicates if password editors and editors with // |ime-mode: disabled;| should use GtkIMContextSimple. // If true, they use GtkIMContextSimple. Otherwise, not. static bool sUseSimpleContext; // Callback methods for native IME events. These methods should call // the related instance methods simply. static gboolean OnRetrieveSurroundingCallback(GtkIMContext* aContext, IMContextWrapper* aModule); static gboolean OnDeleteSurroundingCallback(GtkIMContext* aContext, gint aOffset, gint aNChars, IMContextWrapper* aModule); static void OnCommitCompositionCallback(GtkIMContext* aContext, const gchar* aString, IMContextWrapper* aModule); static void OnChangeCompositionCallback(GtkIMContext* aContext, IMContextWrapper* aModule); static void OnStartCompositionCallback(GtkIMContext* aContext, IMContextWrapper* aModule); static void OnEndCompositionCallback(GtkIMContext* aContext, IMContextWrapper* aModule); // The instance methods for the native IME events. gboolean OnRetrieveSurroundingNative(GtkIMContext* aContext); gboolean OnDeleteSurroundingNative(GtkIMContext* aContext, gint aOffset, gint aNChars); void OnCommitCompositionNative(GtkIMContext* aContext, const gchar* aString); void OnChangeCompositionNative(GtkIMContext* aContext); void OnStartCompositionNative(GtkIMContext* aContext); void OnEndCompositionNative(GtkIMContext* aContext); /** * GetCurrentContext() returns current IM context which is chosen with the * enabled state. * WARNING: * When this class receives some signals for a composition after focus * is moved in Gecko, the result of this may be different from given * context by the signals. */ GtkIMContext* GetCurrentContext() const; /** * GetActiveContext() returns a composing context or current context. */ GtkIMContext* GetActiveContext() const { return mComposingContext ? mComposingContext : GetCurrentContext(); } // If the owner window and IM context have been destroyed, returns TRUE. bool IsDestroyed() { return !mOwnerWindow; } // Sets focus to the instance of this class. void Focus(); // Steals focus from the instance of this class. void Blur(); // Initializes the instance. void Init(); // Reset the current composition of IME. All native composition events // during this processing are ignored. void ResetIME(); // Gets the current composition string by the native APIs. void GetCompositionString(GtkIMContext* aContext, nsAString& aCompositionString); /** * Generates our text range array from current composition string. * * @param aContext A GtkIMContext which is being handled. * @param aCompositionString The data to be dispatched with * compositionchange event. */ already_AddRefed<TextRangeArray> CreateTextRangeArray(GtkIMContext* aContext, const nsAString& aCompositionString); /** * SetTextRange() initializes aTextRange with aPangoAttrIter. * * @param aPangoAttrIter An iter which represents a clause of the * composition string. * @param aUTF8CompositionString The whole composition string (UTF-8). * @param aUTF16CaretOffset The caret offset in the composition * string encoded as UTF-16. * @param aTextRange The result. * @return true if this initializes aTextRange. * Otherwise, false. */ bool SetTextRange(PangoAttrIterator* aPangoAttrIter, const gchar* aUTF8CompositionString, uint32_t aUTF16CaretOffset, TextRange& aTextRange) const; /** * ToNscolor() converts the PangoColor in aPangoAttrColor to nscolor. */ static nscolor ToNscolor(PangoAttrColor* aPangoAttrColor); /** * Move the candidate window with "fake" cursor position. * * @param aContext A GtkIMContext which is being handled. */ void SetCursorPosition(GtkIMContext* aContext); // Queries the current selection offset of the window. uint32_t GetSelectionOffset(nsWindow* aWindow); // Get current paragraph text content and cursor position nsresult GetCurrentParagraph(nsAString& aText, uint32_t& aCursorPos); /** * Delete text portion * * @param aContext A GtkIMContext which is being handled. * @param aOffset Start offset of the range to delete. * @param aNChars Count of characters to delete. It depends * on |g_utf8_strlen()| what is one character. */ nsresult DeleteText(GtkIMContext* aContext, int32_t aOffset, uint32_t aNChars); // Initializes the GUI event. void InitEvent(WidgetGUIEvent& aEvent); // Called before destroying the context to work around some platform bugs. void PrepareToDestroyContext(GtkIMContext* aContext); /** * WARNING: * Following methods dispatch gecko events. Then, the focused widget * can be destroyed, and also it can be stolen focus. If they returns * FALSE, callers cannot continue the composition. * - DispatchCompositionStart * - DispatchCompositionChangeEvent * - DispatchCompositionCommitEvent */ /** * Dispatches a composition start event. * * @param aContext A GtkIMContext which is being handled. * @return true if the focused widget is neither * destroyed nor changed. Otherwise, false. */ bool DispatchCompositionStart(GtkIMContext* aContext); /** * Dispatches a compositionchange event. * * @param aContext A GtkIMContext which is being handled. * @param aCompositionString New composition string. * @return true if the focused widget is neither * destroyed nor changed. Otherwise, false. */ bool DispatchCompositionChangeEvent(GtkIMContext* aContext, const nsAString& aCompositionString); /** * Dispatches a compositioncommit event or compositioncommitasis event. * * @param aContext A GtkIMContext which is being handled. * @param aCommitString If this is nullptr, the composition will * be committed with last dispatched data. * Otherwise, the composition will be * committed with this value. * @return true if the focused widget is neither * destroyed nor changed. Otherwise, false. */ bool DispatchCompositionCommitEvent( GtkIMContext* aContext, const nsAString* aCommitString = nullptr); }; } // namespace widget } // namespace mozilla #endif // #ifndef IMContextWrapper_h_