Bug 1461708 - part 8: Make EventStateManager handle middle click paste as a default action of mouseup event r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Wed, 10 Oct 2018 12:06:17 +0000
changeset 498899 9e84f8d5b086020dba510678222a6c3ec568edf2
parent 498898 0f2549de4e47ed8570b1e0a3f368bc82c17895b9
child 498900 0f1d5395f8013b4dafcd4c849a8cccdcf3c26587
child 498968 f624eab872101f974a3da00d4d19257a755ced1c
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1461708
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1461708 - part 8: Make EventStateManager handle middle click paste as a default action of mouseup event r=smaug This patch makes EventStateManager handle middle click paste as a default action. Unfortunately, we cannot remove the call of HandleMiddleClickPaste() in EditorEventListener because it's important to consume middle click event before any elements in the editor. For example, if clicked HTMLEditor has non-editable <a href> element, middle click event needs to be handled by the editor rather than contentAreaUtils which handles click events of <a href> elements. The cause of this kind of issues is, any click event handlers which handle non-primary button events still listen to "click" events. Therefore, this patch makes HandleMiddleClickPaste() do nothing if the mouseup event is fired on an editor. Differential Revision: https://phabricator.services.mozilla.com/D7855
dom/events/EventStateManager.cpp
dom/events/EventStateManager.h
dom/tests/mochitest/general/test_clipboard_events.html
editor/libeditor/EditorEventListener.cpp
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -3,16 +3,17 @@
 /* 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 "mozilla/Attributes.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/EventStates.h"
+#include "mozilla/HTMLEditor.h"
 #include "mozilla/IMEStateManager.h"
 #include "mozilla/MiscEvents.h"
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/TextComposition.h"
 #include "mozilla/TextEditor.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TouchEvents.h"
@@ -36,16 +37,17 @@
 #include "nsCopySupport.h"
 #include "nsFocusManager.h"
 #include "nsGenericHTMLElement.h"
 #include "nsIClipboard.h"
 #include "nsIContent.h"
 #include "nsIContentInlines.h"
 #include "nsIDocument.h"
 #include "nsIFrame.h"
+#include "nsITextControlElement.h"
 #include "nsIWidget.h"
 #include "nsPresContext.h"
 #include "nsIPresShell.h"
 #include "nsGkAtoms.h"
 #include "nsIFormControl.h"
 #include "nsComboboxControlFrame.h"
 #include "nsIScrollableFrame.h"
 #include "nsIDOMXULControlElement.h"
@@ -5011,16 +5013,22 @@ EventStateManager::InitAndDispatchClickE
   }
 
   // Use local event status for each click event dispatching since it'll be
   // cleared by EventStateManager::PreHandleEvent().  Therefore, dispatching
   // an event means that previous event status will be ignored.
   nsEventStatus status = nsEventStatus_eIgnore;
   nsresult rv = aPresShell->HandleEventWithTarget(&event, targetFrame,
                                                   target, &status);
+  // Copy mMultipleActionsPrevented flag from a click event to the mouseup
+  // event only when it's set to true.  It may be set to true if an editor has
+  // already handled it.  This is important to avoid two or more default
+  // actions handled here.
+  aMouseUpEvent->mFlags.mMultipleActionsPrevented |=
+    event.mFlags.mMultipleActionsPrevented;
   // If current status is nsEventStatus_eConsumeNoDefault, we don't need to
   // overwrite it.
   if (*aStatus == nsEventStatus_eConsumeNoDefault) {
     return rv;
   }
   // If new status is nsEventStatus_eConsumeNoDefault or
   // nsEventStatus_eConsumeDoDefault, use it.
   if (status == nsEventStatus_eConsumeNoDefault ||
@@ -5055,21 +5063,56 @@ EventStateManager::PostHandleMouseUp(Wid
     mouseUpContent = mouseUpContent->GetFlattenedTreeParent();
   }
 
   if (!mouseUpContent && !mCurrentTarget && !aOverrideClickTarget) {
     return NS_OK;
   }
 
   // Fire click events if the event target is still available.
-  nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, aStatus,
+  // Note that do not include the eMouseUp event's status since we ignore it
+  // for compatibility with the other browsers.
+  nsEventStatus status = nsEventStatus_eIgnore;
+  nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, &status,
                                     mouseUpContent, aOverrideClickTarget);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
+
+  // Do not do anything if preceding click events are consumed.
+  // Note that Chromium dispatches "paste" event and actually pates clipboard
+  // text into focused editor even if the preceding click events are consumed.
+  // However, this is different from our traditional behavior and does not
+  // conform to DOM events.  If we need to keep compatibility with Chromium,
+  // we should change it later.
+  if (status == nsEventStatus_eConsumeNoDefault) {
+    *aStatus = nsEventStatus_eConsumeNoDefault;
+    return NS_OK;
+  }
+
+  // Handle middle click paste if it's enabled and the mouse button is middle.
+  if (aMouseUpEvent->button != WidgetMouseEventBase::eMiddleButton ||
+      !WidgetMouseEvent::IsMiddleClickPasteEnabled()) {
+    return NS_OK;
+  }
+  DebugOnly<nsresult> rvIgnored =
+    HandleMiddleClickPaste(presShell, aMouseUpEvent, &status, nullptr);
+  NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+                       "Failed to paste for a middle click");
+
+  // If new status is nsEventStatus_eConsumeNoDefault or
+  // nsEventStatus_eConsumeDoDefault, use it.
+  if (*aStatus != nsEventStatus_eConsumeNoDefault &&
+      (status == nsEventStatus_eConsumeNoDefault ||
+       status == nsEventStatus_eConsumeDoDefault)) {
+    *aStatus = status;
+  }
+
+  // Don't return error even if middle mouse paste fails since we haven't
+  // handled it here.
   return NS_OK;
 }
 
 nsresult
 EventStateManager::DispatchClickEvents(nsIPresShell* aPresShell,
                                        WidgetMouseEvent* aMouseUpEvent,
                                        nsEventStatus* aStatus,
                                        nsIContent* aMouseUpContent,
@@ -5125,25 +5168,45 @@ EventStateManager::DispatchClickEvents(n
 nsresult
 EventStateManager::HandleMiddleClickPaste(nsIPresShell* aPresShell,
                                           WidgetMouseEvent* aMouseEvent,
                                           nsEventStatus* aStatus,
                                           TextEditor* aTextEditor)
 {
   MOZ_ASSERT(aPresShell);
   MOZ_ASSERT(aMouseEvent);
-  MOZ_ASSERT(aMouseEvent->mMessage == eMouseClick &&
-             aMouseEvent->button == WidgetMouseEventBase::eMiddleButton);
+  MOZ_ASSERT((aMouseEvent->mMessage == eMouseClick &&
+              aMouseEvent->button == WidgetMouseEventBase::eMiddleButton) ||
+             EventCausesClickEvents(*aMouseEvent));
   MOZ_ASSERT(aStatus);
   MOZ_ASSERT(*aStatus != nsEventStatus_eConsumeNoDefault);
-  MOZ_ASSERT(aTextEditor);
-
-  RefPtr<Selection> selection = aTextEditor->GetSelection();
-  if (NS_WARN_IF(!selection)) {
-    return NS_ERROR_FAILURE;
+
+  // Even if we're called twice or more for a mouse operation, we should
+  // handle only once.  Although mMultipleActionsPrevented may be set to
+  // true by different event handler in the future, we can use it for now.
+  if (aMouseEvent->mFlags.mMultipleActionsPrevented) {
+    return NS_OK;
+  }
+  aMouseEvent->mFlags.mMultipleActionsPrevented = true;
+
+  RefPtr<Selection> selection;
+  if (aTextEditor) {
+    selection = aTextEditor->GetSelection();
+    if (NS_WARN_IF(!selection)) {
+      return NS_ERROR_FAILURE;
+    }
+  } else {
+    nsIDocument* document = aPresShell->GetDocument();
+    if (NS_WARN_IF(!document)) {
+      return NS_ERROR_FAILURE;
+    }
+    nsCopySupport::GetSelectionForCopy(document, getter_AddRefs(selection));
+    if (NS_WARN_IF(!selection)) {
+      return NS_ERROR_FAILURE;
+    }
   }
 
   // Move selection to the clicked point.
   nsCOMPtr<nsIContent> container;
   int32_t offset;
   nsLayoutUtils::GetContainerAndOffsetAtEvent(aPresShell, aMouseEvent,
                                               getter_AddRefs(container),
                                               &offset);
@@ -5173,16 +5236,22 @@ EventStateManager::HandleMiddleClickPast
   // even if the middle click event was consumed for compatibility with
   // Chromium.
   if (!nsCopySupport::FireClipboardEvent(ePaste, clipboardType,
                                          aPresShell, selection)) {
     *aStatus = nsEventStatus_eConsumeNoDefault;
     return NS_OK;
   }
 
+  // Although we've fired "paste" event, there is no editor to accept the
+  // clipboard content.
+  if (!aTextEditor) {
+    return NS_OK;
+  }
+
   // Check if the editor is still the good target to paste.
   if (aTextEditor->Destroyed() ||
       aTextEditor->IsReadonly() ||
       aTextEditor->IsDisabled()) {
     // XXX Should we consume the event when the editor is readonly and/or
     //     disabled?
     return NS_OK;
   }
--- a/dom/events/EventStateManager.h
+++ b/dom/events/EventStateManager.h
@@ -360,25 +360,29 @@ public:
    * the page width or height.
    */
   enum {
     MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL = 1000
   };
 
   /**
    * HandleMiddleClickPaste() handles middle mouse button event as pasting
-   * clipboard text.
+   * clipboard text.  Note that if aTextEditor is nullptr, this only
+   * dispatches ePaste event because it's necessary for some web apps which
+   * want to implement their own editor and supports middle click paste.
    *
    * @param aPresShell              The PresShell for the ESM.  This lifetime
    *                                should be guaranteed by the caller.
    * @param aMouseEvent             The eMouseClick event which caused the
    *                                paste.
    * @param aStatus                 The event status of aMouseEvent.
    * @param aTextEditor             TextEditor which may be pasted the
    *                                clipboard text by the middle click.
+   *                                If there is no editor for aMouseEvent,
+   *                                set nullptr.
    */
   MOZ_CAN_RUN_SCRIPT
   nsresult HandleMiddleClickPaste(nsIPresShell* aPresShell,
                                   WidgetMouseEvent* aMouseEvent,
                                   nsEventStatus* aStatus,
                                   TextEditor* aTextEditor);
 
 protected:
--- a/dom/tests/mochitest/general/test_clipboard_events.html
+++ b/dom/tests/mochitest/general/test_clipboard_events.html
@@ -62,16 +62,28 @@ async function reset() {
   // Reset value of contentInput.
   contentInput.value = "INPUT TEXT";
 }
 
 function getClipboardText() {
   return SpecialPowers.getClipboardData("text/unicode");
 }
 
+function getHTMLEditor() {
+  let editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+  if (!editingSession) {
+    return null;
+  }
+  let editor = editingSession.getEditorForWindow(window);
+  if (!editor) {
+    return null;
+  }
+  return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
+}
+
 async function putOnClipboard(expected, operationFn, desc, type) {
   await SimpleTest.promiseClipboardChange(expected, operationFn, type);
   ok(true, desc);
 }
 
 async function wontPutOnClipboard(expected, operationFn, desc, type) {
   await SimpleTest.promiseClipboardChange(null, operationFn, type, 300, true);
   ok(SpecialPowers.getClipboardData(type || "text/unicode")
@@ -810,12 +822,82 @@ add_task(async function test_event_targe
   is(pasteTarget.getAttribute("id"), "p1",
      "'paste' event's target should be always an element which includes start container of the first Selection range");
   is(pasteEventCount, 1,
      "'paste' event should be fired only once when Accel+'v' is pressed");
   document.removeEventListener("paste", pasteEventLogger);
   contenteditableContainer.innerHTML = "";
 });
 
+add_task(async function test_paste_event_for_middle_click_without_HTMLEditor() {
+  await SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true],
+                                           ["middlemouse.contentLoadURL", false]]});
+
+  await reset();
+
+  contenteditableContainer.innerHTML = '<div id="non-editable-target">non-editable</div>';
+  let noneditableDiv = document.getElementById("non-editable-target");
+
+  ok(!getHTMLEditor(), "There should not be HTMLEditor");
+
+  let selection = document.getSelection();
+  selection.setBaseAndExtent(content.firstChild, 0,
+                             content.firstChild, "CONTENT".length);
+
+  await putOnClipboard("CONTENT", () => {
+    synthesizeKey("c", {accelKey: 1});
+  }, "copy text from non-editable element");
+
+  let auxclickFired = false;
+  function onAuxClick(event) {
+    auxclickFired = true;
+  }
+  document.addEventListener("auxclick", onAuxClick);
+
+  let pasteEventCount = 0;
+  function onPaste(event) {
+    pasteEventCount++;
+    ok(auxclickFired, "'auxclick' event should be fired before 'paste' event");
+    is(event.target, noneditableDiv,
+       "'paste' event should be fired on the clicked element");
+  }
+  document.addEventListener("paste", onPaste);
+
+  synthesizeMouseAtCenter(noneditableDiv, {button: 1});
+  is(pasteEventCount, 1, "'paste' event should be fired just once");
+
+  pasteEventCount = 0;
+  auxclickFired = false;
+  document.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
+  synthesizeMouseAtCenter(noneditableDiv, {button: 1});
+  is(pasteEventCount, 1,
+     "Even if 'mouseup' event is consumed, 'paste' event should be fired");
+
+  pasteEventCount = 0;
+  auxclickFired = false;
+  document.addEventListener("click", (event) => { event.preventDefault(); }, {once: true, capture: true});
+  synthesizeMouseAtCenter(noneditableDiv, {button: 1});
+  is(pasteEventCount, 0,
+     "If 'click' event is consumed at capturing phase at the document node, 'paste' event should be not be fired");
+
+  pasteEventCount = 0;
+  auxclickFired = false;
+  noneditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
+  synthesizeMouseAtCenter(noneditableDiv, {button: 1});
+  is(pasteEventCount, 1,
+     "Even if 'click' event listener is added to the click event target, 'paste' event should be fired");
+
+  pasteEventCount = 0;
+  auxclickFired = false;
+  document.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
+  synthesizeMouseAtCenter(noneditableDiv, {button: 1});
+  is(pasteEventCount, 0,
+     "If 'auxclick' event is consumed, 'paste' event should be not be fired");
+
+  document.removeEventListener("auxclick", onAuxClick);
+  document.removeEventListener("paste", onPaste);
+  contenteditableContainer.innerHTML = "";
+});
+
 </script>
 </pre>
 </body>
 </html>
--- a/editor/libeditor/EditorEventListener.cpp
+++ b/editor/libeditor/EditorEventListener.cpp
@@ -673,16 +673,26 @@ EditorEventListener::MouseClick(WidgetMo
   }
 
   // If we got a mouse down inside the editing area, we should force the
   // IME to commit before we change the cursor position.
   if (!EnsureCommitCompoisition()) {
     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->button != WidgetMouseEventBase::eMiddleButton ||
       !WidgetMouseEvent::IsMiddleClickPasteEnabled()) {
     return NS_OK;
   }
 
   nsCOMPtr<nsIPresShell> presShell = GetPresShell();
   if (NS_WARN_IF(!presShell)) {
     return NS_OK;