gfx/layers/apz/util/APZEventState.cpp
author Dorel Luca <dluca@mozilla.com>
Fri, 01 Mar 2019 23:28:36 +0200
changeset 519910 48b049ff10f16c25a2ac43812d8087a044bd04ca
parent 519874 77625f533af6a3628ad4203530dbe1b4526aceb6
child 520028 0ff8915bda3dfbd08dcb4350fb2632e0d01b8c8d
permissions -rw-r--r--
Backed out 3 changesets (bug 1525570) for mochitest failures in gfx/layers/apz/test/mochitest/test_group_zoom.html | helper_zoom_prevented.html Backed out changeset ee394d0b085d (bug 1525570) Backed out changeset 77625f533af6 (bug 1525570) Backed out changeset cd326f5e4eb8 (bug 1525570)

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/dom/MouseEventBinding.h"
#include "mozilla/dom/TabChild.h"
#include "mozilla/dom/TabGroup.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 "nsIDOMWindowUtils.h"
#include "nsINamed.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 "nsLayoutUtils.h"
#include "nsIScrollableFrame.h"
#include "nsIScrollbarMediator.h"
#include "mozilla/TouchEvents.h"
#include "mozilla/widget/nsAutoRollup.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(std::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 nsINamed {
 public:
  NS_DECL_ISUPPORTS

  DelayedFireSingleTapEvent(nsWeakPtr aWidget, LayoutDevicePoint& aPoint,
                            Modifiers aModifiers, int32_t aClickCount,
                            nsITimer* aTimer, RefPtr<nsIContent>& aTouchRollup)
      : mWidget(aWidget),
        mPoint(aPoint),
        mModifiers(aModifiers),
        mClickCount(aClickCount)
        // Hold the reference count until we are called back.
        ,
        mTimer(aTimer),
        mTouchRollup(aTouchRollup) {}

  NS_IMETHOD Notify(nsITimer*) override {
    if (nsCOMPtr<nsIWidget> widget = do_QueryReferent(mWidget)) {
      widget::nsAutoRollup rollup(mTouchRollup.get());
      APZCCallbackHelper::FireSingleTapEvent(mPoint, mModifiers, mClickCount,
                                             widget);
    }
    mTimer = nullptr;
    return NS_OK;
  }

  NS_IMETHOD
  GetName(nsACString& aName) override {
    aName.AssignLiteral("DelayedFireSingleTapEvent");
    return NS_OK;
  }

  void ClearTimer() { mTimer = nullptr; }

 private:
  ~DelayedFireSingleTapEvent() {}

  nsWeakPtr mWidget;
  LayoutDevicePoint mPoint;
  Modifiers mModifiers;
  int32_t mClickCount;
  nsCOMPtr<nsITimer> mTimer;
  RefPtr<nsIContent> mTouchRollup;
};

NS_IMPL_ISUPPORTS(DelayedFireSingleTapEvent, nsITimerCallback, nsINamed)

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);

  RefPtr<nsIContent> touchRollup = GetTouchRollup();
  mTouchRollup = nullptr;

  nsCOMPtr<nsIWidget> widget = GetWidget();
  if (!widget) {
    return;
  }

  if (mTouchEndCancelled) {
    return;
  }

  LayoutDevicePoint ldPoint = aPoint * aScale;

  APZES_LOG("Scheduling timer for click event\n");
  nsCOMPtr<nsITimer> timer = NS_NewTimer();
  dom::TabChild* tabChild = widget->GetOwningTabChild();

  if (tabChild && XRE_IsContentProcess()) {
    timer->SetTarget(tabChild->TabGroup()->EventTargetFor(TaskCategory::Other));
  }
  RefPtr<DelayedFireSingleTapEvent> callback = new DelayedFireSingleTapEvent(
      mWidget, ldPoint, aModifiers, aClickCount, timer, touchRollup);
  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) {
  // Synthesize mousemove event for allowing users to emulate to move mouse
  // cursor over the element.  As a result, users can open submenu UI which
  // is opened when mouse cursor is moved over a link (i.e., it's a case that
  // users cannot stay in the page after tapping it).  So, this improves
  // accessibility in websites which are designed for desktop.
  // Note that we don't need to check whether mousemove event is consumed or
  // not because Chrome also ignores the result.
  APZCCallbackHelper::DispatchSynthesizedMouseEvent(
      eMouseMove, 0 /* time */, aPoint * aScale, aModifiers, 0 /* clickCount */,
      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,
      dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH,
      0 /* Use the default value here. */);

  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;
      mTouchRollup = do_GetWeakReference(widget::nsAutoRollup::GetLastRollup());

      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:
      MOZ_ASSERT_UNREACHABLE("Unknown touch event type");
      break;
  }

  if (sentContentResponse && !isTouchPrevented &&
      aApzResponse == nsEventStatus_eConsumeDoDefault &&
      gfxPrefs::PointerEventsEnabled()) {
    WidgetTouchEvent cancelEvent(aEvent);
    cancelEvent.mMessage = eTouchPointerCancel;
    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) {
  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);
      dom::Document* 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);
      dom::Document* 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;
    }
  }
}

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();
}

already_AddRefed<nsIContent> APZEventState::GetTouchRollup() const {
  nsCOMPtr<nsIContent> result = do_QueryReferent(mTouchRollup);
  return result.forget();
}

}  // namespace layers
}  // namespace mozilla