summaryrefslogtreecommitdiffstats
path: root/widget/TextEventDispatcher.h
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/TextEventDispatcher.h568
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_