summaryrefslogtreecommitdiffstats
path: root/gfx/layers/apz/util/APZEventState.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/layers/apz/util/APZEventState.cpp')
-rw-r--r--gfx/layers/apz/util/APZEventState.cpp512
1 files changed, 512 insertions, 0 deletions
diff --git a/gfx/layers/apz/util/APZEventState.cpp b/gfx/layers/apz/util/APZEventState.cpp
new file mode 100644
index 000000000..20a41eed5
--- /dev/null
+++ b/gfx/layers/apz/util/APZEventState.cpp
@@ -0,0 +1,512 @@
+/* -*- 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