Implement link selection on alt+mouse drag.
BUG=244738
TEST=webkit_unit_tests --gtest_filter=LinkSelectionTest.*
Review URL: https://codereview.chromium.org/1774123006
Cr-Commit-Position: refs/heads/master@{#383684}
diff --git a/third_party/WebKit/Source/core/editing/SelectionController.cpp b/third_party/WebKit/Source/core/editing/SelectionController.cpp
index e26e173..6db0a91 100644
--- a/third_party/WebKit/Source/core/editing/SelectionController.cpp
+++ b/third_party/WebKit/Source/core/editing/SelectionController.cpp
@@ -432,7 +432,8 @@
{
// If we got the event back, that must mean it wasn't prevented,
// so it's allowed to start a drag or selection if it wasn't in a scrollbar.
- m_mouseDownMayStartSelect = canMouseDownStartSelect(event.innerNode()) && !event.scrollbar();
+ m_mouseDownMayStartSelect = (canMouseDownStartSelect(event.innerNode()) || isLinkSelection(event))
+ && !event.scrollbar();
m_mouseDownWasSingleClickInSelection = false;
// Avoid double-tap touch gesture confusion by restricting multi-click side
// effects, e.g., word selection, to editable regions.
@@ -562,7 +563,8 @@
|| !(selection().isContentEditable() || (mev.innerNode() && mev.innerNode()->isTextNode())))
return;
- m_mouseDownMayStartSelect = true; // context menu events are always allowed to perform a selection
+ // Context menu events are always allowed to perform a selection.
+ TemporaryChange<bool> mouseDownMayStartSelectChange(m_mouseDownMayStartSelect, true);
if (mev.hitTestResult().isMisspelled())
return selectClosestMisspellingFromMouseEvent(mev);
@@ -623,4 +625,9 @@
return m_frame->selection();
}
+bool isLinkSelection(const MouseEventWithHitTestResults& event)
+{
+ return event.event().altKey() && event.isOverLink();
+}
+
} // namespace blink
diff --git a/third_party/WebKit/Source/core/editing/SelectionController.h b/third_party/WebKit/Source/core/editing/SelectionController.h
index eb259ec..ea9104f 100644
--- a/third_party/WebKit/Source/core/editing/SelectionController.h
+++ b/third_party/WebKit/Source/core/editing/SelectionController.h
@@ -65,6 +65,7 @@
bool mouseDownMayStartSelect() const;
bool mouseDownWasSingleClickInSelection() const;
void notifySelectionChanged();
+ bool hasExtendedSelection() const { return m_selectionState == SelectionState::ExtendedSelection; }
private:
explicit SelectionController(LocalFrame&);
@@ -88,6 +89,8 @@
SelectionState m_selectionState;
};
+bool isLinkSelection(const MouseEventWithHitTestResults&);
+
} // namespace blink
#endif // SelectionController_h
diff --git a/third_party/WebKit/Source/core/input/EventHandler.cpp b/third_party/WebKit/Source/core/input/EventHandler.cpp
index 0cd5917..38065d3 100644
--- a/third_party/WebKit/Source/core/input/EventHandler.cpp
+++ b/third_party/WebKit/Source/core/input/EventHandler.cpp
@@ -412,7 +412,7 @@
bool singleClick = event.event().clickCount() <= 1;
- m_mouseDownMayStartDrag = singleClick;
+ m_mouseDownMayStartDrag = singleClick && !isLinkSelection(event);
selectionController().handleMousePressEvent(event);
@@ -941,7 +941,8 @@
{
bool editable = (node && node->hasEditableStyle());
- if (useHandCursor(node, result.isOverLink()))
+ const bool isOverLink = !selectionController().mouseDownMayStartSelect() && result.isOverLink();
+ if (useHandCursor(node, isOverLink))
return handCursor();
bool inResizer = false;
@@ -1365,7 +1366,12 @@
#endif
WebInputEventResult clickEventResult = WebInputEventResult::NotHandled;
- if (m_clickCount > 0 && !contextMenuEvent && mev.innerNode() && m_clickNode && mev.innerNode()->canParticipateInFlatTree() && m_clickNode->canParticipateInFlatTree()) {
+ const bool shouldDispatchClickEvent = m_clickCount > 0
+ && !contextMenuEvent
+ && mev.innerNode() && m_clickNode
+ && mev.innerNode()->canParticipateInFlatTree() && m_clickNode->canParticipateInFlatTree()
+ && !(selectionController().hasExtendedSelection() && isLinkSelection(mev));
+ if (shouldDispatchClickEvent) {
// Updates distribution because a 'mouseup' event listener can make the
// tree dirty at dispatchMouseEvent() invocation above.
// Unless distribution is updated, commonAncestor would hit ASSERT.
diff --git a/third_party/WebKit/Source/platform/testing/UnitTestHelpers.cpp b/third_party/WebKit/Source/platform/testing/UnitTestHelpers.cpp
index 814a7f7..fd991ac 100644
--- a/third_party/WebKit/Source/platform/testing/UnitTestHelpers.cpp
+++ b/third_party/WebKit/Source/platform/testing/UnitTestHelpers.cpp
@@ -30,6 +30,7 @@
#include "base/message_loop/message_loop.h"
#include "base/path_service.h"
#include "platform/SharedBuffer.h"
+#include "platform/Timer.h"
#include "public/platform/FilePathConversion.h"
#include "public/platform/Platform.h"
#include "public/platform/WebString.h"
@@ -48,6 +49,12 @@
enterRunLoop();
}
+void runDelayedTasks(double delayMs)
+{
+ Platform::current()->currentThread()->getWebTaskRunner()->postDelayedTask(BLINK_FROM_HERE, bind(&exitRunLoop), delayMs);
+ enterRunLoop();
+}
+
String blinkRootDir()
{
base::FilePath path;
diff --git a/third_party/WebKit/Source/platform/testing/UnitTestHelpers.h b/third_party/WebKit/Source/platform/testing/UnitTestHelpers.h
index f73a6ce0..2a036c9 100644
--- a/third_party/WebKit/Source/platform/testing/UnitTestHelpers.h
+++ b/third_party/WebKit/Source/platform/testing/UnitTestHelpers.h
@@ -37,6 +37,9 @@
void runPendingTasks();
+// Wait for delayed task to complete or timers to fire for |delayMs| milliseconds.
+void runDelayedTasks(double delayMs);
+
String blinkRootDir();
PassRefPtr<SharedBuffer> readFromFile(const String& path);
diff --git a/third_party/WebKit/Source/web/tests/FrameTestHelpers.cpp b/third_party/WebKit/Source/web/tests/FrameTestHelpers.cpp
index 859bfe1..dca486c 100644
--- a/third_party/WebKit/Source/web/tests/FrameTestHelpers.cpp
+++ b/third_party/WebKit/Source/web/tests/FrameTestHelpers.cpp
@@ -150,6 +150,18 @@
testing::enterRunLoop();
}
+WebMouseEvent createMouseEvent(WebInputEvent::Type type, WebMouseEvent::Button button, const IntPoint& point, int modifiers)
+{
+ WebMouseEvent result;
+ result.type = type;
+ result.x = result.windowX = result.globalX = point.x();
+ result.y = result.windowX = result.globalX = point.y();
+ result.modifiers = modifiers;
+ result.button = button;
+ result.clickCount = 1;
+ return result;
+}
+
WebLocalFrame* createLocalChild(WebRemoteFrame* parent, const WebString& name, WebFrameClient* client, WebFrame* previousSibling, const WebFrameOwnerProperties& properties)
{
if (!client)
diff --git a/third_party/WebKit/Source/web/tests/FrameTestHelpers.h b/third_party/WebKit/Source/web/tests/FrameTestHelpers.h
index a66c52c..57f105b 100644
--- a/third_party/WebKit/Source/web/tests/FrameTestHelpers.h
+++ b/third_party/WebKit/Source/web/tests/FrameTestHelpers.h
@@ -75,6 +75,8 @@
// using one of the above helper methods whenever possible.
void pumpPendingRequestsForFrameToLoad(WebFrame*);
+WebMouseEvent createMouseEvent(WebInputEvent::Type, WebMouseEvent::Button, const IntPoint&, int modifiers);
+
// Calls WebRemoteFrame::createLocalChild, but with some arguments prefilled
// with default test values (i.e. with a default |client| or |properties| and/or
// with a precalculated |uniqueName|).
diff --git a/third_party/WebKit/Source/web/tests/LinkSelectionTest.cpp b/third_party/WebKit/Source/web/tests/LinkSelectionTest.cpp
new file mode 100644
index 0000000..28a8b4b
--- /dev/null
+++ b/third_party/WebKit/Source/web/tests/LinkSelectionTest.cpp
@@ -0,0 +1,314 @@
+// Copyright (c) 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "core/dom/Range.h"
+#include "core/frame/FrameView.h"
+#include "core/input/EventHandler.h"
+#include "core/page/ChromeClient.h"
+#include "core/page/ContextMenuController.h"
+#include "core/page/FocusController.h"
+#include "core/page/Page.h"
+#include "platform/Cursor.h"
+#include "platform/testing/URLTestHelpers.h"
+#include "platform/testing/UnitTestHelpers.h"
+#include "public/web/WebSettings.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "web/WebLocalFrameImpl.h"
+#include "web/tests/FrameTestHelpers.h"
+
+using ::testing::_;
+
+namespace blink {
+
+IntSize scaled(IntSize p, float scale)
+{
+ p.scale(scale, scale);
+ return p;
+}
+
+class LinkSelectionTestBase : public ::testing::Test {
+protected:
+ enum DragFlag {
+ SendDownEvent = 1,
+ SendUpEvent = 1 << 1
+ };
+ using DragFlags = unsigned;
+
+ void emulateMouseDrag(const IntPoint& downPoint, const IntPoint& upPoint, int modifiers,
+ DragFlags = SendDownEvent | SendUpEvent);
+
+ void emulateMouseClick(const IntPoint& clickPoint, WebMouseEvent::Button, int modifiers, int count = 1);
+ void emulateMouseDown(const IntPoint& clickPoint, WebMouseEvent::Button, int modifiers, int count = 1);
+
+ String getSelectionText();
+
+ FrameTestHelpers::WebViewHelper m_helper;
+ WebViewImpl* m_webView = nullptr;
+ RawPtrWillBePersistent<WebLocalFrameImpl> m_mainFrame = nullptr;
+};
+
+void LinkSelectionTestBase::emulateMouseDrag(const IntPoint& downPoint, const IntPoint& upPoint, int modifiers, DragFlags dragFlags)
+{
+ if (dragFlags & SendDownEvent) {
+ const auto& downEvent = FrameTestHelpers::createMouseEvent(WebMouseEvent::MouseDown, WebMouseEvent::ButtonLeft, downPoint, modifiers);
+ m_webView->handleInputEvent(downEvent);
+ }
+
+ const int kMoveEventsNumber = 10;
+ const float kMoveIncrementFraction = 1. / kMoveEventsNumber;
+ const auto& upDownVector = upPoint - downPoint;
+ for (int i = 0; i < kMoveEventsNumber; ++i) {
+ const auto& movePoint = downPoint + scaled(upDownVector, i * kMoveIncrementFraction);
+ const auto& moveEvent = FrameTestHelpers::createMouseEvent(WebMouseEvent::MouseMove, WebMouseEvent::ButtonLeft, movePoint, modifiers);
+ m_webView->handleInputEvent(moveEvent);
+ }
+
+ if (dragFlags & SendUpEvent) {
+ const auto& upEvent = FrameTestHelpers::createMouseEvent(WebMouseEvent::MouseUp, WebMouseEvent::ButtonLeft, upPoint, modifiers);
+ m_webView->handleInputEvent(upEvent);
+ }
+}
+
+void LinkSelectionTestBase::emulateMouseClick(const IntPoint& clickPoint, WebMouseEvent::Button button, int modifiers, int count)
+{
+ auto event = FrameTestHelpers::createMouseEvent(WebMouseEvent::MouseDown, button, clickPoint, modifiers);
+ event.clickCount = count;
+ m_webView->handleInputEvent(event);
+ event.type = WebMouseEvent::MouseUp;
+ m_webView->handleInputEvent(event);
+}
+
+void LinkSelectionTestBase::emulateMouseDown(const IntPoint& clickPoint, WebMouseEvent::Button button, int modifiers, int count)
+{
+ auto event = FrameTestHelpers::createMouseEvent(WebMouseEvent::MouseDown, button, clickPoint, modifiers);
+ event.clickCount = count;
+ m_webView->handleInputEvent(event);
+}
+
+String LinkSelectionTestBase::getSelectionText()
+{
+ return m_mainFrame->selectionAsText();
+}
+
+class TestFrameClient : public FrameTestHelpers::TestWebFrameClient {
+public:
+ MOCK_METHOD4(loadURLExternally,
+ void(const WebURLRequest&, WebNavigationPolicy, const WebString& downloadName, bool shouldReplaceCurrentEntry));
+};
+
+class LinkSelectionTest : public LinkSelectionTestBase {
+protected:
+ void SetUp() override
+ {
+ const char* const kHTMLString =
+ "<a id='link' href='foo.com' style='font-size:20pt'>Text to select foobar</a>"
+ "<div id='page_text'>Lorem ipsum dolor sit amet</div>";
+
+ // We need to set deviceSupportsMouse setting to true and page's focus controller to active
+ // so that FrameView can set the mouse cursor.
+ m_webView = m_helper.initialize(false, &m_testFrameClient, nullptr,
+ [](WebSettings* settings) { settings->setDeviceSupportsMouse(true); });
+ m_mainFrame = m_webView->mainFrameImpl();
+ FrameTestHelpers::loadHTMLString(m_mainFrame, kHTMLString, URLTestHelpers::toKURL("http://foobar.com"));
+ m_webView->resize(WebSize(800, 600));
+ m_webView->page()->focusController().setActive(true);
+
+ auto* document = m_mainFrame->frame()->document();
+ ASSERT_NE(nullptr, document);
+ auto* linkToSelect = document->getElementById("link")->firstChild();
+ ASSERT_NE(nullptr, linkToSelect);
+ // We get larger range that we actually want to select, because we need a slightly larger
+ // rect to include the last character to the selection.
+ const auto rangeToSelect = Range::create(*document, linkToSelect, 5, linkToSelect, 16);
+
+ const auto& selectionRect = rangeToSelect->boundingBox();
+ const auto& selectionRectCenterY = selectionRect.center().y();
+ m_leftPointInLink = selectionRect.minXMinYCorner();
+ m_leftPointInLink.setY(selectionRectCenterY);
+
+ m_rightPointInLink = selectionRect.maxXMinYCorner();
+ m_rightPointInLink.setY(selectionRectCenterY);
+ m_rightPointInLink.move(-2, 0);
+ }
+
+ TestFrameClient m_testFrameClient;
+ IntPoint m_leftPointInLink;
+ IntPoint m_rightPointInLink;
+};
+
+TEST_F(LinkSelectionTest, MouseDragWithoutAltAllowNoLinkSelection)
+{
+ emulateMouseDrag(m_leftPointInLink, m_rightPointInLink, 0);
+ EXPECT_TRUE(getSelectionText().isEmpty());
+}
+
+TEST_F(LinkSelectionTest, MouseDragWithAltAllowSelection)
+{
+ emulateMouseDrag(m_leftPointInLink, m_rightPointInLink, WebInputEvent::AltKey);
+ EXPECT_EQ("to select", getSelectionText());
+}
+
+TEST_F(LinkSelectionTest, HandCursorDuringLinkDrag)
+{
+ emulateMouseDrag(m_rightPointInLink, m_leftPointInLink, 0, SendDownEvent);
+ m_mainFrame->frame()->localFrameRoot()->eventHandler().scheduleCursorUpdate();
+ testing::runDelayedTasks(50);
+ const auto& cursor = m_mainFrame->frame()->chromeClient().lastSetCursorForTesting();
+ EXPECT_EQ(Cursor::Hand, cursor.getType());
+}
+
+TEST_F(LinkSelectionTest, CaretCursorOverLinkDuringSelection)
+{
+ emulateMouseDrag(m_rightPointInLink, m_leftPointInLink, WebInputEvent::AltKey, SendDownEvent);
+ m_mainFrame->frame()->localFrameRoot()->eventHandler().scheduleCursorUpdate();
+ testing::runDelayedTasks(50);
+ const auto& cursor = m_mainFrame->frame()->chromeClient().lastSetCursorForTesting();
+ EXPECT_EQ(Cursor::IBeam, cursor.getType());
+}
+
+TEST_F(LinkSelectionTest, HandCursorOverLinkAfterContextMenu)
+{
+ // Move mouse.
+ emulateMouseDrag(m_rightPointInLink, m_leftPointInLink, 0, 0);
+
+ // Show context menu. We don't send mouseup event here since in browser it doesn't reach
+ // blink because of shown context menu.
+ emulateMouseDown(m_leftPointInLink, WebMouseEvent::ButtonRight, 0, 1);
+
+ LocalFrame* frame = m_mainFrame->frame();
+ // Hide context menu.
+ frame->page()->contextMenuController().clearContextMenu();
+
+ frame->localFrameRoot()->eventHandler().scheduleCursorUpdate();
+ testing::runDelayedTasks(50);
+ const auto& cursor = m_mainFrame->frame()->chromeClient().lastSetCursorForTesting();
+ EXPECT_EQ(Cursor::Hand, cursor.getType());
+}
+
+TEST_F(LinkSelectionTest, SingleClickWithAltStartsDownload)
+{
+ EXPECT_CALL(m_testFrameClient, loadURLExternally(_, WebNavigationPolicy::WebNavigationPolicyDownload, WebString(), _));
+ emulateMouseClick(m_leftPointInLink, WebMouseEvent::ButtonLeft, WebInputEvent::AltKey);
+}
+
+TEST_F(LinkSelectionTest, SingleClickWithAltStartsDownloadWhenTextSelected)
+{
+ auto* document = m_mainFrame->frame()->document();
+ auto* textToSelect = document->getElementById("page_text")->firstChild();
+ ASSERT_NE(nullptr, textToSelect);
+
+ // Select some page text outside the link element.
+ const RefPtrWillBeRawPtr<Range> rangeToSelect = Range::create(*document, textToSelect, 1, textToSelect, 20);
+ const auto& selectionRect = rangeToSelect->boundingBox();
+ m_mainFrame->moveRangeSelection(selectionRect.minXMinYCorner(), selectionRect.maxXMaxYCorner());
+ EXPECT_FALSE(getSelectionText().isEmpty());
+
+ EXPECT_CALL(m_testFrameClient, loadURLExternally(_, WebNavigationPolicy::WebNavigationPolicyDownload, WebString(), _));
+ emulateMouseClick(m_leftPointInLink, WebMouseEvent::ButtonLeft, WebInputEvent::AltKey);
+}
+
+class LinkSelectionClickEventsTest : public LinkSelectionTestBase {
+protected:
+ class MockEventListener final : public EventListener {
+ public:
+ static PassRefPtrWillBeRawPtr<MockEventListener> create()
+ {
+ return adoptRefWillBeNoop(new MockEventListener());
+ }
+
+ bool operator==(const EventListener& other) const final
+ {
+ return this == &other;
+ }
+
+ MOCK_METHOD2(handleEvent, void(ExecutionContext* executionContext, Event*));
+
+ private:
+ MockEventListener() : EventListener(CPPEventListenerType)
+ {
+ }
+ };
+
+ void SetUp() override
+ {
+ const char* const kHTMLString =
+ "<div id='empty_div' style='width: 100px; height: 100px;'></div>"
+ "<span id='text_div'>Sometexttoshow</span>";
+
+ m_webView = m_helper.initialize(false);
+ m_mainFrame = m_webView->mainFrameImpl();
+ FrameTestHelpers::loadHTMLString(m_mainFrame, kHTMLString, URLTestHelpers::toKURL("http://foobar.com"));
+ m_webView->resize(WebSize(800, 600));
+ m_webView->page()->focusController().setActive(true);
+
+ auto* document = m_mainFrame->frame()->document();
+ ASSERT_NE(nullptr, document);
+
+ auto* emptyDiv = document->getElementById("empty_div");
+ auto* textDiv = document->getElementById("text_div");
+ ASSERT_NE(nullptr, emptyDiv);
+ ASSERT_NE(nullptr, textDiv);
+ }
+
+ void checkMouseClicks(Element& element, bool doubleClickEvent)
+ {
+ struct ScopedListenersCleaner {
+ ScopedListenersCleaner(Element* element) : m_element(element) {}
+
+ ~ScopedListenersCleaner()
+ {
+ m_element->removeAllEventListeners();
+ }
+
+ RawPtrWillBePersistent<Element> m_element;
+ } const listenersCleaner(&element);
+
+ RefPtrWillBeRawPtr<MockEventListener> eventHandler = MockEventListener::create();
+ element.addEventListener(
+ doubleClickEvent ? EventTypeNames::dblclick : EventTypeNames::click,
+ eventHandler);
+
+ ::testing::InSequence s;
+ EXPECT_CALL(*eventHandler, handleEvent(_, _)).Times(1);
+
+ const auto& elemBounds = element.boundsInViewport();
+ const int clickCount = doubleClickEvent ? 2 : 1;
+ emulateMouseClick(elemBounds.center(), WebMouseEvent::ButtonLeft, 0, clickCount);
+
+ if (doubleClickEvent) {
+ EXPECT_EQ(element.innerText().isEmpty(), getSelectionText().isEmpty());
+ }
+ }
+};
+
+TEST_F(LinkSelectionClickEventsTest, SingleAndDoubleClickWillBeHandled)
+{
+ auto* document = m_mainFrame->frame()->document();
+ auto* element = document->getElementById("empty_div");
+
+ {
+ SCOPED_TRACE("Empty div, single click");
+ checkMouseClicks(*element, false);
+ }
+
+ {
+ SCOPED_TRACE("Empty div, double click");
+ checkMouseClicks(*element, true);
+ }
+
+ element = document->getElementById("text_div");
+
+ {
+ SCOPED_TRACE("Text div, single click");
+ checkMouseClicks(*element, false);
+ }
+
+ {
+ SCOPED_TRACE("Text div, double click");
+ checkMouseClicks(*element, true);
+ }
+}
+
+} // namespace blink
diff --git a/third_party/WebKit/Source/web/web.gypi b/third_party/WebKit/Source/web/web.gypi
index 8aa68bc..86f8049 100644
--- a/third_party/WebKit/Source/web/web.gypi
+++ b/third_party/WebKit/Source/web/web.gypi
@@ -263,6 +263,7 @@
'tests/ImeOnFocusTest.cpp',
'tests/KeyboardTest.cpp',
'tests/ListenerLeakTest.cpp',
+ 'tests/LinkSelectionTest.cpp',
'tests/MHTMLTest.cpp',
'tests/PrerenderingTest.cpp',
'tests/ProgrammaticScrollTest.cpp',