blob: a9fc821e4441b9c384e6220d74f8643dedc36548 [file] [log] [blame]
// Copyright 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/input/GestureManager.h"
#include "core/dom/Document.h"
#include "core/editing/SelectionController.h"
#include "core/events/GestureEvent.h"
#include "core/frame/FrameHost.h"
#include "core/frame/FrameView.h"
#include "core/frame/Settings.h"
#include "core/frame/VisualViewport.h"
#include "core/input/EventHandler.h"
#include "core/input/EventHandlingUtil.h"
#include "core/page/ChromeClient.h"
#include "core/page/Page.h"
namespace blink {
GestureManager::GestureManager(LocalFrame* frame,
ScrollManager* scrollManager,
MouseEventManager* mouseEventManager,
PointerEventManager* pointerEventManager,
SelectionController* selectionController)
: m_frame(frame),
m_scrollManager(scrollManager),
m_mouseEventManager(mouseEventManager),
m_pointerEventManager(pointerEventManager),
m_selectionController(selectionController) {
clear();
}
void GestureManager::clear() {
m_suppressMouseEventsFromGestures = false;
m_longTapShouldInvokeContextMenu = false;
m_lastShowPressTimestamp = 0;
}
DEFINE_TRACE(GestureManager) {
visitor->trace(m_frame);
visitor->trace(m_scrollManager);
visitor->trace(m_mouseEventManager);
visitor->trace(m_pointerEventManager);
visitor->trace(m_selectionController);
}
HitTestRequest::HitTestRequestType GestureManager::getHitTypeForGestureType(
PlatformEvent::EventType type) {
HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent;
switch (type) {
case PlatformEvent::GestureShowPress:
case PlatformEvent::GestureTapUnconfirmed:
return hitType | HitTestRequest::Active;
case PlatformEvent::GestureTapDownCancel:
// A TapDownCancel received when no element is active shouldn't really be
// changing hover state.
if (!m_frame->document()->activeHoverElement())
hitType |= HitTestRequest::ReadOnly;
return hitType | HitTestRequest::Release;
case PlatformEvent::GestureTap:
return hitType | HitTestRequest::Release;
case PlatformEvent::GestureTapDown:
case PlatformEvent::GestureLongPress:
case PlatformEvent::GestureLongTap:
case PlatformEvent::GestureTwoFingerTap:
// FIXME: Shouldn't LongTap and TwoFingerTap clear the Active state?
return hitType | HitTestRequest::Active | HitTestRequest::ReadOnly;
default:
NOTREACHED();
return hitType | HitTestRequest::Active | HitTestRequest::ReadOnly;
}
}
WebInputEventResult GestureManager::handleGestureEventInFrame(
const GestureEventWithHitTestResults& targetedEvent) {
DCHECK(!targetedEvent.event().isScrollEvent());
Node* eventTarget = targetedEvent.hitTestResult().innerNode();
const PlatformGestureEvent& gestureEvent = targetedEvent.event();
if (m_scrollManager->canHandleGestureEvent(targetedEvent))
return WebInputEventResult::HandledSuppressed;
if (eventTarget) {
GestureEvent* gestureDomEvent =
GestureEvent::create(eventTarget->document().domWindow(), gestureEvent);
if (gestureDomEvent) {
DispatchEventResult gestureDomEventResult =
eventTarget->dispatchEvent(gestureDomEvent);
if (gestureDomEventResult != DispatchEventResult::NotCanceled) {
DCHECK(gestureDomEventResult !=
DispatchEventResult::CanceledByEventHandler);
return EventHandlingUtil::toWebInputEventResult(gestureDomEventResult);
}
}
}
switch (gestureEvent.type()) {
case PlatformEvent::GestureTapDown:
return handleGestureTapDown(targetedEvent);
case PlatformEvent::GestureTap:
return handleGestureTap(targetedEvent);
case PlatformEvent::GestureShowPress:
return handleGestureShowPress();
case PlatformEvent::GestureLongPress:
return handleGestureLongPress(targetedEvent);
case PlatformEvent::GestureLongTap:
return handleGestureLongTap(targetedEvent);
case PlatformEvent::GestureTwoFingerTap:
return handleGestureTwoFingerTap(targetedEvent);
case PlatformEvent::GesturePinchBegin:
case PlatformEvent::GesturePinchEnd:
case PlatformEvent::GesturePinchUpdate:
case PlatformEvent::GestureTapDownCancel:
case PlatformEvent::GestureTapUnconfirmed:
break;
default:
NOTREACHED();
}
return WebInputEventResult::NotHandled;
}
WebInputEventResult GestureManager::handleGestureTapDown(
const GestureEventWithHitTestResults& targetedEvent) {
m_suppressMouseEventsFromGestures =
m_pointerEventManager->primaryPointerdownCanceled(
targetedEvent.event().uniqueTouchEventId());
return WebInputEventResult::NotHandled;
}
WebInputEventResult GestureManager::handleGestureTap(
const GestureEventWithHitTestResults& targetedEvent) {
FrameView* frameView(m_frame->view());
const PlatformGestureEvent& gestureEvent = targetedEvent.event();
HitTestRequest::HitTestRequestType hitType =
getHitTypeForGestureType(gestureEvent.type());
uint64_t preDispatchDomTreeVersion = m_frame->document()->domTreeVersion();
uint64_t preDispatchStyleVersion = m_frame->document()->styleVersion();
UserGestureIndicator gestureIndicator(DefinitelyProcessingUserGesture);
HitTestResult currentHitTest = targetedEvent.hitTestResult();
// We use the adjusted position so the application isn't surprised to see a
// event with co-ordinates outside the target's bounds.
IntPoint adjustedPoint =
frameView->rootFrameToContents(gestureEvent.position());
const unsigned modifiers = gestureEvent.getModifiers();
if (!m_suppressMouseEventsFromGestures) {
PlatformMouseEvent fakeMouseMove(
gestureEvent.position(), gestureEvent.globalPosition(),
WebPointerProperties::Button::NoButton, PlatformEvent::MouseMoved,
/* clickCount */ 0, static_cast<PlatformEvent::Modifiers>(modifiers),
PlatformMouseEvent::FromTouch, gestureEvent.timestamp(),
WebPointerProperties::PointerType::Mouse);
m_mouseEventManager->setMousePositionAndDispatchMouseEvent(
currentHitTest.innerNode(), EventTypeNames::mousemove, fakeMouseMove);
}
// Do a new hit-test in case the mousemove event changed the DOM.
// Note that if the original hit test wasn't over an element (eg. was over a
// scrollbar) we don't want to re-hit-test because it may be in the wrong
// frame (and there's no way the page could have seen the event anyway). Also
// note that the position of the frame may have changed, so we need to
// recompute the content co-ordinates (updating layout/style as
// hitTestResultAtPoint normally would).
// FIXME: Use a hit-test cache to avoid unnecessary hit tests.
// http://crbug.com/398920
if (currentHitTest.innerNode()) {
LocalFrame* mainFrame = m_frame->localFrameRoot();
if (mainFrame && mainFrame->view())
mainFrame->view()->updateLifecycleToCompositingCleanPlusScrolling();
adjustedPoint = frameView->rootFrameToContents(gestureEvent.position());
currentHitTest = EventHandlingUtil::hitTestResultInFrame(
m_frame, adjustedPoint, hitType);
}
// Capture data for showUnhandledTapUIIfNeeded.
Node* tappedNode = currentHitTest.innerNode();
IntPoint tappedPosition = gestureEvent.position();
Node* tappedNonTextNode = tappedNode;
if (tappedNonTextNode && tappedNonTextNode->isTextNode())
tappedNonTextNode = FlatTreeTraversal::parent(*tappedNonTextNode);
m_mouseEventManager->setClickNode(tappedNonTextNode);
PlatformMouseEvent fakeMouseDown(
gestureEvent.position(), gestureEvent.globalPosition(),
WebPointerProperties::Button::Left, PlatformEvent::MousePressed,
gestureEvent.tapCount(), static_cast<PlatformEvent::Modifiers>(
modifiers | PlatformEvent::LeftButtonDown),
PlatformMouseEvent::FromTouch, gestureEvent.timestamp(),
WebPointerProperties::PointerType::Mouse);
// TODO(mustaq): We suppress MEs plus all it's side effects. What would that
// mean for for TEs? What's the right balance here? crbug.com/617255
WebInputEventResult mouseDownEventResult =
WebInputEventResult::HandledSuppressed;
if (!m_suppressMouseEventsFromGestures) {
m_mouseEventManager->setClickCount(gestureEvent.tapCount());
mouseDownEventResult =
m_mouseEventManager->setMousePositionAndDispatchMouseEvent(
currentHitTest.innerNode(), EventTypeNames::mousedown,
fakeMouseDown);
m_selectionController->initializeSelectionState();
if (mouseDownEventResult == WebInputEventResult::NotHandled)
mouseDownEventResult = m_mouseEventManager->handleMouseFocus(
currentHitTest,
InputDeviceCapabilities::firesTouchEventsSourceCapabilities());
if (mouseDownEventResult == WebInputEventResult::NotHandled)
mouseDownEventResult = m_mouseEventManager->handleMousePressEvent(
MouseEventWithHitTestResults(fakeMouseDown, currentHitTest));
}
if (currentHitTest.innerNode()) {
DCHECK(gestureEvent.type() == PlatformEvent::GestureTap);
HitTestResult result = currentHitTest;
result.setToShadowHostIfInUserAgentShadowRoot();
m_frame->chromeClient().onMouseDown(result.innerNode());
}
// FIXME: Use a hit-test cache to avoid unnecessary hit tests.
// http://crbug.com/398920
if (currentHitTest.innerNode()) {
LocalFrame* mainFrame = m_frame->localFrameRoot();
if (mainFrame && mainFrame->view())
mainFrame->view()->updateAllLifecyclePhases();
adjustedPoint = frameView->rootFrameToContents(gestureEvent.position());
currentHitTest = EventHandlingUtil::hitTestResultInFrame(
m_frame, adjustedPoint, hitType);
}
PlatformMouseEvent fakeMouseUp(
gestureEvent.position(), gestureEvent.globalPosition(),
WebPointerProperties::Button::Left, PlatformEvent::MouseReleased,
gestureEvent.tapCount(), static_cast<PlatformEvent::Modifiers>(modifiers),
PlatformMouseEvent::FromTouch, gestureEvent.timestamp(),
WebPointerProperties::PointerType::Mouse);
WebInputEventResult mouseUpEventResult =
m_suppressMouseEventsFromGestures
? WebInputEventResult::HandledSuppressed
: m_mouseEventManager->setMousePositionAndDispatchMouseEvent(
currentHitTest.innerNode(), EventTypeNames::mouseup,
fakeMouseUp);
WebInputEventResult clickEventResult = WebInputEventResult::NotHandled;
if (tappedNonTextNode) {
if (currentHitTest.innerNode()) {
// Updates distribution because a mouseup (or mousedown) event listener
// can make the tree dirty at dispatchMouseEvent() invocation above.
// Unless distribution is updated, commonAncestor would hit DCHECK. Both
// tappedNonTextNode and currentHitTest.innerNode()) don't need to be
// updated because commonAncestor() will exit early if their documents are
// different.
tappedNonTextNode->updateDistribution();
Node* clickTargetNode = currentHitTest.innerNode()->commonAncestor(
*tappedNonTextNode, EventHandlingUtil::parentForClickEvent);
clickEventResult =
m_mouseEventManager->setMousePositionAndDispatchMouseEvent(
clickTargetNode, EventTypeNames::click, fakeMouseUp);
}
m_mouseEventManager->setClickNode(nullptr);
}
if (mouseUpEventResult == WebInputEventResult::NotHandled)
mouseUpEventResult = m_mouseEventManager->handleMouseReleaseEvent(
MouseEventWithHitTestResults(fakeMouseUp, currentHitTest));
m_mouseEventManager->clearDragHeuristicState();
WebInputEventResult eventResult = EventHandlingUtil::mergeEventResult(
EventHandlingUtil::mergeEventResult(mouseDownEventResult,
mouseUpEventResult),
clickEventResult);
if (eventResult == WebInputEventResult::NotHandled && tappedNode &&
m_frame->page()) {
bool domTreeChanged =
preDispatchDomTreeVersion != m_frame->document()->domTreeVersion();
bool styleChanged =
preDispatchStyleVersion != m_frame->document()->styleVersion();
IntPoint tappedPositionInViewport =
frameHost()->visualViewport().rootFrameToViewport(tappedPosition);
m_frame->chromeClient().showUnhandledTapUIIfNeeded(
tappedPositionInViewport, tappedNode, domTreeChanged || styleChanged);
}
return eventResult;
}
WebInputEventResult GestureManager::handleGestureLongPress(
const GestureEventWithHitTestResults& targetedEvent) {
const PlatformGestureEvent& gestureEvent = targetedEvent.event();
// FIXME: Ideally we should try to remove the extra mouse-specific hit-tests
// here (re-using the supplied HitTestResult), but that will require some
// overhaul of the touch drag-and-drop code and LongPress is such a special
// scenario that it's unlikely to matter much in practice.
IntPoint hitTestPoint =
m_frame->view()->rootFrameToContents(gestureEvent.position());
HitTestResult hitTestResult =
m_frame->eventHandler().hitTestResultAtPoint(hitTestPoint);
m_longTapShouldInvokeContextMenu = false;
bool hitTestContainsLinks = hitTestResult.URLElement() ||
!hitTestResult.absoluteImageURL().isNull() ||
!hitTestResult.absoluteMediaURL().isNull();
if (!hitTestContainsLinks &&
m_mouseEventManager->handleDragDropIfPossible(targetedEvent)) {
m_longTapShouldInvokeContextMenu = true;
return WebInputEventResult::HandledSystem;
}
if (m_selectionController->handleGestureLongPress(gestureEvent,
hitTestResult)) {
m_mouseEventManager->focusDocumentView();
return WebInputEventResult::HandledSystem;
}
return sendContextMenuEventForGesture(targetedEvent);
}
WebInputEventResult GestureManager::handleGestureLongTap(
const GestureEventWithHitTestResults& targetedEvent) {
#if !OS(ANDROID)
if (m_longTapShouldInvokeContextMenu) {
m_longTapShouldInvokeContextMenu = false;
return sendContextMenuEventForGesture(targetedEvent);
}
#endif
return WebInputEventResult::NotHandled;
}
WebInputEventResult GestureManager::handleGestureTwoFingerTap(
const GestureEventWithHitTestResults& targetedEvent) {
return sendContextMenuEventForGesture(targetedEvent);
}
WebInputEventResult GestureManager::sendContextMenuEventForGesture(
const GestureEventWithHitTestResults& targetedEvent) {
const PlatformGestureEvent& gestureEvent = targetedEvent.event();
unsigned modifiers = gestureEvent.getModifiers();
if (!m_suppressMouseEventsFromGestures) {
// Send MouseMoved event prior to handling (https://crbug.com/485290).
PlatformMouseEvent fakeMouseMove(
gestureEvent.position(), gestureEvent.globalPosition(),
WebPointerProperties::Button::NoButton, PlatformEvent::MouseMoved,
/* clickCount */ 0, static_cast<PlatformEvent::Modifiers>(modifiers),
PlatformMouseEvent::FromTouch, gestureEvent.timestamp(),
WebPointerProperties::PointerType::Mouse);
m_mouseEventManager->setMousePositionAndDispatchMouseEvent(
targetedEvent.hitTestResult().innerNode(), EventTypeNames::mousemove,
fakeMouseMove);
}
PlatformEvent::EventType eventType = PlatformEvent::MousePressed;
if (m_frame->settings() && m_frame->settings()->showContextMenuOnMouseUp())
eventType = PlatformEvent::MouseReleased;
PlatformMouseEvent mouseEvent(
targetedEvent.event().position(), targetedEvent.event().globalPosition(),
WebPointerProperties::Button::NoButton, eventType, /* clickCount */ 0,
static_cast<PlatformEvent::Modifiers>(modifiers),
PlatformMouseEvent::FromTouch, WTF::monotonicallyIncreasingTime(),
WebPointerProperties::PointerType::Mouse);
if (!m_suppressMouseEventsFromGestures && m_frame->view()) {
HitTestRequest request(HitTestRequest::Active);
LayoutPoint documentPoint =
m_frame->view()->rootFrameToContents(targetedEvent.event().position());
MouseEventWithHitTestResults mev =
m_frame->document()->performMouseEventHitTest(request, documentPoint,
mouseEvent);
WebInputEventResult eventResult =
m_mouseEventManager->setMousePositionAndDispatchMouseEvent(
mev.innerNode(), EventTypeNames::mousedown, mouseEvent);
if (eventResult == WebInputEventResult::NotHandled)
eventResult = m_mouseEventManager->handleMouseFocus(
mev.hitTestResult(),
InputDeviceCapabilities::firesTouchEventsSourceCapabilities());
if (eventResult == WebInputEventResult::NotHandled)
m_mouseEventManager->handleMousePressEvent(mev);
}
return m_frame->eventHandler().sendContextMenuEvent(mouseEvent);
}
WebInputEventResult GestureManager::handleGestureShowPress() {
m_lastShowPressTimestamp = WTF::monotonicallyIncreasingTime();
FrameView* view = m_frame->view();
if (!view)
return WebInputEventResult::NotHandled;
if (ScrollAnimatorBase* scrollAnimator = view->existingScrollAnimator())
scrollAnimator->cancelAnimation();
const FrameView::ScrollableAreaSet* areas = view->scrollableAreas();
if (!areas)
return WebInputEventResult::NotHandled;
for (const ScrollableArea* scrollableArea : *areas) {
ScrollAnimatorBase* animator = scrollableArea->existingScrollAnimator();
if (animator)
animator->cancelAnimation();
}
return WebInputEventResult::NotHandled;
}
FrameHost* GestureManager::frameHost() const {
if (!m_frame->page())
return nullptr;
return &m_frame->page()->frameHost();
}
double GestureManager::getLastShowPressTimestamp() const {
return m_lastShowPressTimestamp;
}
} // namespace blink