Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#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_