summaryrefslogtreecommitdiffstats
path: root/widget/TextEventDispatcher.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/TextEventDispatcher.cpp989
1 files changed, 989 insertions, 0 deletions
diff --git a/widget/TextEventDispatcher.cpp b/widget/TextEventDispatcher.cpp
new file mode 100644
index 0000000000..6ecd5d355c
--- /dev/null
+++ b/widget/TextEventDispatcher.cpp
@@ -0,0 +1,989 @@
+/* -*- 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/. */
+
+#include "TextEventDispatcher.h"
+
+#include "IMEData.h"
+#include "PuppetWidget.h"
+#include "TextEvents.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsIFrame.h"
+#include "nsIWidget.h"
+#include "nsPIDOMWindow.h"
+#include "nsView.h"
+
+namespace mozilla {
+namespace widget {
+
+/******************************************************************************
+ * TextEventDispatcher
+ *****************************************************************************/
+TextEventDispatcher::TextEventDispatcher(nsIWidget* aWidget)
+ : mWidget(aWidget),
+ mDispatchingEvent(0),
+ mInputTransactionType(eNoInputTransaction),
+ mIsComposing(false),
+ mIsHandlingComposition(false),
+ mHasFocus(false) {
+ MOZ_RELEASE_ASSERT(mWidget, "aWidget must not be nullptr");
+
+ ClearNotificationRequests();
+}
+
+nsresult TextEventDispatcher::BeginInputTransaction(
+ TextEventDispatcherListener* aListener) {
+ return BeginInputTransactionInternal(aListener,
+ eSameProcessSyncInputTransaction);
+}
+
+nsresult TextEventDispatcher::BeginTestInputTransaction(
+ TextEventDispatcherListener* aListener, bool aIsAPZAware) {
+ return BeginInputTransactionInternal(
+ aListener, aIsAPZAware ? eAsyncTestInputTransaction
+ : eSameProcessSyncTestInputTransaction);
+}
+
+nsresult TextEventDispatcher::BeginNativeInputTransaction() {
+ if (NS_WARN_IF(!mWidget)) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<TextEventDispatcherListener> listener =
+ mWidget->GetNativeTextEventDispatcherListener();
+ if (NS_WARN_IF(!listener)) {
+ return NS_ERROR_FAILURE;
+ }
+ return BeginInputTransactionInternal(listener, eNativeInputTransaction);
+}
+
+nsresult TextEventDispatcher::BeginInputTransactionInternal(
+ TextEventDispatcherListener* aListener, InputTransactionType aType) {
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ if (listener == aListener && mInputTransactionType == aType) {
+ UpdateNotificationRequests();
+ return NS_OK;
+ }
+ // If this has composition or is dispatching an event, any other listener
+ // can steal ownership. Especially, if the latter case is allowed,
+ // nobody cannot begin input transaction with this if a modal dialog is
+ // opened during dispatching an event.
+ if (IsComposing() || IsDispatchingEvent()) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+ }
+ mListener = do_GetWeakReference(aListener);
+ mInputTransactionType = aType;
+ if (listener && listener != aListener) {
+ listener->OnRemovedFrom(this);
+ }
+ UpdateNotificationRequests();
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::BeginInputTransactionFor(
+ const WidgetGUIEvent* aEvent, PuppetWidget* aPuppetWidget) {
+ MOZ_ASSERT(XRE_IsContentProcess());
+ MOZ_ASSERT(!IsDispatchingEvent());
+
+ switch (aEvent->mMessage) {
+ case eKeyDown:
+ case eKeyPress:
+ case eKeyUp:
+ MOZ_ASSERT(aEvent->mClass == eKeyboardEventClass);
+ break;
+ case eCompositionStart:
+ case eCompositionChange:
+ case eCompositionCommit:
+ case eCompositionCommitAsIs:
+ MOZ_ASSERT(aEvent->mClass == eCompositionEventClass);
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aEvent->mFlags.mIsSynthesizedForTests) {
+ // If the event is for an automated test and this instance dispatched
+ // an event to the parent process, we can assume that this is already
+ // initialized properly.
+ if (mInputTransactionType == eAsyncTestInputTransaction) {
+ return NS_OK;
+ }
+ // Even if the event coming from the parent process is synthesized for
+ // tests, this process should treat it as "sync" test here because
+ // it won't be go back to the parent process.
+ nsresult rv = BeginInputTransactionInternal(
+ static_cast<TextEventDispatcherListener*>(aPuppetWidget),
+ eSameProcessSyncTestInputTransaction);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ nsresult rv = BeginNativeInputTransaction();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // Emulate modifying members which indicate the state of composition.
+ // If we need to manage more states and/or more complexly, we should create
+ // internal methods which are called by both here and each event dispatcher
+ // method of this class.
+ switch (aEvent->mMessage) {
+ case eKeyDown:
+ case eKeyPress:
+ case eKeyUp:
+ return NS_OK;
+ case eCompositionStart:
+ MOZ_ASSERT(!mIsComposing);
+ mIsComposing = mIsHandlingComposition = true;
+ return NS_OK;
+ case eCompositionChange:
+ MOZ_ASSERT(mIsComposing);
+ MOZ_ASSERT(mIsHandlingComposition);
+ mIsComposing = mIsHandlingComposition = true;
+ return NS_OK;
+ case eCompositionCommit:
+ case eCompositionCommitAsIs:
+ MOZ_ASSERT(mIsComposing);
+ MOZ_ASSERT(mIsHandlingComposition);
+ mIsComposing = false;
+ mIsHandlingComposition = true;
+ return NS_OK;
+ default:
+ MOZ_ASSERT_UNREACHABLE("You forgot to handle the event");
+ return NS_ERROR_UNEXPECTED;
+ }
+}
+void TextEventDispatcher::EndInputTransaction(
+ TextEventDispatcherListener* aListener) {
+ if (NS_WARN_IF(IsComposing()) || NS_WARN_IF(IsDispatchingEvent())) {
+ return;
+ }
+
+ mInputTransactionType = eNoInputTransaction;
+
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (NS_WARN_IF(!listener)) {
+ return;
+ }
+
+ if (NS_WARN_IF(listener != aListener)) {
+ return;
+ }
+
+ mListener = nullptr;
+ listener->OnRemovedFrom(this);
+ UpdateNotificationRequests();
+}
+
+void TextEventDispatcher::OnDestroyWidget() {
+ mWidget = nullptr;
+ mHasFocus = false;
+ ClearNotificationRequests();
+ mPendingComposition.Clear();
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ mListener = nullptr;
+ mWritingMode.reset();
+ mInputTransactionType = eNoInputTransaction;
+ if (listener) {
+ listener->OnRemovedFrom(this);
+ }
+}
+
+nsresult TextEventDispatcher::GetState() const {
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (!listener) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ if (!mWidget || mWidget->Destroyed()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return NS_OK;
+}
+
+void TextEventDispatcher::InitEvent(WidgetGUIEvent& aEvent) const {
+ aEvent.mRefPoint = LayoutDeviceIntPoint(0, 0);
+ aEvent.mFlags.mIsSynthesizedForTests = IsForTests();
+ if (aEvent.mClass != eCompositionEventClass) {
+ return;
+ }
+ void* pseudoIMEContext = GetPseudoIMEContext();
+ if (pseudoIMEContext) {
+ aEvent.AsCompositionEvent()->mNativeIMEContext.InitWithRawNativeIMEContext(
+ pseudoIMEContext);
+ }
+#ifdef DEBUG
+ else {
+ MOZ_ASSERT(!XRE_IsContentProcess(),
+ "Why did the content process start native event transaction?");
+ MOZ_ASSERT(aEvent.AsCompositionEvent()->mNativeIMEContext.IsValid(),
+ "Native IME context shouldn't be invalid");
+ }
+#endif // #ifdef DEBUG
+}
+
+Maybe<WritingMode> TextEventDispatcher::MaybeQueryWritingModeAtSelection()
+ const {
+ if (mHasFocus || mWritingMode.isSome()) {
+ return mWritingMode;
+ }
+
+ if (NS_WARN_IF(!mWidget)) {
+ return Nothing();
+ }
+
+ // If a remote content has focus and IME does not have focus, it's going to
+ // fail eQuerySelectedText in ContentCacheParent. For avoiding to waste
+ // unnecessary runtime cost and to prevent unnecessary warnings, we should
+ // not dispatch the event in the case.
+ const InputContext inputContext = mWidget->GetInputContext();
+ if (XRE_IsE10sParentProcess() && inputContext.IsOriginContentProcess() &&
+ !inputContext.mIMEState.IsEditable()) {
+ return Nothing();
+ }
+
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ mWidget);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ const_cast<TextEventDispatcher*>(this)->DispatchEvent(
+ mWidget, querySelectedTextEvent, status);
+ if (!querySelectedTextEvent.FoundSelection()) {
+ return Nothing();
+ }
+
+ return Some(querySelectedTextEvent.mReply->mWritingMode);
+}
+
+nsresult TextEventDispatcher::DispatchEvent(nsIWidget* aWidget,
+ WidgetGUIEvent& aEvent,
+ nsEventStatus& aStatus) {
+ MOZ_ASSERT(!aEvent.AsInputEvent(), "Use DispatchInputEvent()");
+
+ RefPtr<TextEventDispatcher> kungFuDeathGrip(this);
+ nsCOMPtr<nsIWidget> widget(aWidget);
+ mDispatchingEvent++;
+ nsresult rv = widget->DispatchEvent(&aEvent, aStatus);
+ mDispatchingEvent--;
+ return rv;
+}
+
+nsresult TextEventDispatcher::DispatchInputEvent(nsIWidget* aWidget,
+ WidgetInputEvent& aEvent,
+ nsEventStatus& aStatus) {
+ RefPtr<TextEventDispatcher> kungFuDeathGrip(this);
+ nsCOMPtr<nsIWidget> widget(aWidget);
+ mDispatchingEvent++;
+
+ // If the event is dispatched via nsIWidget::DispatchInputEvent(), it
+ // sends the event to the parent process first since APZ needs to handle it
+ // first. However, some callers (e.g., keyboard apps on B2G and tests
+ // expecting synchronous dispatch) don't want this to do that.
+ nsresult rv = NS_OK;
+ if (ShouldSendInputEventToAPZ()) {
+ aStatus = widget->DispatchInputEvent(&aEvent).mContentStatus;
+ } else {
+ rv = widget->DispatchEvent(&aEvent, aStatus);
+ }
+
+ mDispatchingEvent--;
+ return rv;
+}
+
+nsresult TextEventDispatcher::StartComposition(
+ nsEventStatus& aStatus, const WidgetEventTime* aEventTime) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsresult rv = GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(mIsComposing)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // When you change some members from here, you may need same change in
+ // BeginInputTransactionFor().
+ mIsComposing = mIsHandlingComposition = true;
+ WidgetCompositionEvent compositionStartEvent(true, eCompositionStart,
+ mWidget);
+ InitEvent(compositionStartEvent);
+ if (aEventTime) {
+ compositionStartEvent.AssignEventTime(*aEventTime);
+ }
+ rv = DispatchEvent(mWidget, compositionStartEvent, aStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::StartCompositionAutomaticallyIfNecessary(
+ nsEventStatus& aStatus, const WidgetEventTime* aEventTime) {
+ if (IsComposing()) {
+ return NS_OK;
+ }
+
+ nsresult rv = StartComposition(aStatus, aEventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // If started composition has already been committed, we shouldn't dispatch
+ // the compositionchange event.
+ if (!IsComposing()) {
+ aStatus = nsEventStatus_eConsumeNoDefault;
+ return NS_OK;
+ }
+
+ // Note that the widget might be destroyed during a call of
+ // StartComposition(). In such case, we shouldn't keep dispatching next
+ // event.
+ rv = GetState();
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(rv != NS_ERROR_NOT_INITIALIZED,
+ "aDispatcher must still be initialized in this case");
+ aStatus = nsEventStatus_eConsumeNoDefault;
+ return NS_OK; // Don't throw exception in this case
+ }
+
+ aStatus = nsEventStatus_eIgnore;
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::CommitComposition(
+ nsEventStatus& aStatus, const nsAString* aCommitString,
+ const WidgetEventTime* aEventTime) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsresult rv = GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // When there is no composition, caller shouldn't try to commit composition
+ // with non-existing composition string nor commit composition with empty
+ // string.
+ if (NS_WARN_IF(!IsComposing() &&
+ (!aCommitString || aCommitString->IsEmpty()))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIWidget> widget(mWidget);
+ rv = StartCompositionAutomaticallyIfNecessary(aStatus, aEventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ return NS_OK;
+ }
+
+ // When you change some members from here, you may need same change in
+ // BeginInputTransactionFor().
+
+ // End current composition and make this free for other IMEs.
+ mIsComposing = false;
+
+ EventMessage message =
+ aCommitString ? eCompositionCommit : eCompositionCommitAsIs;
+ WidgetCompositionEvent compositionCommitEvent(true, message, widget);
+ InitEvent(compositionCommitEvent);
+ if (aEventTime) {
+ compositionCommitEvent.AssignEventTime(*aEventTime);
+ }
+ if (message == eCompositionCommit) {
+ compositionCommitEvent.mData = *aCommitString;
+ // If aCommitString comes from TextInputProcessor, it may be void, but
+ // editor requires non-void string even when it's empty.
+ compositionCommitEvent.mData.SetIsVoid(false);
+ // Don't send CRLF nor CR, replace it with LF here.
+ compositionCommitEvent.mData.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ compositionCommitEvent.mData.ReplaceSubstring(u"\r"_ns, u"\n"_ns);
+ }
+ rv = DispatchEvent(widget, compositionCommitEvent, aStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::NotifyIME(
+ const IMENotification& aIMENotification) {
+ nsresult rv = NS_ERROR_NOT_IMPLEMENTED;
+
+ switch (aIMENotification.mMessage) {
+ case NOTIFY_IME_OF_FOCUS: {
+ mWritingMode = MaybeQueryWritingModeAtSelection();
+ break;
+ }
+ case NOTIFY_IME_OF_BLUR:
+ mHasFocus = false;
+ mWritingMode.reset();
+ ClearNotificationRequests();
+ break;
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ // If content handles composition events when native IME doesn't have
+ // composition, that means that we completely finished handling
+ // composition(s). Note that when focused content is in a remote
+ // process, this is sent when all dispatched composition events
+ // have been handled in the remote process.
+ if (!IsComposing()) {
+ mIsHandlingComposition = false;
+ }
+ break;
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ if (mHasFocus && aIMENotification.mSelectionChangeData.HasRange()) {
+ mWritingMode =
+ Some(aIMENotification.mSelectionChangeData.GetWritingMode());
+ }
+ break;
+ default:
+ break;
+ }
+
+ // First, send the notification to current input transaction's listener.
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ rv = listener->NotifyIME(this, aIMENotification);
+ }
+
+ if (!mWidget) {
+ return rv;
+ }
+
+ // If current input transaction isn't for native event handler, we should
+ // send the notification to the native text event dispatcher listener
+ // since native event handler may need to do something from
+ // TextEventDispatcherListener::NotifyIME() even before there is no
+ // input transaction yet. For example, native IME handler may need to
+ // create new context at receiving NOTIFY_IME_OF_FOCUS. In this case,
+ // mListener may not be initialized since input transaction should be
+ // initialized immediately before dispatching every WidgetKeyboardEvent
+ // and WidgetCompositionEvent (dispatching events always occurs after
+ // focus move).
+ nsCOMPtr<TextEventDispatcherListener> nativeListener =
+ mWidget->GetNativeTextEventDispatcherListener();
+ if (listener != nativeListener && nativeListener) {
+ switch (aIMENotification.mMessage) {
+ case REQUEST_TO_COMMIT_COMPOSITION:
+ case REQUEST_TO_CANCEL_COMPOSITION:
+ // It's not necessary to notify native IME of requests.
+ break;
+ default: {
+ // Even if current input transaction's listener returns NS_OK or
+ // something, we need to notify native IME of notifications because
+ // when user typing after TIP does something, the changed information
+ // is necessary for them.
+ nsresult rv2 = nativeListener->NotifyIME(this, aIMENotification);
+ // But return the result from current listener except when the
+ // notification isn't handled.
+ if (rv == NS_ERROR_NOT_IMPLEMENTED) {
+ rv = rv2;
+ }
+ break;
+ }
+ }
+ }
+
+ if (aIMENotification.mMessage == NOTIFY_IME_OF_FOCUS) {
+ mHasFocus = true;
+ UpdateNotificationRequests();
+ }
+
+ return rv;
+}
+
+void TextEventDispatcher::ClearNotificationRequests() {
+ mIMENotificationRequests = IMENotificationRequests();
+}
+
+void TextEventDispatcher::UpdateNotificationRequests() {
+ ClearNotificationRequests();
+
+ // If it doesn't has focus, no notifications are available.
+ if (!mHasFocus || !mWidget) {
+ return;
+ }
+
+ // If there is a listener, its requests are necessary.
+ nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ mIMENotificationRequests = listener->GetIMENotificationRequests();
+ }
+
+ // Even if this is in non-native input transaction, native IME needs
+ // requests. So, add native IME requests too.
+ if (!IsInNativeInputTransaction()) {
+ nsCOMPtr<TextEventDispatcherListener> nativeListener =
+ mWidget->GetNativeTextEventDispatcherListener();
+ if (nativeListener) {
+ mIMENotificationRequests |= nativeListener->GetIMENotificationRequests();
+ }
+ }
+}
+
+bool TextEventDispatcher::DispatchKeyboardEvent(
+ EventMessage aMessage, const WidgetKeyboardEvent& aKeyboardEvent,
+ nsEventStatus& aStatus, void* aData) {
+ return DispatchKeyboardEventInternal(aMessage, aKeyboardEvent, aStatus,
+ aData);
+}
+
+bool TextEventDispatcher::DispatchKeyboardEventInternal(
+ EventMessage aMessage, const WidgetKeyboardEvent& aKeyboardEvent,
+ nsEventStatus& aStatus, void* aData, uint32_t aIndexOfKeypress,
+ bool aNeedsCallback) {
+ // Note that this method is also used for dispatching key events on a plugin
+ // because key events on a plugin should be dispatched same as normal key
+ // events. Then, only some handlers which need to intercept key events
+ // before the focused plugin (e.g., reserved shortcut key handlers) can
+ // consume the events.
+ MOZ_ASSERT(
+ aMessage == eKeyDown || aMessage == eKeyUp || aMessage == eKeyPress,
+ "Invalid aMessage value");
+ nsresult rv = GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ // If the key shouldn't cause keypress events, don't this patch them.
+ if (aMessage == eKeyPress && !aKeyboardEvent.ShouldCauseKeypressEvents()) {
+ return false;
+ }
+
+ // Basically, key events shouldn't be dispatched during composition.
+ // Note that plugin process has different IME context. Therefore, we don't
+ // need to check our composition state when the key event is fired on a
+ // plugin.
+ if (IsComposing()) {
+ // However, if we need to behave like other browsers, we need the keydown
+ // and keyup events. Note that this behavior is also allowed by D3E spec.
+ // FYI: keypress events must not be fired during composition.
+ if (!StaticPrefs::dom_keyboardevent_dispatch_during_composition() ||
+ aMessage == eKeyPress) {
+ return false;
+ }
+ // XXX If there was mOnlyContentDispatch for this case, it might be useful
+ // because our chrome doesn't assume that key events are fired during
+ // composition.
+ }
+
+ WidgetKeyboardEvent keyEvent(true, aMessage, mWidget);
+ InitEvent(keyEvent);
+ keyEvent.AssignKeyEventData(aKeyboardEvent, false);
+ // Command arrays are not duplicated by AssignKeyEventData() due to
+ // both performance and footprint reasons. So, when TextInputProcessor
+ // emulates real text input or synthesizing keyboard events for tests,
+ // the arrays may be initialized all commands already. If so, we need to
+ // duplicate the arrays here, but we should do this only when we're
+ // dispatching eKeyPress events because BrowserParent::SendRealKeyEvent()
+ // does this only for eKeyPress event. Note that this is not required if
+ // we're in the main process because in the parent process, the edit commands
+ // will be initialized by `ExecuteEditCommands()` (when the event is handled
+ // by editor event listener) or `InitAllEditCommands()` (when the event is
+ // set to a content process). We should test whether these pathes work or
+ // not too.
+ if (XRE_IsContentProcess() && keyEvent.mIsSynthesizedByTIP) {
+ if (aMessage == eKeyPress) {
+ keyEvent.AssignCommands(aKeyboardEvent);
+ } else {
+ // Prevent retriving native edit commands if we're in a content process
+ // because only `eKeyPress` events coming from the main process have
+ // edit commands (See `BrowserParent::SendRealKeyEvent`). And also
+ // retriving edit commands from a content process requires synchonous
+ // IPC and that makes running tests slower. Therefore, we should mark
+ // the `eKeyPress` event does not need to retrieve edit commands anymore.
+ keyEvent.PreventNativeKeyBindings();
+ }
+ }
+
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ // If the key event should be dispatched as consumed event, marking it here.
+ // This is useful to prevent double action. This is intended to the system
+ // has already consumed the event but we need to dispatch the event for
+ // compatibility with older version and other browsers. So, we should not
+ // stop cross process forwarding of them.
+ keyEvent.PreventDefaultBeforeDispatch(CrossProcessForwarding::eAllow);
+ }
+
+ // Corrects each member for the specific key event type.
+ if (keyEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING) {
+ MOZ_ASSERT(!aIndexOfKeypress,
+ "aIndexOfKeypress must be 0 for non-printable key");
+ // If the keyboard event isn't caused by printable key, its charCode should
+ // be 0.
+ keyEvent.SetCharCode(0);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT_IF(aMessage == eKeyDown || aMessage == eKeyUp,
+ !aIndexOfKeypress);
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ aMessage == eKeyPress,
+ aIndexOfKeypress < std::max<size_t>(keyEvent.mKeyValue.Length(), 1));
+ char16_t ch =
+ keyEvent.mKeyValue.IsEmpty() ? 0 : keyEvent.mKeyValue[aIndexOfKeypress];
+ keyEvent.SetCharCode(static_cast<uint32_t>(ch));
+ if (aMessage == eKeyPress) {
+ // keyCode of eKeyPress events of printable keys should be always 0.
+ keyEvent.mKeyCode = 0;
+ // eKeyPress events are dispatched for every character.
+ // So, each key value of eKeyPress events should be a character.
+ if (ch) {
+ keyEvent.mKeyValue.Assign(ch);
+ } else {
+ keyEvent.mKeyValue.Truncate();
+ }
+ }
+ }
+ if (aMessage == eKeyUp) {
+ // mIsRepeat of keyup event must be false.
+ keyEvent.mIsRepeat = false;
+ }
+ // mIsComposing should be initialized later.
+ keyEvent.mIsComposing = false;
+ if (mInputTransactionType == eNativeInputTransaction) {
+ // Copy mNativeKeyEvent here because for safety for other users of
+ // AssignKeyEventData(), it doesn't copy this.
+ keyEvent.mNativeKeyEvent = aKeyboardEvent.mNativeKeyEvent;
+ } else {
+ // If it's not a keyboard event for native key event, we should ensure that
+ // mNativeKeyEvent is null.
+ keyEvent.mNativeKeyEvent = nullptr;
+ }
+ // TODO: Manage mUniqueId here.
+
+ // Request the alternative char codes for the key event.
+ // eKeyDown also needs alternative char codes because nsXBLWindowKeyHandler
+ // needs to check if a following keypress event is reserved by chrome for
+ // stopping propagation of its preceding keydown event.
+ keyEvent.mAlternativeCharCodes.Clear();
+ if ((aMessage == eKeyDown || aMessage == eKeyPress) &&
+ (aNeedsCallback || keyEvent.IsControl() || keyEvent.IsAlt() ||
+ keyEvent.IsMeta() || keyEvent.IsOS())) {
+ nsCOMPtr<TextEventDispatcherListener> listener =
+ do_QueryReferent(mListener);
+ if (listener) {
+ DebugOnly<WidgetKeyboardEvent> original(keyEvent);
+ listener->WillDispatchKeyboardEvent(this, keyEvent, aIndexOfKeypress,
+ aData);
+ MOZ_ASSERT(keyEvent.mMessage ==
+ static_cast<WidgetKeyboardEvent&>(original).mMessage);
+ MOZ_ASSERT(keyEvent.mKeyCode ==
+ static_cast<WidgetKeyboardEvent&>(original).mKeyCode);
+ MOZ_ASSERT(keyEvent.mLocation ==
+ static_cast<WidgetKeyboardEvent&>(original).mLocation);
+ MOZ_ASSERT(keyEvent.mIsRepeat ==
+ static_cast<WidgetKeyboardEvent&>(original).mIsRepeat);
+ MOZ_ASSERT(keyEvent.mIsComposing ==
+ static_cast<WidgetKeyboardEvent&>(original).mIsComposing);
+ MOZ_ASSERT(keyEvent.mKeyNameIndex ==
+ static_cast<WidgetKeyboardEvent&>(original).mKeyNameIndex);
+ MOZ_ASSERT(keyEvent.mCodeNameIndex ==
+ static_cast<WidgetKeyboardEvent&>(original).mCodeNameIndex);
+ MOZ_ASSERT(keyEvent.mKeyValue ==
+ static_cast<WidgetKeyboardEvent&>(original).mKeyValue);
+ MOZ_ASSERT(keyEvent.mCodeValue ==
+ static_cast<WidgetKeyboardEvent&>(original).mCodeValue);
+ }
+ }
+
+ if (StaticPrefs::
+ dom_keyboardevent_keypress_dispatch_non_printable_keys_only_system_group_in_content() &&
+ keyEvent.mMessage == eKeyPress &&
+ !keyEvent.ShouldKeyPressEventBeFiredOnContent()) {
+ // Note that even if we set it to true, this may be overwritten by
+ // PresShell::DispatchEventToDOM().
+ keyEvent.mFlags.mOnlySystemGroupDispatchInContent = true;
+ }
+
+ // If an editable element has focus and we're in the parent process, we should
+ // retrieve native key bindings right now because even if it matches with a
+ // reserved shortcut key, it should be handled by the editor.
+ if (XRE_IsParentProcess() && mHasFocus &&
+ (aMessage == eKeyDown || aMessage == eKeyPress)) {
+ keyEvent.InitAllEditCommands(mWritingMode);
+ }
+
+ DispatchInputEvent(mWidget, keyEvent, aStatus);
+ return true;
+}
+
+bool TextEventDispatcher::MaybeDispatchKeypressEvents(
+ const WidgetKeyboardEvent& aKeyboardEvent, nsEventStatus& aStatus,
+ void* aData, bool aNeedsCallback) {
+ // If the key event was consumed, keypress event shouldn't be fired.
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ return false;
+ }
+
+ // If the key shouldn't cause keypress events, don't fire them.
+ if (!aKeyboardEvent.ShouldCauseKeypressEvents()) {
+ return false;
+ }
+
+ // If the key isn't a printable key or just inputting one character or
+ // no character, we should dispatch only one keypress. Otherwise, i.e.,
+ // if the key is a printable key and inputs multiple characters, keypress
+ // event should be dispatched the count of inputting characters times.
+ size_t keypressCount =
+ aKeyboardEvent.mKeyNameIndex != KEY_NAME_INDEX_USE_STRING
+ ? 1
+ : std::max(static_cast<nsAString::size_type>(1),
+ aKeyboardEvent.mKeyValue.Length());
+ bool isDispatched = false;
+ bool consumed = false;
+ for (size_t i = 0; i < keypressCount; i++) {
+ aStatus = nsEventStatus_eIgnore;
+ if (!DispatchKeyboardEventInternal(eKeyPress, aKeyboardEvent, aStatus,
+ aData, i, aNeedsCallback)) {
+ // The widget must have been gone.
+ break;
+ }
+ isDispatched = true;
+ if (!consumed) {
+ consumed = (aStatus == nsEventStatus_eConsumeNoDefault);
+ }
+ }
+
+ // If one of the keypress event was consumed, return ConsumeNoDefault.
+ if (consumed) {
+ aStatus = nsEventStatus_eConsumeNoDefault;
+ }
+
+ return isDispatched;
+}
+
+/******************************************************************************
+ * TextEventDispatcher::PendingComposition
+ *****************************************************************************/
+
+TextEventDispatcher::PendingComposition::PendingComposition() { Clear(); }
+
+void TextEventDispatcher::PendingComposition::Clear() {
+ mString.Truncate();
+ mClauses = nullptr;
+ mCaret.mRangeType = TextRangeType::eUninitialized;
+ mReplacedNativeLineBreakers = false;
+}
+
+void TextEventDispatcher::PendingComposition::EnsureClauseArray() {
+ if (mClauses) {
+ return;
+ }
+ mClauses = new TextRangeArray();
+}
+
+nsresult TextEventDispatcher::PendingComposition::SetString(
+ const nsAString& aString) {
+ MOZ_ASSERT(!mReplacedNativeLineBreakers);
+ mString = aString;
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::PendingComposition::AppendClause(
+ uint32_t aLength, TextRangeType aTextRangeType) {
+ MOZ_ASSERT(!mReplacedNativeLineBreakers);
+
+ if (NS_WARN_IF(!aLength)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ switch (aTextRangeType) {
+ case TextRangeType::eRawClause:
+ case TextRangeType::eSelectedRawClause:
+ case TextRangeType::eConvertedClause:
+ case TextRangeType::eSelectedClause: {
+ EnsureClauseArray();
+ TextRange textRange;
+ textRange.mStartOffset =
+ mClauses->IsEmpty() ? 0 : mClauses->LastElement().mEndOffset;
+ textRange.mEndOffset = textRange.mStartOffset + aLength;
+ textRange.mRangeType = aTextRangeType;
+ mClauses->AppendElement(textRange);
+ return NS_OK;
+ }
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+}
+
+nsresult TextEventDispatcher::PendingComposition::SetCaret(uint32_t aOffset,
+ uint32_t aLength) {
+ MOZ_ASSERT(!mReplacedNativeLineBreakers);
+
+ mCaret.mStartOffset = aOffset;
+ mCaret.mEndOffset = mCaret.mStartOffset + aLength;
+ mCaret.mRangeType = TextRangeType::eCaret;
+ return NS_OK;
+}
+
+nsresult TextEventDispatcher::PendingComposition::Set(
+ const nsAString& aString, const TextRangeArray* aRanges) {
+ Clear();
+
+ nsresult rv = SetString(aString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!aRanges || aRanges->IsEmpty()) {
+ // Create dummy range if mString isn't empty.
+ if (!mString.IsEmpty()) {
+ rv = AppendClause(mString.Length(), TextRangeType::eRawClause);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ ReplaceNativeLineBreakers();
+ }
+ return NS_OK;
+ }
+
+ // Adjust offsets in the ranges for XP linefeed character (only \n).
+ for (uint32_t i = 0; i < aRanges->Length(); ++i) {
+ TextRange range = aRanges->ElementAt(i);
+ if (range.mRangeType == TextRangeType::eCaret) {
+ mCaret = range;
+ } else {
+ EnsureClauseArray();
+ mClauses->AppendElement(range);
+ }
+ }
+ ReplaceNativeLineBreakers();
+ return NS_OK;
+}
+
+void TextEventDispatcher::PendingComposition::ReplaceNativeLineBreakers() {
+ mReplacedNativeLineBreakers = true;
+
+ // If the composition string is empty, we don't need to do anything.
+ if (mString.IsEmpty()) {
+ return;
+ }
+
+ nsAutoString nativeString(mString);
+ // Don't expose CRLF nor CR to web contents, instead, use LF.
+ mString.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ mString.ReplaceSubstring(u"\r"_ns, u"\n"_ns);
+
+ // If the length isn't changed, we don't need to adjust any offset and length
+ // of mClauses nor mCaret.
+ if (nativeString.Length() == mString.Length()) {
+ return;
+ }
+
+ if (mClauses) {
+ for (TextRange& clause : *mClauses) {
+ AdjustRange(clause, nativeString);
+ }
+ }
+ if (mCaret.mRangeType == TextRangeType::eCaret) {
+ AdjustRange(mCaret, nativeString);
+ }
+}
+
+// static
+void TextEventDispatcher::PendingComposition::AdjustRange(
+ TextRange& aRange, const nsAString& aNativeString) {
+ TextRange nativeRange = aRange;
+ // XXX Following code wastes runtime cost because this causes computing
+ // mStartOffset for each clause from the start of composition string.
+ // If we'd make TextRange have only its length, we don't need to do
+ // this. However, this must not be so serious problem because
+ // composition string is usually short and separated as a few clauses.
+ if (nativeRange.mStartOffset > 0) {
+ nsAutoString preText(Substring(aNativeString, 0, nativeRange.mStartOffset));
+ preText.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ aRange.mStartOffset = preText.Length();
+ }
+ if (nativeRange.Length() == 0) {
+ aRange.mEndOffset = aRange.mStartOffset;
+ } else {
+ nsAutoString clause(Substring(aNativeString, nativeRange.mStartOffset,
+ nativeRange.Length()));
+ clause.ReplaceSubstring(u"\r\n"_ns, u"\n"_ns);
+ aRange.mEndOffset = aRange.mStartOffset + clause.Length();
+ }
+}
+
+nsresult TextEventDispatcher::PendingComposition::Flush(
+ TextEventDispatcher* aDispatcher, nsEventStatus& aStatus,
+ const WidgetEventTime* aEventTime) {
+ aStatus = nsEventStatus_eIgnore;
+
+ nsresult rv = aDispatcher->GetState();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (mClauses && !mClauses->IsEmpty() &&
+ mClauses->LastElement().mEndOffset != mString.Length()) {
+ NS_WARNING(
+ "Sum of length of the all clauses must be same as the string "
+ "length");
+ Clear();
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ if (mCaret.mRangeType == TextRangeType::eCaret) {
+ if (mCaret.mEndOffset > mString.Length()) {
+ NS_WARNING("Caret position is out of the composition string");
+ Clear();
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ EnsureClauseArray();
+ mClauses->AppendElement(mCaret);
+ }
+
+ // If the composition string is set without Set(), we need to replace native
+ // line breakers in the composition string with XP line breaker.
+ if (!mReplacedNativeLineBreakers) {
+ ReplaceNativeLineBreakers();
+ }
+
+ RefPtr<TextEventDispatcher> kungFuDeathGrip(aDispatcher);
+ nsCOMPtr<nsIWidget> widget(aDispatcher->mWidget);
+ WidgetCompositionEvent compChangeEvent(true, eCompositionChange, widget);
+ aDispatcher->InitEvent(compChangeEvent);
+ if (aEventTime) {
+ compChangeEvent.AssignEventTime(*aEventTime);
+ }
+ compChangeEvent.mData = mString;
+ // If mString comes from TextInputProcessor, it may be void, but editor
+ // requires non-void string even when it's empty.
+ compChangeEvent.mData.SetIsVoid(false);
+ if (mClauses) {
+ MOZ_ASSERT(!mClauses->IsEmpty(),
+ "mClauses must be non-empty array when it's not nullptr");
+ compChangeEvent.mRanges = mClauses;
+ }
+
+ // While this method dispatches a composition event, some other event handler
+ // cause more clauses to be added. So, we should clear pending composition
+ // before dispatching the event.
+ Clear();
+
+ rv = aDispatcher->StartCompositionAutomaticallyIfNecessary(aStatus,
+ aEventTime);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (aStatus == nsEventStatus_eConsumeNoDefault) {
+ return NS_OK;
+ }
+ rv = aDispatcher->DispatchEvent(widget, compChangeEvent, aStatus);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla