Mypal/gfx/layers/apz/util/APZEventState.cpp
2019-03-11 13:26:37 +03:00

513 lines
17 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "APZEventState.h"
#include "ActiveElementManager.h"
#include "APZCCallbackHelper.h"
#include "gfxPrefs.h"
#include "LayersLogging.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/Move.h"
#include "mozilla/Preferences.h"
#include "mozilla/TouchEvents.h"
#include "mozilla/layers/APZCCallbackHelper.h"
#include "nsCOMPtr.h"
#include "nsDocShell.h"
#include "nsIDOMMouseEvent.h"
#include "nsIDOMWindowUtils.h"
#include "nsIScrollableFrame.h"
#include "nsIScrollbarMediator.h"
#include "nsITimer.h"
#include "nsIWeakReferenceUtils.h"
#include "nsIWidget.h"
#include "nsLayoutUtils.h"
#include "nsQueryFrame.h"
#include "TouchManager.h"
#include "nsIDOMMouseEvent.h"
#include "nsLayoutUtils.h"
#include "nsIScrollableFrame.h"
#include "nsIScrollbarMediator.h"
#include "mozilla/TouchEvents.h"
#define APZES_LOG(...)
// #define APZES_LOG(...) printf_stderr("APZES: " __VA_ARGS__)
// Static helper functions
namespace {
int32_t
WidgetModifiersToDOMModifiers(mozilla::Modifiers aModifiers)
{
int32_t result = 0;
if (aModifiers & mozilla::MODIFIER_SHIFT) {
result |= nsIDOMWindowUtils::MODIFIER_SHIFT;
}
if (aModifiers & mozilla::MODIFIER_CONTROL) {
result |= nsIDOMWindowUtils::MODIFIER_CONTROL;
}
if (aModifiers & mozilla::MODIFIER_ALT) {
result |= nsIDOMWindowUtils::MODIFIER_ALT;
}
if (aModifiers & mozilla::MODIFIER_META) {
result |= nsIDOMWindowUtils::MODIFIER_META;
}
if (aModifiers & mozilla::MODIFIER_ALTGRAPH) {
result |= nsIDOMWindowUtils::MODIFIER_ALTGRAPH;
}
if (aModifiers & mozilla::MODIFIER_CAPSLOCK) {
result |= nsIDOMWindowUtils::MODIFIER_CAPSLOCK;
}
if (aModifiers & mozilla::MODIFIER_FN) {
result |= nsIDOMWindowUtils::MODIFIER_FN;
}
if (aModifiers & mozilla::MODIFIER_FNLOCK) {
result |= nsIDOMWindowUtils::MODIFIER_FNLOCK;
}
if (aModifiers & mozilla::MODIFIER_NUMLOCK) {
result |= nsIDOMWindowUtils::MODIFIER_NUMLOCK;
}
if (aModifiers & mozilla::MODIFIER_SCROLLLOCK) {
result |= nsIDOMWindowUtils::MODIFIER_SCROLLLOCK;
}
if (aModifiers & mozilla::MODIFIER_SYMBOL) {
result |= nsIDOMWindowUtils::MODIFIER_SYMBOL;
}
if (aModifiers & mozilla::MODIFIER_SYMBOLLOCK) {
result |= nsIDOMWindowUtils::MODIFIER_SYMBOLLOCK;
}
if (aModifiers & mozilla::MODIFIER_OS) {
result |= nsIDOMWindowUtils::MODIFIER_OS;
}
return result;
}
} // namespace
namespace mozilla {
namespace layers {
static int32_t sActiveDurationMs = 10;
static bool sActiveDurationMsSet = false;
APZEventState::APZEventState(nsIWidget* aWidget,
ContentReceivedInputBlockCallback&& aCallback)
: mWidget(nullptr) // initialized in constructor body
, mActiveElementManager(new ActiveElementManager())
, mContentReceivedInputBlockCallback(Move(aCallback))
, mPendingTouchPreventedResponse(false)
, mPendingTouchPreventedBlockId(0)
, mEndTouchIsClick(false)
, mTouchEndCancelled(false)
, mLastTouchIdentifier(0)
{
nsresult rv;
mWidget = do_GetWeakReference(aWidget, &rv);
MOZ_ASSERT(NS_SUCCEEDED(rv), "APZEventState constructed with a widget that"
" does not support weak references. APZ will NOT work!");
if (!sActiveDurationMsSet) {
Preferences::AddIntVarCache(&sActiveDurationMs,
"ui.touch_activation.duration_ms",
sActiveDurationMs);
sActiveDurationMsSet = true;
}
}
APZEventState::~APZEventState()
{}
class DelayedFireSingleTapEvent final : public nsITimerCallback
{
public:
NS_DECL_ISUPPORTS
DelayedFireSingleTapEvent(nsWeakPtr aWidget,
LayoutDevicePoint& aPoint,
Modifiers aModifiers,
int32_t aClickCount,
nsITimer* aTimer)
: mWidget(aWidget)
, mPoint(aPoint)
, mModifiers(aModifiers)
, mClickCount(aClickCount)
// Hold the reference count until we are called back.
, mTimer(aTimer)
{
}
NS_IMETHOD Notify(nsITimer*) override
{
if (nsCOMPtr<nsIWidget> widget = do_QueryReferent(mWidget)) {
APZCCallbackHelper::FireSingleTapEvent(mPoint, mModifiers, mClickCount, widget);
}
mTimer = nullptr;
return NS_OK;
}
void ClearTimer() {
mTimer = nullptr;
}
private:
~DelayedFireSingleTapEvent()
{
}
nsWeakPtr mWidget;
LayoutDevicePoint mPoint;
Modifiers mModifiers;
int32_t mClickCount;
nsCOMPtr<nsITimer> mTimer;
};
NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback)
void
APZEventState::ProcessSingleTap(const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale,
Modifiers aModifiers,
const ScrollableLayerGuid& aGuid,
int32_t aClickCount)
{
APZES_LOG("Handling single tap at %s on %s with %d\n",
Stringify(aPoint).c_str(), Stringify(aGuid).c_str(), mTouchEndCancelled);
nsCOMPtr<nsIWidget> widget = GetWidget();
if (!widget) {
return;
}
if (mTouchEndCancelled) {
return;
}
LayoutDevicePoint ldPoint = aPoint * aScale;
if (!mActiveElementManager->ActiveElementUsesStyle()) {
// If the active element isn't visually affected by the :active style, we
// have no need to wait the extra sActiveDurationMs to make the activation
// visually obvious to the user.
APZCCallbackHelper::FireSingleTapEvent(ldPoint, aModifiers, aClickCount, widget);
return;
}
APZES_LOG("Active element uses style, scheduling timer for click event\n");
nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID);
RefPtr<DelayedFireSingleTapEvent> callback =
new DelayedFireSingleTapEvent(mWidget, ldPoint, aModifiers, aClickCount, timer);
nsresult rv = timer->InitWithCallback(callback,
sActiveDurationMs,
nsITimer::TYPE_ONE_SHOT);
if (NS_FAILED(rv)) {
// Make |callback| not hold the timer, so they will both be destructed when
// we leave the scope of this function.
callback->ClearTimer();
}
}
bool
APZEventState::FireContextmenuEvents(const nsCOMPtr<nsIPresShell>& aPresShell,
const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale,
Modifiers aModifiers,
const nsCOMPtr<nsIWidget>& aWidget)
{
// Converting the modifiers to DOM format for the DispatchMouseEvent call
// is the most useless thing ever because nsDOMWindowUtils::SendMouseEvent
// just converts them back to widget format, but that API has many callers,
// including in JS code, so it's not trivial to change.
bool eventHandled =
APZCCallbackHelper::DispatchMouseEvent(aPresShell, NS_LITERAL_STRING("contextmenu"),
aPoint, 2, 1, WidgetModifiersToDOMModifiers(aModifiers), true,
nsIDOMMouseEvent::MOZ_SOURCE_TOUCH);
APZES_LOG("Contextmenu event handled: %d\n", eventHandled);
if (eventHandled) {
// If the contextmenu event was handled then we're showing a contextmenu,
// and so we should remove any activation
mActiveElementManager->ClearActivation();
#ifndef XP_WIN
} else {
// If the contextmenu wasn't consumed, fire the eMouseLongTap event.
nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers,
/*clickCount*/ 1, aWidget);
eventHandled = (status == nsEventStatus_eConsumeNoDefault);
APZES_LOG("eMouseLongTap event handled: %d\n", eventHandled);
#endif
}
return eventHandled;
}
void
APZEventState::ProcessLongTap(const nsCOMPtr<nsIPresShell>& aPresShell,
const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale,
Modifiers aModifiers,
const ScrollableLayerGuid& aGuid,
uint64_t aInputBlockId)
{
APZES_LOG("Handling long tap at %s\n", Stringify(aPoint).c_str());
nsCOMPtr<nsIWidget> widget = GetWidget();
if (!widget) {
return;
}
SendPendingTouchPreventedResponse(false);
#ifdef XP_WIN
// On Windows, we fire the contextmenu events when the user lifts their
// finger, in keeping with the platform convention. This happens in the
// ProcessLongTapUp function. However, we still fire the eMouseLongTap event
// at this time, because things like text selection or dragging may want
// to know about it.
nsEventStatus status = APZCCallbackHelper::DispatchSynthesizedMouseEvent(
eMouseLongTap, /*time*/ 0, aPoint * aScale, aModifiers, /*clickCount*/ 1,
widget);
bool eventHandled = (status == nsEventStatus_eConsumeNoDefault);
#else
bool eventHandled = FireContextmenuEvents(aPresShell, aPoint, aScale,
aModifiers, widget);
#endif
mContentReceivedInputBlockCallback(aGuid, aInputBlockId, eventHandled);
if (eventHandled) {
// Also send a touchcancel to content, so that listeners that might be
// waiting for a touchend don't trigger.
WidgetTouchEvent cancelTouchEvent(true, eTouchCancel, widget.get());
cancelTouchEvent.mModifiers = aModifiers;
auto ldPoint = LayoutDeviceIntPoint::Round(aPoint * aScale);
cancelTouchEvent.mTouches.AppendElement(new mozilla::dom::Touch(mLastTouchIdentifier,
ldPoint, LayoutDeviceIntPoint(), 0, 0));
APZCCallbackHelper::DispatchWidgetEvent(cancelTouchEvent);
}
}
void
APZEventState::ProcessLongTapUp(const nsCOMPtr<nsIPresShell>& aPresShell,
const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale,
Modifiers aModifiers)
{
#ifdef XP_WIN
nsCOMPtr<nsIWidget> widget = GetWidget();
if (widget) {
FireContextmenuEvents(aPresShell, aPoint, aScale, aModifiers, widget);
}
#endif
}
void
APZEventState::ProcessTouchEvent(const WidgetTouchEvent& aEvent,
const ScrollableLayerGuid& aGuid,
uint64_t aInputBlockId,
nsEventStatus aApzResponse,
nsEventStatus aContentResponse)
{
if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() > 0) {
mActiveElementManager->SetTargetElement(aEvent.mTouches[0]->GetTarget());
mLastTouchIdentifier = aEvent.mTouches[0]->Identifier();
}
bool isTouchPrevented = aContentResponse == nsEventStatus_eConsumeNoDefault;
bool sentContentResponse = false;
APZES_LOG("Handling event type %d\n", aEvent.mMessage);
switch (aEvent.mMessage) {
case eTouchStart: {
mTouchEndCancelled = false;
sentContentResponse = SendPendingTouchPreventedResponse(false);
// sentContentResponse can be true here if we get two TOUCH_STARTs in a row
// and just responded to the first one.
// We're about to send a response back to APZ, but we should only do it
// for events that went through APZ (which should be all of them).
MOZ_ASSERT(aEvent.mFlags.mHandledByAPZ);
if (isTouchPrevented) {
mContentReceivedInputBlockCallback(aGuid, aInputBlockId, isTouchPrevented);
sentContentResponse = true;
} else {
APZES_LOG("Event not prevented; pending response for %" PRIu64 " %s\n",
aInputBlockId, Stringify(aGuid).c_str());
mPendingTouchPreventedResponse = true;
mPendingTouchPreventedGuid = aGuid;
mPendingTouchPreventedBlockId = aInputBlockId;
}
break;
}
case eTouchEnd:
if (isTouchPrevented) {
mTouchEndCancelled = true;
mEndTouchIsClick = false;
}
MOZ_FALLTHROUGH;
case eTouchCancel:
mActiveElementManager->HandleTouchEndEvent(mEndTouchIsClick);
MOZ_FALLTHROUGH;
case eTouchMove: {
if (mPendingTouchPreventedResponse) {
MOZ_ASSERT(aGuid == mPendingTouchPreventedGuid);
}
sentContentResponse = SendPendingTouchPreventedResponse(isTouchPrevented);
break;
}
default:
NS_WARNING("Unknown touch event type");
}
if (sentContentResponse &&
aApzResponse == nsEventStatus_eConsumeDoDefault &&
gfxPrefs::PointerEventsEnabled()) {
WidgetTouchEvent cancelEvent(aEvent);
cancelEvent.mMessage = eTouchCancel;
cancelEvent.mFlags.mCancelable = false; // mMessage != eTouchCancel;
for (uint32_t i = 0; i < cancelEvent.mTouches.Length(); ++i) {
if (mozilla::dom::Touch* touch = cancelEvent.mTouches[i]) {
touch->convertToPointer = true;
}
}
nsEventStatus status;
cancelEvent.mWidget->DispatchEvent(&cancelEvent, status);
}
}
void
APZEventState::ProcessWheelEvent(const WidgetWheelEvent& aEvent,
const ScrollableLayerGuid& aGuid,
uint64_t aInputBlockId)
{
// If this event starts a swipe, indicate that it shouldn't result in a
// scroll by setting defaultPrevented to true.
bool defaultPrevented = aEvent.DefaultPrevented() || aEvent.TriggersSwipe();
mContentReceivedInputBlockCallback(aGuid, aInputBlockId, defaultPrevented);
}
void
APZEventState::ProcessMouseEvent(const WidgetMouseEvent& aEvent,
const ScrollableLayerGuid& aGuid,
uint64_t aInputBlockId)
{
// If we get here and the drag block has not been confirmed by the code in
// nsSliderFrame, then no scrollbar reacted to the event thus APZC will
// ignore this drag block. We can send defaultPrevented as either true or
// false, it doesn't matter, because APZ won't have the scrollbar metrics
// anyway, and will know to drop the block.
bool defaultPrevented = false;
mContentReceivedInputBlockCallback(aGuid, aInputBlockId, defaultPrevented);
}
void
APZEventState::ProcessAPZStateChange(ViewID aViewId,
APZStateChange aChange,
int aArg)
{
switch (aChange)
{
case APZStateChange::eTransformBegin:
{
nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
if (sf) {
sf->SetTransformingByAPZ(true);
}
nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf);
if (scrollbarMediator) {
scrollbarMediator->ScrollbarActivityStarted();
}
nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
nsIDocument* doc = content ? content->GetComposedDoc() : nullptr;
nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
if (docshell && sf) {
nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
nsdocshell->NotifyAsyncPanZoomStarted();
}
break;
}
case APZStateChange::eTransformEnd:
{
nsIScrollableFrame* sf = nsLayoutUtils::FindScrollableFrameFor(aViewId);
if (sf) {
sf->SetTransformingByAPZ(false);
}
nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sf);
if (scrollbarMediator) {
scrollbarMediator->ScrollbarActivityStopped();
}
nsIContent* content = nsLayoutUtils::FindContentFor(aViewId);
nsIDocument* doc = content ? content->GetComposedDoc() : nullptr;
nsCOMPtr<nsIDocShell> docshell(doc ? doc->GetDocShell() : nullptr);
if (docshell && sf) {
nsDocShell* nsdocshell = static_cast<nsDocShell*>(docshell.get());
nsdocshell->NotifyAsyncPanZoomStopped();
}
break;
}
case APZStateChange::eStartTouch:
{
mActiveElementManager->HandleTouchStart(aArg);
break;
}
case APZStateChange::eStartPanning:
{
// The user started to pan, so we don't want anything to be :active.
mActiveElementManager->ClearActivation();
break;
}
case APZStateChange::eEndTouch:
{
mEndTouchIsClick = aArg;
mActiveElementManager->HandleTouchEnd();
break;
}
case APZStateChange::eSentinel:
// Should never happen, but we want this case branch to stop the compiler
// whining about unhandled values.
MOZ_ASSERT(false);
break;
}
}
void
APZEventState::ProcessClusterHit()
{
// If we hit a cluster of links then we shouldn't activate any of them,
// as we will be showing the zoomed view. (This is only called on Fennec).
#ifndef MOZ_WIDGET_ANDROID
MOZ_ASSERT(false);
#endif
mActiveElementManager->ClearActivation();
}
bool
APZEventState::SendPendingTouchPreventedResponse(bool aPreventDefault)
{
if (mPendingTouchPreventedResponse) {
APZES_LOG("Sending response %d for pending guid: %s\n", aPreventDefault,
Stringify(mPendingTouchPreventedGuid).c_str());
mContentReceivedInputBlockCallback(mPendingTouchPreventedGuid,
mPendingTouchPreventedBlockId, aPreventDefault);
mPendingTouchPreventedResponse = false;
return true;
}
return false;
}
already_AddRefed<nsIWidget>
APZEventState::GetWidget() const
{
nsCOMPtr<nsIWidget> result = do_QueryReferent(mWidget);
return result.forget();
}
} // namespace layers
} // namespace mozilla