diff options
Diffstat (limited to '')
-rw-r--r-- | widget/TextEventDispatcher.h | 568 |
1 files changed, 568 insertions, 0 deletions
diff --git a/widget/TextEventDispatcher.h b/widget/TextEventDispatcher.h new file mode 100644 index 0000000000..6ab131f079 --- /dev/null +++ b/widget/TextEventDispatcher.h @@ -0,0 +1,568 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 mozilla_textcompositionsynthesizer_h_ +#define mozilla_textcompositionsynthesizer_h_ + +#include "mozilla/RefPtr.h" +#include "nsString.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Maybe.h" +#include "mozilla/TextEventDispatcherListener.h" +#include "mozilla/TextRange.h" +#include "mozilla/widget/IMEData.h" +#include "WritingModes.h" + +class nsIWidget; + +namespace mozilla { +namespace widget { + +class PuppetWidget; + +/** + * TextEventDispatcher is a helper class for dispatching widget events defined + * in TextEvents.h. Currently, this is a helper for dispatching + * WidgetCompositionEvent and WidgetKeyboardEvent. This manages the behavior + * of them for conforming to DOM Level 3 Events. + * An instance of this class is created by nsIWidget instance and owned by it. + * This is typically created only by the top level widgets because only they + * handle IME. + */ + +class TextEventDispatcher final { + ~TextEventDispatcher() = default; + + NS_INLINE_DECL_REFCOUNTING(TextEventDispatcher) + + public: + explicit TextEventDispatcher(nsIWidget* aWidget); + + /** + * Initializes the instance for IME or automated test. Either IME or tests + * need to call one of them before starting composition. If they return + * NS_ERROR_ALREADY_INITIALIZED, it means that the listener already listens + * notifications from TextEventDispatcher for same purpose (for IME or tests). + * If this returns another error, the caller shouldn't keep starting + * composition. + * + * @param aListener Specify the listener to listen notifications and + * requests. This must not be null. + * NOTE: aListener is stored as weak reference in + * TextEventDispatcher. See mListener + * definition below. + */ + nsresult BeginInputTransaction(TextEventDispatcherListener* aListener); + nsresult BeginTestInputTransaction(TextEventDispatcherListener* aListener, + bool aIsAPZAware); + nsresult BeginNativeInputTransaction(); + + /** + * BeginInputTransactionFor() should be used when aPuppetWidget dispatches + * a composition or keyboard event coming from its parent process. + */ + nsresult BeginInputTransactionFor(const WidgetGUIEvent* aEvent, + PuppetWidget* aPuppetWidget); + + /** + * EndInputTransaction() should be called when the listener stops using + * the TextEventDispatcher. + * + * @param aListener The listener using the TextEventDispatcher instance. + */ + void EndInputTransaction(TextEventDispatcherListener* aListener); + + /** + * OnDestroyWidget() is called when mWidget is being destroyed. + */ + void OnDestroyWidget(); + + nsIWidget* GetWidget() const { return mWidget; } + + /** + * Return true starting from ending handling focus notification and until + * receiving blur notification. + */ + bool HasFocus() const { return mHasFocus; } + + const IMENotificationRequests& IMENotificationRequestsRef() const { + return mIMENotificationRequests; + } + + /** + * OnWidgetChangeIMENotificationRequests() is called when aWidget's + * IMENotificationRequest is maybe modified by unusual path. E.g., + * modified in an async path. + */ + void OnWidgetChangeIMENotificationRequests(nsIWidget* aWidget) { + MOZ_ASSERT(aWidget); + if (mWidget == aWidget) { + UpdateNotificationRequests(); + } + } + + /** + * GetState() returns current state of this class. + * + * @return NS_OK: Fine to compose text. + * NS_ERROR_NOT_INITIALIZED: BeginInputTransaction() or + * BeginInputTransactionForTests() + * should be called. + * NS_ERROR_NOT_AVAILABLE: The widget isn't available for + * composition. + */ + nsresult GetState() const; + + /** + * IsComposing() returns true after calling StartComposition() and before + * calling CommitComposition(). In other words, native IME has composition + * when this returns true. + */ + bool IsComposing() const { return mIsComposing; } + + /** + * IsHandlingComposition() returns true after calling StartComposition() and + * content has not handled eCompositionCommit(AsIs) event. In other words, + * our content has composition when this returns true. + */ + bool IsHandlingComposition() const { return mIsHandlingComposition; } + + /** + * IsInNativeInputTransaction() returns true if native IME handler began a + * transaction and it's not finished yet. + */ + bool IsInNativeInputTransaction() const { + return mInputTransactionType == eNativeInputTransaction; + } + + /** + * IsDispatchingEvent() returns true while this instance dispatching an event. + */ + bool IsDispatchingEvent() const { return mDispatchingEvent > 0; } + + /** + * GetPseudoIMEContext() returns pseudo native IME context if there is an + * input transaction whose type is not for native event handler. + * Otherwise, returns nullptr. + */ + void* GetPseudoIMEContext() const { + if (mInputTransactionType == eNoInputTransaction || + mInputTransactionType == eNativeInputTransaction) { + return nullptr; + } + return const_cast<TextEventDispatcher*>(this); + } + + /** + * Return writing mode at selection while this has focus. Otherwise, or + * never exists selection ranges, this returns Nothing. + */ + const Maybe<WritingMode>& MaybeWritingModeRefAtSelection() const { + return mWritingMode; + } + + /** + * MaybeQueryWritingModeAtSelection() returns writing mode at current + * selection even if this does not have focus. If this is not focused, this + * queries selection. Then, chrome script can run due to flushing the layout + * if an element in chrome has focus (but it should not cause any problem + * hopefully). + */ + MOZ_CAN_RUN_SCRIPT Maybe<WritingMode> MaybeQueryWritingModeAtSelection() + const; + + /** + * StartComposition() starts composition explicitly. + * + * @param aEventTime If this is not nullptr, WidgetCompositionEvent will + * be initialized with this. Otherwise, initialized + * with the time at initializing. + */ + nsresult StartComposition(nsEventStatus& aStatus, + const WidgetEventTime* aEventTime = nullptr); + + /** + * CommitComposition() commits composition. + * + * @param aCommitString If this is null, commits with the last composition + * string. Otherwise, commits the composition with + * this value. + * @param aEventTime If this is not nullptr, WidgetCompositionEvent will + * be initialized with this. Otherwise, initialized + * with the time at initializing. + */ + nsresult CommitComposition(nsEventStatus& aStatus, + const nsAString* aCommitString = nullptr, + const WidgetEventTime* aEventTime = nullptr); + + /** + * SetPendingCompositionString() sets new composition string which will be + * dispatched with eCompositionChange event by calling Flush(). + * + * @param aString New composition string. + */ + nsresult SetPendingCompositionString(const nsAString& aString) { + return mPendingComposition.SetString(aString); + } + + /** + * AppendClauseToPendingComposition() appends a clause information to + * the pending composition string. + * + * @param aLength Length of the clause. + * @param aTextRangeType One of TextRangeType::eRawClause, + * TextRangeType::eSelectedRawClause, + * TextRangeType::eConvertedClause or + * TextRangeType::eSelectedClause. + */ + nsresult AppendClauseToPendingComposition(uint32_t aLength, + TextRangeType aTextRangeType) { + return mPendingComposition.AppendClause(aLength, aTextRangeType); + } + + /** + * SetCaretInPendingComposition() sets caret position in the pending + * composition string and its length. This is optional. If IME doesn't + * want to show caret, it shouldn't need to call this. + * + * @param aOffset Offset of the caret in the pending composition + * string. This should not be larger than the length + * of the pending composition string. + * @param aLength Caret width. If this is 0, caret will be collapsed. + * Note that Gecko doesn't supported wide caret yet, + * therefore, this is ignored for now. + */ + nsresult SetCaretInPendingComposition(uint32_t aOffset, uint32_t aLength) { + return mPendingComposition.SetCaret(aOffset, aLength); + } + + /** + * SetPendingComposition() is useful if native IME handler already creates + * array of clauses and/or caret information. + * + * @param aString Composition string. This may include native line + * breakers since they will be replaced with XP line + * breakers automatically. + * @param aRanges This should include the ranges of clauses and/or + * a range of caret. Note that this method allows + * some ranges overlap each other and the range order + * is not from start to end. + */ + nsresult SetPendingComposition(const nsAString& aString, + const TextRangeArray* aRanges) { + return mPendingComposition.Set(aString, aRanges); + } + + /** + * FlushPendingComposition() sends the pending composition string + * to the widget of the store DOM window. Before calling this, IME needs to + * set pending composition string with SetPendingCompositionString(), + * AppendClauseToPendingComposition() and/or + * SetCaretInPendingComposition(). + * + * @param aEventTime If this is not nullptr, WidgetCompositionEvent will + * be initialized with this. Otherwise, initialized + * with the time at initializing. + */ + nsresult FlushPendingComposition( + nsEventStatus& aStatus, const WidgetEventTime* aEventTime = nullptr) { + return mPendingComposition.Flush(this, aStatus, aEventTime); + } + + /** + * ClearPendingComposition() makes this instance forget pending composition. + */ + void ClearPendingComposition() { mPendingComposition.Clear(); } + + /** + * GetPendingCompositionClauses() returns text ranges which was appended by + * AppendClauseToPendingComposition() or SetPendingComposition(). + */ + const TextRangeArray* GetPendingCompositionClauses() const { + return mPendingComposition.GetClauses(); + } + + /** + * @see nsIWidget::NotifyIME() + */ + // Calling NotifyIME may call OS's API so that everything could happen. + // We should mark it MOZ_CAN_RUN_SCRIPT later. + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult + NotifyIME(const IMENotification& aIMENotification); + + /** + * DispatchKeyboardEvent() maybe dispatches aKeyboardEvent. + * + * @param aMessage Must be eKeyDown or eKeyUp. + * Use MaybeDispatchKeypressEvents() for dispatching + * eKeyPress. + * @param aKeyboardEvent A keyboard event. + * @param aStatus If dispatching event should be marked as consumed, + * set nsEventStatus_eConsumeNoDefault. Otherwise, + * set nsEventStatus_eIgnore. After dispatching + * a event and it's consumed this returns + * nsEventStatus_eConsumeNoDefault. + * @param aData Calling this method may cause calling + * WillDispatchKeyboardEvent() of the listener. + * aData will be set to its argument. + * @return true if an event is dispatched. Otherwise, false. + */ + bool DispatchKeyboardEvent(EventMessage aMessage, + const WidgetKeyboardEvent& aKeyboardEvent, + nsEventStatus& aStatus, void* aData = nullptr); + + /** + * MaybeDispatchKeypressEvents() maybe dispatches a keypress event which is + * generated from aKeydownEvent. + * + * @param aKeyboardEvent A keyboard event. + * @param aStatus Sets the result when the caller dispatches + * aKeyboardEvent. Note that if the value is + * nsEventStatus_eConsumeNoDefault, this does NOT + * dispatch keypress events. + * When this method dispatches one or more keypress + * events and one of them is consumed, this returns + * nsEventStatus_eConsumeNoDefault. + * @param aData Calling this method may cause calling + * WillDispatchKeyboardEvent() of the listener. + * aData will be set to its argument. + * @param aNeedsCallback Set true when caller needs to initialize each + * eKeyPress event immediately before dispatch. + * Then, WillDispatchKeyboardEvent() is always called. + * @return true if one or more events are dispatched. + * Otherwise, false. + */ + bool MaybeDispatchKeypressEvents(const WidgetKeyboardEvent& aKeyboardEvent, + nsEventStatus& aStatus, + void* aData = nullptr, + bool aNeedsCallback = false); + + private: + // mWidget is owner of the instance. When this is created, this is set. + // And when mWidget is released, this is cleared by OnDestroyWidget(). + // Note that mWidget may be destroyed already (i.e., mWidget->Destroyed() may + // return true). + nsIWidget* mWidget; + // mListener is a weak reference to TextEventDispatcherListener. That might + // be referred by JS. Therefore, the listener might be difficult to release + // itself if this is a strong reference. Additionally, it's difficult to + // check if a method to uninstall the listener is called by valid instance. + // So, using weak reference is the best way in this case. + nsWeakPtr mListener; + // mIMENotificationRequests should store current IME's notification requests. + // So, this may be invalid when IME doesn't have focus. + IMENotificationRequests mIMENotificationRequests; + // mWritingMode stores writing mode at current selection starting from + // receiving focus notification and until receiving blur notification. When + // selection is changed, this is updated by every selection change + // notification. + Maybe<WritingMode> mWritingMode; + + // mPendingComposition stores new composition string temporarily. + // These values will be used for dispatching eCompositionChange event + // in Flush(). When Flush() is called, the members will be cleared + // automatically. + class PendingComposition { + public: + PendingComposition(); + nsresult SetString(const nsAString& aString); + nsresult AppendClause(uint32_t aLength, TextRangeType aTextRangeType); + nsresult SetCaret(uint32_t aOffset, uint32_t aLength); + nsresult Set(const nsAString& aString, const TextRangeArray* aRanges); + nsresult Flush(TextEventDispatcher* aDispatcher, nsEventStatus& aStatus, + const WidgetEventTime* aEventTime); + const TextRangeArray* GetClauses() const { return mClauses; } + void Clear(); + + private: + nsString mString; + RefPtr<TextRangeArray> mClauses; + TextRange mCaret; + bool mReplacedNativeLineBreakers; + + void EnsureClauseArray(); + + /** + * ReplaceNativeLineBreakers() replaces "\r\n" and "\r" to "\n" and adjust + * each clause information and the caret information. + */ + void ReplaceNativeLineBreakers(); + + /** + * AdjustRange() adjusts aRange as in the string with XP line breakers. + * + * @param aRange The reference to a range in aNativeString. + * This will be modified. + * @param aNativeString The string with native line breakers. + * This may include "\r\n" and/or "\r". + */ + static void AdjustRange(TextRange& aRange, const nsAString& aNativeString); + }; + PendingComposition mPendingComposition; + + // While dispatching an event, this is incremented. + uint16_t mDispatchingEvent; + + enum InputTransactionType : uint8_t { + // No input transaction has been started. + eNoInputTransaction, + // Input transaction for native IME or keyboard event handler. Note that + // keyboard events may be dispatched via parent process if there is. + // In remote processes, this is also used when events come from the parent + // process and are not for tests because we cannot distinguish if + // TextEventDispatcher has which type of transaction when it dispatches + // (eNativeInputTransaction or eSameProcessSyncInputTransaction). + eNativeInputTransaction, + // Input transaction for automated tests which are APZ-aware. Note that + // keyboard events may be dispatched via parent process if there is. + eAsyncTestInputTransaction, + // Input transaction for automated tests which assume events are fired + // synchronously. I.e., keyboard events are always dispatched in the + // current process. + // In remote processes, this is also used when events come from the parent + // process and are not dispatched by the instance itself for APZ-aware + // tests because this instance won't dispatch the events via the parent + // process again. + eSameProcessSyncTestInputTransaction, + // Input transaction for others (currently, only FuzzingFunctions). + // Events are fired synchronously in the process. + // XXX Should we make this async for testing default action handlers in + // the main process? + eSameProcessSyncInputTransaction + }; + + InputTransactionType mInputTransactionType; + + bool IsForTests() const { + return mInputTransactionType == eAsyncTestInputTransaction || + mInputTransactionType == eSameProcessSyncTestInputTransaction; + } + + // ShouldSendInputEventToAPZ() returns true when WidgetInputEvent should + // be dispatched via its parent process (if there is) for APZ. Otherwise, + // when the input transaction is for IME of B2G or automated tests which + // isn't APZ-aware, WidgetInputEvent should be dispatched form current + // process directly. + bool ShouldSendInputEventToAPZ() const { + switch (mInputTransactionType) { + case eNativeInputTransaction: + case eAsyncTestInputTransaction: + return true; + case eSameProcessSyncTestInputTransaction: + case eSameProcessSyncInputTransaction: + return false; + case eNoInputTransaction: + NS_WARNING( + "Why does the caller need to dispatch an event when " + "there is no input transaction?"); + return true; + default: + MOZ_CRASH("Define the behavior of new InputTransactionType"); + } + } + + // See IsComposing(). + bool mIsComposing; + + // See IsHandlingComposition(). + bool mIsHandlingComposition; + + // true while NOTIFY_IME_OF_FOCUS is received but NOTIFY_IME_OF_BLUR has not + // received yet. Otherwise, false. + bool mHasFocus; + + nsresult BeginInputTransactionInternal(TextEventDispatcherListener* aListener, + InputTransactionType aType); + + /** + * InitEvent() initializes aEvent. This must be called before dispatching + * the event. + */ + void InitEvent(WidgetGUIEvent& aEvent) const; + + /** + * DispatchEvent() dispatches aEvent on aWidget. + */ + nsresult DispatchEvent(nsIWidget* aWidget, WidgetGUIEvent& aEvent, + nsEventStatus& aStatus); + + /** + * DispatchInputEvent() dispatches aEvent on aWidget. + */ + nsresult DispatchInputEvent(nsIWidget* aWidget, WidgetInputEvent& aEvent, + nsEventStatus& aStatus); + + /** + * StartCompositionAutomaticallyIfNecessary() starts composition if it hasn't + * been started it yet. + * + * @param aStatus If it succeeded to start composition normally, this + * returns nsEventStatus_eIgnore. Otherwise, e.g., + * the composition is canceled during dispatching + * compositionstart event, this returns + * nsEventStatus_eConsumeNoDefault. In this case, + * the caller shouldn't keep doing its job. + * @param aEventTime If this is not nullptr, WidgetCompositionEvent will + * be initialized with this. Otherwise, initialized + * with the time at initializing. + * @return Only when something unexpected occurs, this returns + * an error. Otherwise, returns NS_OK even if aStatus + * is nsEventStatus_eConsumeNoDefault. + */ + nsresult StartCompositionAutomaticallyIfNecessary( + nsEventStatus& aStatus, const WidgetEventTime* aEventTime); + + /** + * DispatchKeyboardEventInternal() maybe dispatches aKeyboardEvent. + * + * @param aMessage Must be eKeyDown, eKeyUp or eKeyPress. + * @param aKeyboardEvent A keyboard event. If aMessage is eKeyPress and + * the event is for second or later character, its + * mKeyValue should be empty string. + * @param aStatus If dispatching event should be marked as consumed, + * set nsEventStatus_eConsumeNoDefault. Otherwise, + * set nsEventStatus_eIgnore. After dispatching + * a event and it's consumed this returns + * nsEventStatus_eConsumeNoDefault. + * @param aData Calling this method may cause calling + * WillDispatchKeyboardEvent() of the listener. + * aData will be set to its argument. + * @param aIndexOfKeypress This must be 0 if aMessage isn't eKeyPress or + * aKeyboard.mKeyNameIndex isn't + * KEY_NAME_INDEX_USE_STRING. Otherwise, i.e., + * when an eKeyPress event causes inputting + * text, this must be between 0 and + * mKeyValue.Length() - 1 since keypress events + * sending only one character per event. + * @param aNeedsCallback Set true when caller needs to initialize each + * eKeyPress event immediately before dispatch. + * Then, WillDispatchKeyboardEvent() is always called. + * @return true if an event is dispatched. Otherwise, false. + */ + // TODO: Mark this as MOZ_CAN_RUN_SCRIPT instead. + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool DispatchKeyboardEventInternal( + EventMessage aMessage, const WidgetKeyboardEvent& aKeyboardEvent, + nsEventStatus& aStatus, void* aData, uint32_t aIndexOfKeypress = 0, + bool aNeedsCallback = false); + + /** + * ClearNotificationRequests() clears mIMENotificationRequests. + */ + void ClearNotificationRequests(); + + /** + * UpdateNotificationRequests() updates mIMENotificationRequests with + * current state. If the instance doesn't have focus, this clears + * mIMENotificationRequests. Otherwise, updates it with both requests of + * current listener and native listener. + */ + void UpdateNotificationRequests(); +}; + +} // namespace widget +} // namespace mozilla + +#endif // #ifndef mozilla_widget_textcompositionsynthesizer_h_ |