diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /editor/libeditor/EditorEventListener.cpp | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor/libeditor/EditorEventListener.cpp')
-rw-r--r-- | editor/libeditor/EditorEventListener.cpp | 1237 |
1 files changed, 1237 insertions, 0 deletions
diff --git a/editor/libeditor/EditorEventListener.cpp b/editor/libeditor/EditorEventListener.cpp new file mode 100644 index 0000000000..299057d17f --- /dev/null +++ b/editor/libeditor/EditorEventListener.cpp @@ -0,0 +1,1237 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 et tw=78: */ +/* 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 "EditorEventListener.h" + +#include "EditorBase.h" // for EditorBase, etc. +#include "EditorUtils.h" // for EditorUtils +#include "HTMLEditor.h" // for HTMLEditor +#include "TextEditor.h" // for TextEditor + +#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc. +#include "mozilla/AutoRestore.h" +#include "mozilla/ContentEvents.h" // for InternalFocusEvent +#include "mozilla/EventListenerManager.h" // for EventListenerManager +#include "mozilla/EventStateManager.h" // for EventStateManager +#include "mozilla/IMEStateManager.h" // for IMEStateManager +#include "mozilla/LookAndFeel.h" // for LookAndFeel +#include "mozilla/NativeKeyBindingsType.h" // for NativeKeyBindingsType +#include "mozilla/Preferences.h" // for Preferences +#include "mozilla/PresShell.h" // for PresShell +#include "mozilla/TextEvents.h" // for WidgetCompositionEvent +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/Document.h" // for Document +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/DragEvent.h" +#include "mozilla/dom/Element.h" // for Element +#include "mozilla/dom/Event.h" // for Event +#include "mozilla/dom/EventTarget.h" // for EventTarget +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/MouseEvent.h" // for MouseEvent +#include "mozilla/dom/Selection.h" + +#include "nsAString.h" +#include "nsCaret.h" // for nsCaret +#include "nsDebug.h" // for NS_WARNING, etc. +#include "nsFocusManager.h" // for nsFocusManager +#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::input +#include "nsIContent.h" // for nsIContent +#include "nsIContentInlines.h" // for nsINode::IsInDesignMode() +#include "nsIController.h" // for nsIController +#include "nsID.h" +#include "nsIFormControl.h" // for nsIFormControl, etc. +#include "nsINode.h" // for nsINode, etc. +#include "nsIWidget.h" // for nsIWidget +#include "nsLiteralString.h" // for NS_LITERAL_STRING +#include "nsPIWindowRoot.h" // for nsPIWindowRoot +#include "nsPrintfCString.h" // for nsPrintfCString +#include "nsRange.h" +#include "nsServiceManagerUtils.h" // for do_GetService +#include "nsString.h" // for nsAutoString +#include "nsQueryObject.h" // for do_QueryObject +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH +# include "nsContentUtils.h" // for nsContentUtils, etc. +# include "nsIBidiKeyboard.h" // for nsIBidiKeyboard +#endif + +#include "mozilla/dom/BrowserParent.h" + +class nsPresContext; + +namespace mozilla { + +using namespace dom; + +MOZ_CAN_RUN_SCRIPT static void DoCommandCallback(Command aCommand, + void* aData) { + Document* doc = static_cast<Document*>(aData); + nsPIDOMWindowOuter* win = doc->GetWindow(); + if (!win) { + return; + } + nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot(); + if (!root) { + return; + } + + const char* commandStr = WidgetKeyboardEvent::GetCommandStr(aCommand); + + nsCOMPtr<nsIController> controller; + root->GetControllerForCommand(commandStr, false /* for any window */, + getter_AddRefs(controller)); + if (!controller) { + return; + } + + bool commandEnabled; + if (NS_WARN_IF(NS_FAILED( + controller->IsCommandEnabled(commandStr, &commandEnabled)))) { + return; + } + if (commandEnabled) { + controller->DoCommand(commandStr); + } +} + +EditorEventListener::EditorEventListener() + : mEditorBase(nullptr), + mCommitText(false), + mInTransaction(false), + mMouseDownOrUpConsumedByIME(false) +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + , + mHaveBidiKeyboards(false), + mShouldSwitchTextDirection(false), + mSwitchToRTL(false) +#endif +{ +} + +EditorEventListener::~EditorEventListener() { + if (mEditorBase) { + NS_WARNING("We've not been uninstalled yet"); + Disconnect(); + } +} + +nsresult EditorEventListener::Connect(EditorBase* aEditorBase) { + if (NS_WARN_IF(!aEditorBase)) { + return NS_ERROR_INVALID_ARG; + } + +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard(); + if (bidiKeyboard) { + bool haveBidiKeyboards = false; + bidiKeyboard->GetHaveBidiKeyboards(&haveBidiKeyboards); + mHaveBidiKeyboards = haveBidiKeyboards; + } +#endif + + mEditorBase = aEditorBase; + + nsresult rv = InstallToEditor(); + if (NS_FAILED(rv)) { + NS_WARNING("EditorEventListener::InstallToEditor() failed"); + Disconnect(); + } + return rv; +} + +nsresult EditorEventListener::InstallToEditor() { + MOZ_ASSERT(mEditorBase, "The caller must set mEditorBase"); + + EventTarget* eventTarget = mEditorBase->GetDOMEventTarget(); + if (NS_WARN_IF(!eventTarget)) { + return NS_ERROR_FAILURE; + } + + // register the event listeners with the listener manager + EventListenerManager* eventListenerManager = + eventTarget->GetOrCreateListenerManager(); + if (NS_WARN_IF(!eventListenerManager)) { + return NS_ERROR_FAILURE; + } + + // For non-html editor, ie.TextEditor, we want to preserve + // the event handling order to ensure listeners that are + // added to <input> and <texarea> still working as expected. + EventListenerFlags flags = mEditorBase->IsHTMLEditor() + ? TrustedEventsAtSystemGroupCapture() + : TrustedEventsAtSystemGroupBubble(); +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + eventListenerManager->AddEventListenerByType(this, u"keydown"_ns, flags); + eventListenerManager->AddEventListenerByType(this, u"keyup"_ns, flags); +#endif + + eventListenerManager->AddEventListenerByType(this, u"keypress"_ns, flags); + eventListenerManager->AddEventListenerByType(this, u"dragover"_ns, flags); + eventListenerManager->AddEventListenerByType(this, u"dragleave"_ns, flags); + eventListenerManager->AddEventListenerByType(this, u"drop"_ns, flags); + // XXX We should add the mouse event listeners as system event group. + // E.g., web applications cannot prevent middle mouse paste by + // preventDefault() of click event at bubble phase. + // However, if we do so, all click handlers in any frames and frontend + // code need to check if it's editable. It makes easier create new bugs. + eventListenerManager->AddEventListenerByType(this, u"mousedown"_ns, + TrustedEventsAtCapture()); + eventListenerManager->AddEventListenerByType(this, u"mouseup"_ns, + TrustedEventsAtCapture()); + eventListenerManager->AddEventListenerByType(this, u"click"_ns, + TrustedEventsAtCapture()); + eventListenerManager->AddEventListenerByType( + this, u"auxclick"_ns, TrustedEventsAtSystemGroupCapture()); + // Focus event doesn't bubble so adding the listener to capturing phase as + // system event group. + eventListenerManager->AddEventListenerByType( + this, u"blur"_ns, TrustedEventsAtSystemGroupCapture()); + eventListenerManager->AddEventListenerByType( + this, u"focus"_ns, TrustedEventsAtSystemGroupCapture()); + eventListenerManager->AddEventListenerByType( + this, u"text"_ns, TrustedEventsAtSystemGroupBubble()); + eventListenerManager->AddEventListenerByType( + this, u"compositionstart"_ns, TrustedEventsAtSystemGroupBubble()); + eventListenerManager->AddEventListenerByType( + this, u"compositionend"_ns, TrustedEventsAtSystemGroupBubble()); + + return NS_OK; +} + +void EditorEventListener::Disconnect() { + if (DetachedFromEditor()) { + return; + } + UninstallFromEditor(); + + const OwningNonNull<EditorBase> editorBase = *mEditorBase; + mEditorBase = nullptr; + + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm) { + nsIContent* focusedContent = fm->GetFocusedElement(); + mozilla::dom::Element* root = editorBase->GetRoot(); + if (focusedContent && root && + focusedContent->IsInclusiveDescendantOf(root)) { + // Reset the Selection ancestor limiter and SelectionController state + // that EditorBase::InitializeSelection set up. + DebugOnly<nsresult> rvIgnored = editorBase->FinalizeSelection(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EditorBase::FinalizeSelection() failed, but ignored"); + } + } +} + +void EditorEventListener::UninstallFromEditor() { + CleanupDragDropCaret(); + + EventTarget* eventTarget = mEditorBase->GetDOMEventTarget(); + if (NS_WARN_IF(!eventTarget)) { + return; + } + + EventListenerManager* eventListenerManager = + eventTarget->GetOrCreateListenerManager(); + if (NS_WARN_IF(!eventListenerManager)) { + return; + } + + EventListenerFlags flags = mEditorBase->IsHTMLEditor() + ? TrustedEventsAtSystemGroupCapture() + : TrustedEventsAtSystemGroupBubble(); +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + eventListenerManager->RemoveEventListenerByType(this, u"keydown"_ns, flags); + eventListenerManager->RemoveEventListenerByType(this, u"keyup"_ns, flags); +#endif + eventListenerManager->RemoveEventListenerByType(this, u"keypress"_ns, flags); + eventListenerManager->RemoveEventListenerByType(this, u"dragover"_ns, flags); + eventListenerManager->RemoveEventListenerByType(this, u"dragleave"_ns, flags); + eventListenerManager->RemoveEventListenerByType(this, u"drop"_ns, flags); + eventListenerManager->RemoveEventListenerByType(this, u"mousedown"_ns, + TrustedEventsAtCapture()); + eventListenerManager->RemoveEventListenerByType(this, u"mouseup"_ns, + TrustedEventsAtCapture()); + eventListenerManager->RemoveEventListenerByType(this, u"click"_ns, + TrustedEventsAtCapture()); + eventListenerManager->RemoveEventListenerByType( + this, u"auxclick"_ns, TrustedEventsAtSystemGroupCapture()); + eventListenerManager->RemoveEventListenerByType( + this, u"blur"_ns, TrustedEventsAtSystemGroupCapture()); + eventListenerManager->RemoveEventListenerByType( + this, u"focus"_ns, TrustedEventsAtSystemGroupCapture()); + eventListenerManager->RemoveEventListenerByType( + this, u"text"_ns, TrustedEventsAtSystemGroupBubble()); + eventListenerManager->RemoveEventListenerByType( + this, u"compositionstart"_ns, TrustedEventsAtSystemGroupBubble()); + eventListenerManager->RemoveEventListenerByType( + this, u"compositionend"_ns, TrustedEventsAtSystemGroupBubble()); +} + +PresShell* EditorEventListener::GetPresShell() const { + MOZ_ASSERT(!DetachedFromEditor()); + return mEditorBase->GetPresShell(); +} + +nsPresContext* EditorEventListener::GetPresContext() const { + PresShell* presShell = GetPresShell(); + return presShell ? presShell->GetPresContext() : nullptr; +} + +bool EditorEventListener::EditorHasFocus() { + MOZ_ASSERT(!DetachedFromEditor()); + const Element* focusedElement = mEditorBase->GetFocusedElement(); + return focusedElement && focusedElement->IsInComposedDoc(); +} + +NS_IMPL_ISUPPORTS(EditorEventListener, nsIDOMEventListener) + +bool EditorEventListener::DetachedFromEditor() const { return !mEditorBase; } + +bool EditorEventListener::DetachedFromEditorOrDefaultPrevented( + WidgetEvent* aWidgetEvent) const { + return NS_WARN_IF(!aWidgetEvent) || DetachedFromEditor() || + aWidgetEvent->DefaultPrevented(); +} + +bool EditorEventListener::EnsureCommitComposition() { + MOZ_ASSERT(!DetachedFromEditor()); + RefPtr<EditorBase> editorBase(mEditorBase); + editorBase->CommitComposition(); + return !DetachedFromEditor(); +} + +NS_IMETHODIMP EditorEventListener::HandleEvent(Event* aEvent) { + // Let's handle each event with the message of the internal event of the + // coming event. If the DOM event was created with improper interface, + // e.g., keydown event is created with |new MouseEvent("keydown", {});|, + // its message is always 0. Therefore, we can ban such strange event easy. + // However, we need to handle strange "focus" and "blur" event. See the + // following code of this switch statement. + // NOTE: Each event handler may require specific event interface. Before + // calling it, this queries the specific interface. If it would fail, + // each event handler would just ignore the event. So, in this method, + // you don't need to check if the QI succeeded before each call. + WidgetEvent* internalEvent = aEvent->WidgetEventPtr(); + + if (DetachedFromEditor()) { + return NS_OK; + } + + // For nested documents with multiple HTMLEditor registered on different + // nsWindowRoot, make sure the HTMLEditor for the original event target + // handles the events. + if (mEditorBase->IsHTMLEditor()) { + nsCOMPtr<nsINode> originalEventTargetNode = + nsINode::FromEventTargetOrNull(aEvent->GetOriginalTarget()); + + if (originalEventTargetNode && + mEditorBase != originalEventTargetNode->OwnerDoc()->GetHTMLEditor()) { + return NS_OK; + } + if (!originalEventTargetNode && internalEvent->mMessage == eFocus && + aEvent->GetCurrentTarget()->IsRootWindow()) { + return NS_OK; + } + } + + switch (internalEvent->mMessage) { + // dragover and drop + case eDragOver: + case eDrop: { + // The editor which is registered on nsWindowRoot shouldn't handle + // drop events when it can be handled by Input or TextArea element on + // the chain. + if (aEvent->GetCurrentTarget()->IsRootWindow() && + TextControlElement::FromEventTargetOrNull( + internalEvent->GetDOMEventTarget())) { + return NS_OK; + } + // aEvent should be grabbed by the caller since this is + // nsIDOMEventListener method. However, our clang plugin cannot check it + // if we use Event::As*Event(). So, we need to grab it by ourselves. + RefPtr<DragEvent> dragEvent = aEvent->AsDragEvent(); + nsresult rv = DragOverOrDrop(dragEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::DragOverOrDrop() failed"); + return rv; + } + // DragLeave + case eDragLeave: { + RefPtr<DragEvent> dragEvent = aEvent->AsDragEvent(); + nsresult rv = DragLeave(dragEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::DragLeave() failed"); + return rv; + } +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + // keydown + case eKeyDown: { + nsresult rv = KeyDown(internalEvent->AsKeyboardEvent()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::KeyDown() failed"); + return rv; + } + // keyup + case eKeyUp: { + nsresult rv = KeyUp(internalEvent->AsKeyboardEvent()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::KeyUp() failed"); + return rv; + } +#endif // #ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + // keypress + case eKeyPress: { + nsresult rv = KeyPress(internalEvent->AsKeyboardEvent()); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::KeyPress() failed"); + return rv; + } + // mousedown + case eMouseDown: { + // EditorEventListener may receive (1) all mousedown, mouseup and click + // events, (2) only mousedown event or (3) only mouseup event. + // mMouseDownOrUpConsumedByIME is used only for ignoring click event if + // preceding mousedown and/or mouseup event is consumed by IME. + // Therefore, even if case #2 or case #3 occurs, + // mMouseDownOrUpConsumedByIME is true here. Therefore, we should always + // overwrite it here. + mMouseDownOrUpConsumedByIME = + NotifyIMEOfMouseButtonEvent(internalEvent->AsMouseEvent()); + if (mMouseDownOrUpConsumedByIME) { + return NS_OK; + } + RefPtr<MouseEvent> mouseEvent = aEvent->AsMouseEvent(); + if (NS_WARN_IF(!mouseEvent)) { + return NS_OK; + } + nsresult rv = MouseDown(mouseEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::MouseDown() failed"); + return rv; + } + // mouseup + case eMouseUp: { + // See above comment in the eMouseDown case, first. + // This code assumes that case #1 is occuring. However, if case #3 may + // occurs after case #2 and the mousedown is consumed, + // mMouseDownOrUpConsumedByIME is true even though EditorEventListener + // has not received the preceding mousedown event of this mouseup event. + // So, mMouseDownOrUpConsumedByIME may be invalid here. However, + // this is not a matter because mMouseDownOrUpConsumedByIME is referred + // only by eMouseClick case but click event is fired only in case #1. + // So, before a click event is fired, mMouseDownOrUpConsumedByIME is + // always initialized in the eMouseDown case if it's referred. + if (NotifyIMEOfMouseButtonEvent(internalEvent->AsMouseEvent())) { + mMouseDownOrUpConsumedByIME = true; + } + if (mMouseDownOrUpConsumedByIME) { + return NS_OK; + } + RefPtr<MouseEvent> mouseEvent = aEvent->AsMouseEvent(); + if (NS_WARN_IF(!mouseEvent)) { + return NS_OK; + } + nsresult rv = MouseUp(mouseEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::MouseUp() failed"); + return rv; + } + // click + case eMouseClick: { + WidgetMouseEvent* widgetMouseEvent = internalEvent->AsMouseEvent(); + // Don't handle non-primary click events + if (widgetMouseEvent->mButton != MouseButton::ePrimary) { + return NS_OK; + } + [[fallthrough]]; + } + // auxclick + case eMouseAuxClick: { + WidgetMouseEvent* widgetMouseEvent = internalEvent->AsMouseEvent(); + if (NS_WARN_IF(!widgetMouseEvent)) { + return NS_OK; + } + // If the preceding mousedown event or mouseup event was consumed, + // editor shouldn't handle this click event. + if (mMouseDownOrUpConsumedByIME) { + mMouseDownOrUpConsumedByIME = false; + widgetMouseEvent->PreventDefault(); + return NS_OK; + } + nsresult rv = MouseClick(widgetMouseEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::MouseClick() failed"); + return rv; + } + // focus + case eFocus: { + const InternalFocusEvent* focusEvent = internalEvent->AsFocusEvent(); + if (NS_WARN_IF(!focusEvent)) { + return NS_ERROR_FAILURE; + } + nsresult rv = Focus(*focusEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::Focus() failed"); + return rv; + } + // blur + case eBlur: { + const InternalFocusEvent* blurEvent = internalEvent->AsFocusEvent(); + if (NS_WARN_IF(!blurEvent)) { + return NS_ERROR_FAILURE; + } + nsresult rv = Blur(*blurEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorEventListener::Blur() failed"); + return rv; + } + // text + case eCompositionChange: { + nsresult rv = + HandleChangeComposition(internalEvent->AsCompositionEvent()); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorEventListener::HandleChangeComposition() failed"); + return rv; + } + // compositionstart + case eCompositionStart: { + nsresult rv = HandleStartComposition(internalEvent->AsCompositionEvent()); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorEventListener::HandleStartComposition() failed"); + return rv; + } + // compositionend + case eCompositionEnd: { + HandleEndComposition(internalEvent->AsCompositionEvent()); + return NS_OK; + } + default: + break; + } + +#ifdef DEBUG + nsAutoString eventType; + aEvent->GetType(eventType); + nsPrintfCString assertMessage( + "Editor doesn't handle \"%s\" event " + "because its internal event doesn't have proper message", + NS_ConvertUTF16toUTF8(eventType).get()); + NS_ASSERTION(false, assertMessage.get()); +#endif + + return NS_OK; +} + +#ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + +// This function is borrowed from Chromium's ImeInput::IsCtrlShiftPressed +bool IsCtrlShiftPressed(const WidgetKeyboardEvent* aKeyboardEvent, + bool& isRTL) { + MOZ_ASSERT(aKeyboardEvent); + // To check if a user is pressing only a control key and a right-shift key + // (or a left-shift key), we use the steps below: + // 1. Check if a user is pressing a control key and a right-shift key (or + // a left-shift key). + // 2. If the condition 1 is true, we should check if there are any other + // keys pressed at the same time. + // To ignore the keys checked in 1, we set their status to 0 before + // checking the key status. + + if (!aKeyboardEvent->IsControl()) { + return false; + } + + switch (aKeyboardEvent->mLocation) { + case eKeyLocationRight: + isRTL = true; + break; + case eKeyLocationLeft: + isRTL = false; + break; + default: + return false; + } + + // Scan the key status to find pressed keys. We should abandon changing the + // text direction when there are other pressed keys. + return !aKeyboardEvent->IsAlt() && !aKeyboardEvent->IsMeta(); +} + +// This logic is mostly borrowed from Chromium's +// RenderWidgetHostViewWin::OnKeyEvent. + +nsresult EditorEventListener::KeyUp(const WidgetKeyboardEvent* aKeyboardEvent) { + if (NS_WARN_IF(!aKeyboardEvent) || DetachedFromEditor()) { + return NS_OK; + } + + if (!mHaveBidiKeyboards) { + return NS_OK; + } + + // XXX Why doesn't this method check if it's consumed? + RefPtr<EditorBase> editorBase(mEditorBase); + if ((aKeyboardEvent->mKeyCode == NS_VK_SHIFT || + aKeyboardEvent->mKeyCode == NS_VK_CONTROL) && + mShouldSwitchTextDirection && + (editorBase->IsTextEditor() || + editorBase->AsHTMLEditor()->IsPlaintextMailComposer())) { + editorBase->SwitchTextDirectionTo(mSwitchToRTL + ? EditorBase::TextDirection::eRTL + : EditorBase::TextDirection::eLTR); + mShouldSwitchTextDirection = false; + } + return NS_OK; +} + +nsresult EditorEventListener::KeyDown( + const WidgetKeyboardEvent* aKeyboardEvent) { + if (NS_WARN_IF(!aKeyboardEvent) || DetachedFromEditor()) { + return NS_OK; + } + + if (!mHaveBidiKeyboards) { + return NS_OK; + } + + // XXX Why isn't this method check if it's consumed? + if (aKeyboardEvent->mKeyCode == NS_VK_SHIFT) { + bool switchToRTL; + if (IsCtrlShiftPressed(aKeyboardEvent, switchToRTL)) { + mShouldSwitchTextDirection = true; + mSwitchToRTL = switchToRTL; + } + } else if (aKeyboardEvent->mKeyCode != NS_VK_CONTROL) { + // In case the user presses any other key besides Ctrl and Shift + mShouldSwitchTextDirection = false; + } + return NS_OK; +} + +#endif // #ifdef HANDLE_NATIVE_TEXT_DIRECTION_SWITCH + +nsresult EditorEventListener::KeyPress(WidgetKeyboardEvent* aKeyboardEvent) { + if (NS_WARN_IF(!aKeyboardEvent)) { + return NS_OK; + } + + RefPtr<EditorBase> editorBase(mEditorBase); + if (!editorBase->IsAcceptableInputEvent(aKeyboardEvent) || + DetachedFromEditorOrDefaultPrevented(aKeyboardEvent)) { + return NS_OK; + } + + // The exposed root of our editor may have been hidden or destroyed by a + // preceding event listener. However, the destruction has not occurred yet if + // pending notifications have not been flushed yet. Therefore, before + // handling user input, we need to get the latest state and if it's now + // destroyed with the flushing, we should just ignore this event instead of + // returning error since this is just a event listener. + RefPtr<Document> document = editorBase->GetDocument(); + if (!document) { + return NS_OK; + } + document->FlushPendingNotifications(FlushType::Layout); + if (editorBase->Destroyed() || DetachedFromEditor()) { + return NS_OK; + } + + nsresult rv = editorBase->HandleKeyPressEvent(aKeyboardEvent); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::HandleKeyPressEvent() failed"); + return rv; + } + + auto GetWidget = [&]() -> nsIWidget* { + if (aKeyboardEvent->mWidget) { + return aKeyboardEvent->mWidget; + } + // If the event is created by chrome script, the widget is always nullptr. + return IMEStateManager::GetWidgetForTextInputHandling(); + }; + + if (DetachedFromEditor()) { + return NS_OK; + } + + if (LookAndFeel::GetInt(LookAndFeel::IntID::HideCursorWhileTyping)) { + if (nsPresContext* pc = GetPresContext()) { + if (nsIWidget* widget = GetWidget()) { + pc->EventStateManager()->StartHidingCursorWhileTyping(widget); + } + } + } + + if (aKeyboardEvent->DefaultPrevented()) { + return NS_OK; + } + + if (!ShouldHandleNativeKeyBindings(aKeyboardEvent)) { + return NS_OK; + } + + // Now, ask the native key bindings to handle the event. + + nsIWidget* widget = GetWidget(); + if (NS_WARN_IF(!widget)) { + return NS_OK; + } + + RefPtr<Document> doc = editorBase->GetDocument(); + + // WidgetKeyboardEvent::ExecuteEditCommands() requires non-nullptr mWidget. + // If the event is created by chrome script, it is nullptr but we need to + // execute native key bindings. Therefore, we need to set widget to + // WidgetEvent::mWidget temporarily. + AutoRestore<nsCOMPtr<nsIWidget>> saveWidget(aKeyboardEvent->mWidget); + aKeyboardEvent->mWidget = widget; + if (aKeyboardEvent->ExecuteEditCommands(NativeKeyBindingsType::RichTextEditor, + DoCommandCallback, doc)) { + aKeyboardEvent->PreventDefault(); + } + return NS_OK; +} + +nsresult EditorEventListener::MouseClick(WidgetMouseEvent* aMouseClickEvent) { + if (NS_WARN_IF(!aMouseClickEvent) || DetachedFromEditor()) { + return NS_OK; + } + // nothing to do if editor isn't editable or clicked on out of the editor. + OwningNonNull<EditorBase> editorBase = *mEditorBase; + if (editorBase->IsReadonly() || + !editorBase->IsAcceptableInputEvent(aMouseClickEvent)) { + return NS_OK; + } + + // Notifies clicking on editor to IMEStateManager even when the event was + // consumed. + if (EditorHasFocus()) { + if (RefPtr<nsPresContext> presContext = GetPresContext()) { + RefPtr<Element> focusedElement = mEditorBase->GetFocusedElement(); + IMEStateManager::OnClickInEditor(*presContext, focusedElement, + *aMouseClickEvent); + if (DetachedFromEditor()) { + return NS_OK; + } + } + } + + if (DetachedFromEditorOrDefaultPrevented(aMouseClickEvent)) { + // We're done if 'preventdefault' is true (see for example bug 70698). + return NS_OK; + } + + // If we got a mouse down inside the editing area, we should force the + // IME to commit before we change the cursor position. + if (!EnsureCommitComposition()) { + return NS_OK; + } + + // XXX The following code is hack for our buggy "click" and "auxclick" + // implementation. "auxclick" event was added recently, however, + // any non-primary button click event handlers in our UI still keep + // listening to "click" events. Additionally, "auxclick" event is + // fired after "click" events and even if we do this in the system event + // group, middle click opens new tab before us. Therefore, we need to + // handle middle click at capturing phase of the default group even + // though this makes web apps cannot prevent middle click paste with + // calling preventDefault() of "click" nor "auxclick". + + if (aMouseClickEvent->mButton != MouseButton::eMiddle || + !WidgetMouseEvent::IsMiddleClickPasteEnabled()) { + return NS_OK; + } + + RefPtr<PresShell> presShell = GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return NS_OK; + } + nsPresContext* presContext = GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return NS_OK; + } + MOZ_ASSERT(!aMouseClickEvent->DefaultPrevented()); + nsEventStatus status = nsEventStatus_eIgnore; + RefPtr<EventStateManager> esm = presContext->EventStateManager(); + DebugOnly<nsresult> rvIgnored = esm->HandleMiddleClickPaste( + presShell, aMouseClickEvent, &status, editorBase); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "EventStateManager::HandleMiddleClickPaste() failed, but ignored"); + if (status == nsEventStatus_eConsumeNoDefault) { + // We no longer need to StopImmediatePropagation here since + // ClickHandlerChild.sys.mjs checks for and ignores editables, so won't + // re-handle the event + aMouseClickEvent->PreventDefault(); + } + return NS_OK; +} + +bool EditorEventListener::NotifyIMEOfMouseButtonEvent( + WidgetMouseEvent* aMouseEvent) { + MOZ_ASSERT(aMouseEvent); + + if (!EditorHasFocus()) { + return false; + } + + RefPtr<nsPresContext> presContext = GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return false; + } + RefPtr<Element> focusedElement = mEditorBase->GetFocusedElement(); + return IMEStateManager::OnMouseButtonEventInEditor( + *presContext, focusedElement, *aMouseEvent); +} + +nsresult EditorEventListener::MouseDown(MouseEvent* aMouseEvent) { + // FYI: We don't need to check if it's already consumed here because + // we need to commit composition at mouse button operation. + // FYI: This may be called by HTMLEditorEventListener::MouseDown() even + // when the event is not acceptable for committing composition. + if (DetachedFromEditor()) { + return NS_OK; + } + Unused << EnsureCommitComposition(); + return NS_OK; +} + +/** + * Drag event implementation + */ + +void EditorEventListener::RefuseToDropAndHideCaret(DragEvent* aDragEvent) { + MOZ_ASSERT(aDragEvent->WidgetEventPtr()->mFlags.mInSystemGroup); + + aDragEvent->PreventDefault(); + aDragEvent->StopImmediatePropagation(); + DataTransfer* dataTransfer = aDragEvent->GetDataTransfer(); + if (dataTransfer) { + dataTransfer->SetDropEffectInt(nsIDragService::DRAGDROP_ACTION_NONE); + } + if (mCaret) { + mCaret->SetVisible(false); + } +} + +nsresult EditorEventListener::DragOverOrDrop(DragEvent* aDragEvent) { + MOZ_ASSERT(aDragEvent); + MOZ_ASSERT(aDragEvent->WidgetEventPtr()->mMessage == eDrop || + aDragEvent->WidgetEventPtr()->mMessage == eDragOver); + + if (aDragEvent->WidgetEventPtr()->mMessage == eDrop) { + CleanupDragDropCaret(); + MOZ_ASSERT(!mCaret); + } else { + InitializeDragDropCaret(); + MOZ_ASSERT(mCaret); + } + + if (DetachedFromEditorOrDefaultPrevented(aDragEvent->WidgetEventPtr())) { + return NS_OK; + } + + int32_t dropOffset = -1; + nsCOMPtr<nsIContent> dropParentContent = + aDragEvent->GetRangeParentContentAndOffset(&dropOffset); + if (NS_WARN_IF(!dropParentContent) || NS_WARN_IF(dropOffset < 0)) { + return NS_ERROR_FAILURE; + } + if (DetachedFromEditor()) { + RefuseToDropAndHideCaret(aDragEvent); + return NS_OK; + } + + bool notEditable = + !dropParentContent->IsEditable() || mEditorBase->IsReadonly(); + + // First of all, hide caret if we won't insert the drop data into the editor + // obviously. + if (mCaret && (IsFileControlTextBox() || notEditable)) { + mCaret->SetVisible(false); + } + + // If we're a native anonymous <input> element in <input type="file">, + // we don't need to handle the drop. + if (IsFileControlTextBox()) { + return NS_OK; + } + + // If the drop target isn't ediable, the drop should be handled by the + // element. + if (notEditable) { + // If we're a text control element which is readonly or disabled, + // we should refuse to drop. + if (mEditorBase->IsTextEditor()) { + RefuseToDropAndHideCaret(aDragEvent); + return NS_OK; + } + // Otherwise, we shouldn't handle the drop. + return NS_OK; + } + + // If the drag event does not have any data which we can handle, we should + // refuse to drop even if some parents can handle it because user may be + // trying to drop it on us, not our parent. For example, users don't want + // to drop HTML data to parent contenteditable element if they drop it on + // a child <input> element. + if (!DragEventHasSupportingData(aDragEvent)) { + RefuseToDropAndHideCaret(aDragEvent); + return NS_OK; + } + + // If we don't accept the data drop at the point, for example, while dragging + // selection, it's not allowed dropping on its selection ranges. In this + // case, any parents shouldn't handle the drop instead of us, for example, + // dropping text shouldn't be treated as URL and load new page. + if (!CanInsertAtDropPosition(aDragEvent)) { + RefuseToDropAndHideCaret(aDragEvent); + return NS_OK; + } + + WidgetDragEvent* asWidgetEvent = aDragEvent->WidgetEventPtr()->AsDragEvent(); + AutoRestore<bool> inHTMLEditorEventListener( + asWidgetEvent->mInHTMLEditorEventListener); + if (mEditorBase->IsHTMLEditor()) { + asWidgetEvent->mInHTMLEditorEventListener = true; + } + aDragEvent->PreventDefault(); + + aDragEvent->StopImmediatePropagation(); + + if (asWidgetEvent->mMessage == eDrop) { + RefPtr<EditorBase> editorBase = mEditorBase; + nsresult rv = editorBase->HandleDropEvent(aDragEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::HandleDropEvent() failed"); + return rv; + } + + MOZ_ASSERT(asWidgetEvent->mMessage == eDragOver); + + // If we handle the dragged item, we need to adjust drop effect here + // because once DataTransfer is retrieved, DragEvent has initialized it + // with nsContentUtils::SetDataTransferInEvent() but it does not check + // whether the content is movable or not. + DataTransfer* dataTransfer = aDragEvent->GetDataTransfer(); + if (dataTransfer && + dataTransfer->DropEffectInt() == nsIDragService::DRAGDROP_ACTION_MOVE) { + nsCOMPtr<nsINode> dragSource = dataTransfer->GetMozSourceNode(); + if (dragSource && !dragSource->IsEditable()) { + // In this case, we shouldn't allow "move" because the drag source + // isn't editable. + dataTransfer->SetDropEffectInt( + nsContentUtils::FilterDropEffect(nsIDragService::DRAGDROP_ACTION_COPY, + dataTransfer->EffectAllowedInt())); + } + } + + if (!mCaret) { + return NS_OK; + } + + mCaret->SetVisible(true); + mCaret->SetCaretPosition(dropParentContent, dropOffset); + + return NS_OK; +} + +void EditorEventListener::InitializeDragDropCaret() { + if (mCaret) { + return; + } + + RefPtr<PresShell> presShell = GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return; + } + + mCaret = new nsCaret(); + DebugOnly<nsresult> rvIgnored = mCaret->Init(presShell); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "nsCaret::Init() failed, but ignored"); + mCaret->SetCaretReadOnly(true); + // This is to avoid the requirement that the Selection is Collapsed which + // it can't be when dragging a selection in the same shell. + // See nsCaret::IsVisible(). + mCaret->SetVisibilityDuringSelection(true); + + presShell->SetCaret(mCaret); +} + +void EditorEventListener::CleanupDragDropCaret() { + if (!mCaret) { + return; + } + + mCaret->SetVisible(false); // hide it, so that it turns off its timer + + RefPtr<PresShell> presShell = GetPresShell(); + if (presShell) { + presShell->RestoreCaret(); + } + + mCaret->Terminate(); + mCaret = nullptr; +} + +nsresult EditorEventListener::DragLeave(DragEvent* aDragEvent) { + // XXX If aDragEvent was created by chrome script, its defaultPrevented + // may be true, though. We shouldn't handle such event but we don't + // have a way to distinguish if coming event is created by chrome script. + NS_WARNING_ASSERTION(!aDragEvent->WidgetEventPtr()->DefaultPrevented(), + "eDragLeave shouldn't be cancelable"); + if (NS_WARN_IF(!aDragEvent) || DetachedFromEditor()) { + return NS_OK; + } + + CleanupDragDropCaret(); + + return NS_OK; +} + +bool EditorEventListener::DragEventHasSupportingData( + DragEvent* aDragEvent) const { + MOZ_ASSERT( + !DetachedFromEditorOrDefaultPrevented(aDragEvent->WidgetEventPtr())); + MOZ_ASSERT(aDragEvent->GetDataTransfer()); + + // Plaintext editors only support dropping text. Otherwise, HTML and files + // can be dropped as well. + DataTransfer* dataTransfer = aDragEvent->GetDataTransfer(); + if (!dataTransfer) { + NS_WARNING("No data transfer returned"); + return false; + } + return dataTransfer->HasType(NS_LITERAL_STRING_FROM_CSTRING(kTextMime)) || + dataTransfer->HasType( + NS_LITERAL_STRING_FROM_CSTRING(kMozTextInternal)) || + (mEditorBase->IsHTMLEditor() && + !mEditorBase->AsHTMLEditor()->IsPlaintextMailComposer() && + (dataTransfer->HasType(NS_LITERAL_STRING_FROM_CSTRING(kHTMLMime)) || + dataTransfer->HasType(NS_LITERAL_STRING_FROM_CSTRING(kFileMime)))); +} + +bool EditorEventListener::CanInsertAtDropPosition(DragEvent* aDragEvent) { + MOZ_ASSERT( + !DetachedFromEditorOrDefaultPrevented(aDragEvent->WidgetEventPtr())); + MOZ_ASSERT(!mEditorBase->IsReadonly()); + MOZ_ASSERT(DragEventHasSupportingData(aDragEvent)); + + DataTransfer* dataTransfer = aDragEvent->GetDataTransfer(); + if (NS_WARN_IF(!dataTransfer)) { + return false; + } + + // If there is no source node, this is probably an external drag and the + // drop is allowed. The later checks rely on checking if the drag target + // is the same as the drag source. + nsCOMPtr<nsINode> sourceNode = dataTransfer->GetMozSourceNode(); + if (!sourceNode) { + return true; + } + + // There is a source node, so compare the source documents and this document. + // Disallow drops on the same document. + + RefPtr<Document> targetDocument = mEditorBase->GetDocument(); + if (NS_WARN_IF(!targetDocument)) { + return false; + } + + RefPtr<Document> sourceDocument = sourceNode->OwnerDoc(); + + // If the source and the dest are not same document, allow to drop it always. + if (targetDocument != sourceDocument) { + return true; + } + + // If the source node is a remote browser, treat this as coming from a + // different document and allow the drop. + if (BrowserParent::GetFrom(nsIContent::FromNode(sourceNode))) { + return true; + } + + RefPtr<Selection> selection = mEditorBase->GetSelection(); + if (!selection) { + return false; + } + + // If selection is collapsed, allow to drop it always. + if (selection->IsCollapsed()) { + return true; + } + + int32_t dropOffset = -1; + nsCOMPtr<nsIContent> dropParentContent = + aDragEvent->GetRangeParentContentAndOffset(&dropOffset); + if (!dropParentContent || NS_WARN_IF(dropOffset < 0) || + NS_WARN_IF(DetachedFromEditor())) { + return false; + } + + return !nsContentUtils::IsPointInSelection(*selection, *dropParentContent, + dropOffset); +} + +nsresult EditorEventListener::HandleStartComposition( + WidgetCompositionEvent* aCompositionStartEvent) { + if (NS_WARN_IF(!aCompositionStartEvent)) { + return NS_ERROR_FAILURE; + } + if (DetachedFromEditor()) { + return NS_OK; + } + RefPtr<EditorBase> editorBase(mEditorBase); + if (!editorBase->IsAcceptableInputEvent(aCompositionStartEvent)) { + return NS_OK; + } + // Although, "compositionstart" should be cancelable, but currently, + // eCompositionStart event coming from widget is not cancelable. + MOZ_ASSERT(!aCompositionStartEvent->DefaultPrevented(), + "eCompositionStart shouldn't be cancelable"); + nsresult rv = editorBase->OnCompositionStart(*aCompositionStartEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::OnCompositionStart() failed"); + return rv; +} + +nsresult EditorEventListener::HandleChangeComposition( + WidgetCompositionEvent* aCompositionChangeEvent) { + if (NS_WARN_IF(!aCompositionChangeEvent)) { + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(!aCompositionChangeEvent->DefaultPrevented(), + "eCompositionChange event shouldn't be cancelable"); + if (DetachedFromEditor()) { + return NS_OK; + } + RefPtr<EditorBase> editorBase(mEditorBase); + if (!editorBase->IsAcceptableInputEvent(aCompositionChangeEvent)) { + return NS_OK; + } + + // if we are readonly, then do nothing. + if (editorBase->IsReadonly()) { + return NS_OK; + } + + nsresult rv = editorBase->OnCompositionChange(*aCompositionChangeEvent); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::OnCompositionChange() failed"); + return rv; +} + +void EditorEventListener::HandleEndComposition( + WidgetCompositionEvent* aCompositionEndEvent) { + if (NS_WARN_IF(!aCompositionEndEvent) || DetachedFromEditor()) { + return; + } + RefPtr<EditorBase> editorBase(mEditorBase); + if (!editorBase->IsAcceptableInputEvent(aCompositionEndEvent)) { + return; + } + MOZ_ASSERT(!aCompositionEndEvent->DefaultPrevented(), + "eCompositionEnd shouldn't be cancelable"); + + editorBase->OnCompositionEnd(*aCompositionEndEvent); +} + +nsresult EditorEventListener::Focus(const InternalFocusEvent& aFocusEvent) { + if (DetachedFromEditor()) { + return NS_OK; + } + + nsCOMPtr<nsINode> originalEventTargetNode = + nsINode::FromEventTargetOrNull(aFocusEvent.GetOriginalDOMEventTarget()); + if (NS_WARN_IF(!originalEventTargetNode)) { + return NS_ERROR_UNEXPECTED; + } + + // If the target is a document node but it's not editable, we should + // ignore it because actual focused element's event is going to come. + if (originalEventTargetNode->IsDocument()) { + if (!originalEventTargetNode->IsInDesignMode()) { + return NS_OK; + } + } + // We should not receive focus events whose target is not a content node + // unless the node is a document node. + else if (NS_WARN_IF(!originalEventTargetNode->IsContent())) { + return NS_OK; + } + + const OwningNonNull<EditorBase> editorBase(*mEditorBase); + DebugOnly<nsresult> rvIgnored = editorBase->OnFocus(*originalEventTargetNode); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::OnFocus() failed"); + return NS_OK; // Don't return error code to the event listener manager. +} + +nsresult EditorEventListener::Blur(const InternalFocusEvent& aBlurEvent) { + if (DetachedFromEditor()) { + return NS_OK; + } + + DebugOnly<nsresult> rvIgnored = mEditorBase->OnBlur(aBlurEvent.mTarget); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), "EditorBase::OnBlur() failed"); + return NS_OK; // Don't return error code to the event listener manager. +} + +bool EditorEventListener::IsFileControlTextBox() { + MOZ_ASSERT(!DetachedFromEditor()); + + RefPtr<EditorBase> editorBase(mEditorBase); + Element* rootElement = editorBase->GetRoot(); + if (!rootElement || !rootElement->ChromeOnlyAccess()) { + return false; + } + nsIContent* parent = rootElement->FindFirstNonChromeOnlyAccessContent(); + if (!parent || !parent->IsHTMLElement(nsGkAtoms::input)) { + return false; + } + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(parent); + return formControl->ControlType() == FormControlType::InputFile; +} + +bool EditorEventListener::ShouldHandleNativeKeyBindings( + WidgetKeyboardEvent* aKeyboardEvent) { + MOZ_ASSERT(!DetachedFromEditor()); + + // Only return true if the target of the event is a desendant of the active + // editing host in order to match the similar decision made in + // nsXBLWindowKeyHandler. + // Note that IsAcceptableInputEvent doesn't check for the active editing + // host for keyboard events, otherwise this check would have been + // unnecessary. IsAcceptableInputEvent currently makes a similar check for + // mouse events. + + nsCOMPtr<nsIContent> targetContent = nsIContent::FromEventTargetOrNull( + aKeyboardEvent->GetOriginalDOMEventTarget()); + if (NS_WARN_IF(!targetContent)) { + return false; + } + + RefPtr<HTMLEditor> htmlEditor = HTMLEditor::GetFrom(mEditorBase); + if (!htmlEditor) { + return false; + } + + if (htmlEditor->IsInDesignMode()) { + // Don't need to perform any checks in designMode documents. + return true; + } + + nsIContent* editingHost = htmlEditor->ComputeEditingHost(); + if (!editingHost) { + return false; + } + + return targetContent->IsInclusiveDescendantOf(editingHost); +} + +} // namespace mozilla |