Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "EventStateManager.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/Attributes.h"
#include "mozilla/EditorBase.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventForwards.h"
#include "mozilla/Hal.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/Likely.h"
#include "mozilla/FocusModel.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PointerLockManager.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/ScrollTypes.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextControlElement.h"
#include "mozilla/TextEditor.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TouchEvents.h"
#include "mozilla/Telemetry.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/BrowserBridgeChild.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/DOMIntersectionObserver.h"
#include "mozilla/dom/DragEvent.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/FrameLoaderBinding.h"
#include "mozilla/dom/HTMLLabelElement.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/PointerEventHandler.h"
#include "mozilla/dom/UIEvent.h"
#include "mozilla/dom/UIEventBinding.h"
#include "mozilla/dom/UserActivation.h"
#include "mozilla/dom/WheelEventBinding.h"
#include "mozilla/glean/GleanMetrics.h"
#include "mozilla/ScrollContainerFrame.h"
#include "mozilla/StaticPrefs_accessibility.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/StaticPrefs_mousewheel.h"
#include "mozilla/StaticPrefs_ui.h"
#include "mozilla/StaticPrefs_zoom.h"
#include "ContentEventHandler.h"
#include "IMEContentObserver.h"
#include "WheelHandlingHelper.h"
#include "RemoteDragStartData.h"
#include "nsCommandParams.h"
#include "nsCOMPtr.h"
#include "nsCopySupport.h"
#include "nsFocusManager.h"
#include "nsGenericHTMLElement.h"
#include "nsIClipboard.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "mozilla/dom/Document.h"
#include "nsICookieJarSettings.h"
#include "nsIFrame.h"
#include "nsFrameLoaderOwner.h"
#include "nsIWeakReferenceUtils.h"
#include "nsIWidget.h"
#include "nsLiteralString.h"
#include "nsPresContext.h"
#include "nsTArray.h"
#include "nsGkAtoms.h"
#include "nsIFormControl.h"
#include "nsComboboxControlFrame.h"
#include "nsIDOMXULControlElement.h"
#include "nsNameSpaceManager.h"
#include "nsIBaseWindow.h"
#include "nsFrameSelection.h"
#include "nsPIDOMWindow.h"
#include "nsPIWindowRoot.h"
#include "nsIWebNavigation.h"
#include "nsIDocumentViewer.h"
#include "nsFrameManager.h"
#include "nsIBrowserChild.h"
#include "nsMenuPopupFrame.h"
#include "nsIObserverService.h"
#include "nsIDocShell.h"
#include "nsSubDocumentFrame.h"
#include "nsLayoutUtils.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsUnicharUtils.h"
#include "nsContentUtils.h"
#include "imgIContainer.h"
#include "nsIProperties.h"
#include "nsISupportsPrimitives.h"
#include "nsServiceManagerUtils.h"
#include "nsITimer.h"
#include "nsFontMetrics.h"
#include "nsIDragService.h"
#include "nsIDragSession.h"
#include "mozilla/dom/DataTransfer.h"
#include "nsContentAreaDragDrop.h"
#include "nsTreeBodyFrame.h"
#include "nsIController.h"
#include "mozilla/Services.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/Record.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/Preferences.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/ProfilerLabels.h"
#include "Units.h"
#ifdef XP_MACOSX
# import <ApplicationServices/ApplicationServices.h>
#endif
namespace mozilla {
using namespace dom;
static const LayoutDeviceIntPoint kInvalidRefPoint =
LayoutDeviceIntPoint(-1, -1);
static uint32_t gMouseOrKeyboardEventCounter = 0;
static nsITimer* gUserInteractionTimer = nullptr;
static nsITimerCallback* gUserInteractionTimerCallback = nullptr;
static const double kCursorLoadingTimeout = 1000; // ms
static AutoWeakFrame gLastCursorSourceFrame;
static TimeStamp gLastCursorUpdateTime;
static TimeStamp gTypingStartTime;
static TimeStamp gTypingEndTime;
static int32_t gTypingInteractionKeyPresses = 0;
static dom::InteractionData gTypingInteraction = {};
static inline int32_t RoundDown(double aDouble) {
return (aDouble > 0) ? static_cast<int32_t>(floor(aDouble))
: static_cast<int32_t>(ceil(aDouble));
}
static bool IsSelectingLink(nsIFrame* aTargetFrame) {
if (!aTargetFrame) {
return false;
}
const nsFrameSelection* frameSel = aTargetFrame->GetConstFrameSelection();
if (!frameSel || !frameSel->GetDragState()) {
return false;
}
if (!nsContentUtils::GetClosestLinkInFlatTree(aTargetFrame->GetContent())) {
return false;
}
return true;
}
static UniquePtr<WidgetMouseEvent> CreateMouseOrPointerWidgetEvent(
WidgetMouseEvent* aMouseEvent, EventMessage aMessage,
EventTarget* aRelatedTarget);
/**
* Returns the common ancestor for mouseup purpose, given the
* current mouseup target and the previous mousedown target.
*/
static nsINode* GetCommonAncestorForMouseUp(
nsINode* aCurrentMouseUpTarget, nsINode* aLastMouseDownTarget,
Maybe<FormControlType>& aLastMouseDownInputControlType) {
if (!aCurrentMouseUpTarget || !aLastMouseDownTarget) {
return nullptr;
}
if (aCurrentMouseUpTarget == aLastMouseDownTarget) {
return aCurrentMouseUpTarget;
}
// Build the chain of parents
AutoTArray<nsINode*, 30> parents1;
do {
parents1.AppendElement(aCurrentMouseUpTarget);
aCurrentMouseUpTarget = aCurrentMouseUpTarget->GetFlattenedTreeParentNode();
} while (aCurrentMouseUpTarget);
AutoTArray<nsINode*, 30> parents2;
do {
parents2.AppendElement(aLastMouseDownTarget);
if (aLastMouseDownTarget == parents1.LastElement()) {
break;
}
aLastMouseDownTarget = aLastMouseDownTarget->GetFlattenedTreeParentNode();
} while (aLastMouseDownTarget);
// Find where the parent chain differs
uint32_t pos1 = parents1.Length();
uint32_t pos2 = parents2.Length();
nsINode* parent = nullptr;
for (uint32_t len = std::min(pos1, pos2); len > 0; --len) {
nsINode* child1 = parents1.ElementAt(--pos1);
nsINode* child2 = parents2.ElementAt(--pos2);
if (child1 != child2) {
break;
}
// If the input control type is different between mouseup and mousedown,
// this is not a valid click.
if (HTMLInputElement* input = HTMLInputElement::FromNodeOrNull(child1)) {
if (aLastMouseDownInputControlType.isSome() &&
aLastMouseDownInputControlType.ref() != input->ControlType()) {
break;
}
}
parent = child1;
}
return parent;
}
LazyLogModule sMouseBoundaryLog("MouseBoundaryEvents");
LazyLogModule sPointerBoundaryLog("PointerBoundaryEvents");
/******************************************************************/
/* mozilla::UITimerCallback */
/******************************************************************/
class UITimerCallback final : public nsITimerCallback, public nsINamed {
public:
UITimerCallback() : mPreviousCount(0) {}
NS_DECL_ISUPPORTS
NS_DECL_NSITIMERCALLBACK
NS_DECL_NSINAMED
private:
~UITimerCallback() = default;
uint32_t mPreviousCount;
};
NS_IMPL_ISUPPORTS(UITimerCallback, nsITimerCallback, nsINamed)
// If aTimer is nullptr, this method always sends "user-interaction-inactive"
// notification.
NS_IMETHODIMP
UITimerCallback::Notify(nsITimer* aTimer) {
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (!obs) return NS_ERROR_FAILURE;
if ((gMouseOrKeyboardEventCounter == mPreviousCount) || !aTimer) {
gMouseOrKeyboardEventCounter = 0;
obs->NotifyObservers(nullptr, "user-interaction-inactive", nullptr);
if (gUserInteractionTimer) {
gUserInteractionTimer->Cancel();
NS_RELEASE(gUserInteractionTimer);
}
} else {
obs->NotifyObservers(nullptr, "user-interaction-active", nullptr);
EventStateManager::UpdateUserActivityTimer();
if (XRE_IsParentProcess()) {
hal::BatteryInformation batteryInfo;
hal::GetCurrentBatteryInformation(&batteryInfo);
glean::power_battery::percentage_when_user_active.AccumulateSingleSample(
uint64_t(batteryInfo.level() * 100));
}
}
mPreviousCount = gMouseOrKeyboardEventCounter;
return NS_OK;
}
NS_IMETHODIMP
UITimerCallback::GetName(nsACString& aName) {
aName.AssignLiteral("UITimerCallback_timer");
return NS_OK;
}
/******************************************************************/
/* mozilla::OverOutElementsWrapper */
/******************************************************************/
NS_IMPL_CYCLE_COLLECTION(OverOutElementsWrapper, mDeepestEnterEventTarget,
mDispatchingOverEventTarget,
mDispatchingOutOrDeepestLeaveEventTarget)
NS_IMPL_CYCLE_COLLECTING_ADDREF(OverOutElementsWrapper)
NS_IMPL_CYCLE_COLLECTING_RELEASE(OverOutElementsWrapper)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(OverOutElementsWrapper)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
already_AddRefed<nsIWidget> OverOutElementsWrapper::GetLastOverWidget() const {
nsCOMPtr<nsIWidget> widget = do_QueryReferent(mLastOverWidget);
return widget.forget();
}
void OverOutElementsWrapper::ContentRemoved(nsIContent& aContent) {
if (!mDeepestEnterEventTarget) {
return;
}
if (!nsContentUtils::ContentIsFlattenedTreeDescendantOf(
mDeepestEnterEventTarget, &aContent)) {
return;
}
LogModule* const logModule = mType == BoundaryEventType::Mouse
? sMouseBoundaryLog
: sPointerBoundaryLog;
if (!StaticPrefs::
dom_events_mouse_pointer_boundary_keep_enter_targets_after_over_target_removed()) {
MOZ_LOG(logModule, LogLevel::Info,
("The last \"over\" event target (%p) is removed",
mDeepestEnterEventTarget.get()));
StoreOverEventTargetAndDeepestEnterEventTarget(nullptr);
return;
}
if (mDispatchingOverEventTarget &&
(mDeepestEnterEventTarget == mDispatchingOverEventTarget ||
nsContentUtils::ContentIsFlattenedTreeDescendantOf(
mDispatchingOverEventTarget, &aContent))) {
if (mDispatchingOverEventTarget ==
mDispatchingOutOrDeepestLeaveEventTarget) {
MOZ_LOG(logModule, LogLevel::Info,
("The dispatching \"%s\" event target (%p) is removed",
LastOverEventTargetIsOutEventTarget() ? "out" : "leave",
mDispatchingOutOrDeepestLeaveEventTarget.get()));
mDispatchingOutOrDeepestLeaveEventTarget = nullptr;
}
MOZ_LOG(logModule, LogLevel::Info,
("The dispatching \"over\" event target (%p) is removed",
mDispatchingOverEventTarget.get()));
mDispatchingOverEventTarget = nullptr;
}
if (mDispatchingOutOrDeepestLeaveEventTarget &&
(mDeepestEnterEventTarget == mDispatchingOutOrDeepestLeaveEventTarget ||
nsContentUtils::ContentIsFlattenedTreeDescendantOf(
mDispatchingOutOrDeepestLeaveEventTarget, &aContent))) {
MOZ_LOG(logModule, LogLevel::Info,
("The dispatching \"%s\" event target (%p) is removed",
LastOverEventTargetIsOutEventTarget() ? "out" : "leave",
mDispatchingOutOrDeepestLeaveEventTarget.get()));
mDispatchingOutOrDeepestLeaveEventTarget = nullptr;
}
MOZ_LOG(logModule, LogLevel::Info,
("The last \"%s\" event target (%p) is removed and now the last "
"deepest enter target becomes %s(%p)",
LastOverEventTargetIsOutEventTarget() ? "over" : "enter",
mDeepestEnterEventTarget.get(),
aContent.GetFlattenedTreeParent()
? ToString(*aContent.GetFlattenedTreeParent()).c_str()
: "nullptr",
aContent.GetFlattenedTreeParent()));
UpdateDeepestEnterEventTarget(aContent.GetFlattenedTreeParent());
}
void OverOutElementsWrapper::TryToRestorePendingRemovedOverTarget(
const WidgetEvent* aEvent) {
if (!MaybeHasPendingRemovingOverEventTarget()) {
return;
}
LogModule* const logModule = mType == BoundaryEventType::Mouse
? sMouseBoundaryLog
: sPointerBoundaryLog;
// If we receive a mouse event immediately, let's try to restore the last
// "over" event target as the following "out" event target. We assume that a
// synthesized mousemove or another mouse event is being dispatched at latest
// the next animation frame from the removal. However, synthesized mouse move
// which is enqueued by ContentRemoved() may not sent to this instance because
// the target is considered with the latest layout, so the document of this
// instance may be moved somewhere before the next animation frame.
// Therefore, we should not restore the last "over" target if we receive an
// unexpected event like a keyboard event, a wheel event, etc.
if (aEvent->AsMouseEvent()) {
// Restore the original "over" event target should be allowed only when it's
// reconnected under the last deepest "enter" event target because we need
// to dispatch "leave" events later at least on the ancestors which have
// never been removed from the tree.
// XXX If new ancestor is inserted between mDeepestEnterEventTarget and
// mPendingToRemoveLastOverEventTarget, we will dispatch "leave" event even
// though we have not dispatched "enter" event on the element. For fixing
// this, we need to store the full path of the last "out" event target when
// it's removed from the tree. I guess we can be relax for this issue
// because this hack is required for web apps which reconnect the target
// to the same position immediately.
// XXX Should be IsInclusiveFlatTreeDescendantOf()? However, it may
// be reconnected into a subtree which is different from where the
// last over element was.
nsCOMPtr<nsIContent> pendingRemovingOverEventTarget =
GetPendingRemovingOverEventTarget();
if (pendingRemovingOverEventTarget &&
pendingRemovingOverEventTarget->IsInclusiveDescendantOf(
mDeepestEnterEventTarget)) {
// StoreOverEventTargetAndDeepestEnterEventTarget() always resets
// mLastOverWidget. When we restore the pending removing "over" event
// target, we need to keep storing the original "over" widget too.
nsCOMPtr<nsIWeakReference> widget = std::move(mLastOverWidget);
StoreOverEventTargetAndDeepestEnterEventTarget(
pendingRemovingOverEventTarget);
mLastOverWidget = std::move(widget);
MOZ_LOG(logModule, LogLevel::Info,
("The \"over\" event target (%p) is restored",
mDeepestEnterEventTarget.get()));
return;
}
MOZ_LOG(logModule, LogLevel::Debug,
("Forgetting the last \"over\" event target (%p) because it is not "
"reconnected under the deepest enter event target (%p)",
mPendingRemovingOverEventTarget.get(),
mDeepestEnterEventTarget.get()));
} else {
MOZ_LOG(logModule, LogLevel::Debug,
("Forgetting the last \"over\" event target (%p) because an "
"unexpected event (%s) is being dispatched, that means that "
"EventStateManager didn't receive a synthesized mousemove which "
"should be dispatched at next animation frame from the removal",
mPendingRemovingOverEventTarget.get(), ToChar(aEvent->mMessage)));
}
// Now, we should not restore mPendingRemovingOverEventTarget to
// mDeepestEnterEventTarget anymore since mPendingRemovingOverEventTarget was
// moved outside the subtree of mDeepestEnterEventTarget.
mPendingRemovingOverEventTarget = nullptr;
}
void OverOutElementsWrapper::WillDispatchOverAndEnterEvent(
nsIContent* aOverEventTarget) {
StoreOverEventTargetAndDeepestEnterEventTarget(aOverEventTarget);
// Store the first "over" event target we fire and don't refire "over" event
// to that element while the first "over" event is still ongoing.
mDispatchingOverEventTarget = aOverEventTarget;
}
void OverOutElementsWrapper::DidDispatchOverAndEnterEvent(
nsIContent* aOriginalOverTargetInComposedDoc,
nsIWidget* aOverEventTargetWidget) {
mDispatchingOverEventTarget = nullptr;
mLastOverWidget = do_GetWeakReference(aOverEventTargetWidget);
// Pointer Events define that once the `pointerover` event target is removed
// from the tree, `pointerout` should not be fired on that and the closest
// connected ancestor at the target removal should be kept as the deepest
// `pointerleave` target. Therefore, we don't need the special handling for
// `pointerout` event target if the last `pointerover` target is temporarily
// removed from the tree.
if (mType == OverOutElementsWrapper::BoundaryEventType::Pointer) {
return;
}
// Assume that the caller checks whether aOriginalOverTarget is in the
// original document. If we don't enable the strict mouse/pointer event
// boundary event dispatching by the pref (see below),
// mDeepestEnterEventTarget is set to nullptr when the last "over" target is
// removed. Therefore, we cannot check whether aOriginalOverTarget is in the
// original document here.
if (!aOriginalOverTargetInComposedDoc) {
return;
}
MOZ_ASSERT_IF(mDeepestEnterEventTarget,
mDeepestEnterEventTarget->GetComposedDoc() ==
aOriginalOverTargetInComposedDoc->GetComposedDoc());
// If the "mouseover" event target is removed temporarily while we're
// dispatching "mouseover" and "mouseenter" events and the target gets back
// under the deepest enter event target, we should restore the "mouseover"
// target.
if ((!StaticPrefs::
dom_events_mouse_pointer_boundary_keep_enter_targets_after_over_target_removed() &&
!mDeepestEnterEventTarget) ||
(!LastOverEventTargetIsOutEventTarget() && mDeepestEnterEventTarget &&
nsContentUtils::ContentIsFlattenedTreeDescendantOf(
aOriginalOverTargetInComposedDoc, mDeepestEnterEventTarget))) {
StoreOverEventTargetAndDeepestEnterEventTarget(
aOriginalOverTargetInComposedDoc);
LogModule* const logModule = mType == BoundaryEventType::Mouse
? sMouseBoundaryLog
: sPointerBoundaryLog;
MOZ_LOG(logModule, LogLevel::Info,
("The \"over\" event target (%p) is restored",
mDeepestEnterEventTarget.get()));
}
}
void OverOutElementsWrapper::StoreOverEventTargetAndDeepestEnterEventTarget(
nsIContent* aOverEventTargetAndDeepestEnterEventTarget) {
mDeepestEnterEventTarget = aOverEventTargetAndDeepestEnterEventTarget;
mPendingRemovingOverEventTarget = nullptr;
mDeepestEnterEventTargetIsOverEventTarget = !!mDeepestEnterEventTarget;
mLastOverWidget = nullptr; // Set it after dispatching the "over" event.
}
void OverOutElementsWrapper::UpdateDeepestEnterEventTarget(
nsIContent* aDeepestEnterEventTarget) {
if (MOZ_UNLIKELY(mDeepestEnterEventTarget == aDeepestEnterEventTarget)) {
return;
}
if (!aDeepestEnterEventTarget) {
// If the root element is removed, we don't need to dispatch "leave"
// events on any elements. Therefore, we can forget everything.
StoreOverEventTargetAndDeepestEnterEventTarget(nullptr);
return;
}
if (LastOverEventTargetIsOutEventTarget()) {
MOZ_ASSERT(mDeepestEnterEventTarget);
if (mType == BoundaryEventType::Pointer) {
// The spec of Pointer Events defines that once the `pointerover` event
// target is removed from the tree, `pointerout` should not be fired on
// that and the closest connected ancestor at the target removal should be
// kept as the deepest `pointerleave` target. All browsers considers the
// last `pointerover` event target is removed immediately when it occurs.
// Therefore, we don't need the special handling which we do for the
// `mouseout` event target below for considering whether we'll dispatch
// `pointerout` on the last `pointerover` target.
mPendingRemovingOverEventTarget = nullptr;
} else {
// Now, the `mouseout` event target is removed from the DOM at least
// temporarily. Let's keep storing it for restoring it if it's
// reconnected into mDeepestEnterEventTarget in a tick because the other
// browsers do not treat temporary removal of the last `mouseover` target
// keeps storing it as the next `mouseout` event target.
MOZ_ASSERT(!mPendingRemovingOverEventTarget);
MOZ_ASSERT(mDeepestEnterEventTarget);
mPendingRemovingOverEventTarget =
do_GetWeakReference(mDeepestEnterEventTarget);
}
} else {
MOZ_ASSERT(!mDeepestEnterEventTargetIsOverEventTarget);
// If mDeepestEnterEventTarget is not the last "over" event target, we've
// already done the complicated state managing above. Therefore, we only
// need to update mDeepestEnterEventTarget in this case.
}
mDeepestEnterEventTarget = aDeepestEnterEventTarget;
mDeepestEnterEventTargetIsOverEventTarget = false;
// Do not update mLastOverWidget here because it's required to ignore some
// following pointer events which are fired on widget under different top
// level widget.
}
/******************************************************************/
/* mozilla::EventStateManager */
/******************************************************************/
static uint32_t sESMInstanceCount = 0;
bool EventStateManager::sNormalLMouseEventInProcess = false;
int16_t EventStateManager::sCurrentMouseBtn = MouseButton::eNotPressed;
EventStateManager* EventStateManager::sActiveESM = nullptr;
EventStateManager* EventStateManager::sCursorSettingManager = nullptr;
AutoWeakFrame EventStateManager::sLastDragOverFrame = nullptr;
LayoutDeviceIntPoint EventStateManager::sPreLockScreenPoint =
LayoutDeviceIntPoint(0, 0);
LayoutDeviceIntPoint EventStateManager::sLastRefPoint = kInvalidRefPoint;
CSSIntPoint EventStateManager::sLastScreenPoint = CSSIntPoint(0, 0);
LayoutDeviceIntPoint EventStateManager::sSynthCenteringPoint = kInvalidRefPoint;
CSSIntPoint EventStateManager::sLastClientPoint = CSSIntPoint(0, 0);
nsCOMPtr<nsIContent> EventStateManager::sDragOverContent = nullptr;
EventStateManager::WheelPrefs* EventStateManager::WheelPrefs::sInstance =
nullptr;
EventStateManager::DeltaAccumulator*
EventStateManager::DeltaAccumulator::sInstance = nullptr;
constexpr const StyleCursorKind kInvalidCursorKind =
static_cast<StyleCursorKind>(255);
EventStateManager::EventStateManager()
: mLockCursor(kInvalidCursorKind),
mCurrentTarget(nullptr),
// init d&d gesture state machine variables
mGestureDownPoint(0, 0),
mGestureModifiers(0),
mGestureDownButtons(0),
mGestureDownButton(0),
mPresContext(nullptr),
mShouldAlwaysUseLineDeltas(false),
mShouldAlwaysUseLineDeltasInitialized(false),
mGestureDownInTextControl(false),
mInTouchDrag(false),
m_haveShutdown(false) {
if (sESMInstanceCount == 0) {
gUserInteractionTimerCallback = new UITimerCallback();
if (gUserInteractionTimerCallback) NS_ADDREF(gUserInteractionTimerCallback);
UpdateUserActivityTimer();
}
++sESMInstanceCount;
}
nsresult EventStateManager::UpdateUserActivityTimer() {
if (!gUserInteractionTimerCallback) return NS_OK;
if (!gUserInteractionTimer) {
gUserInteractionTimer = NS_NewTimer().take();
}
if (gUserInteractionTimer) {
gUserInteractionTimer->InitWithCallback(
gUserInteractionTimerCallback,
StaticPrefs::dom_events_user_interaction_interval(),
nsITimer::TYPE_ONE_SHOT);
}
return NS_OK;
}
nsresult EventStateManager::Init() {
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (!observerService) return NS_ERROR_FAILURE;
observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
return NS_OK;
}
bool EventStateManager::ShouldAlwaysUseLineDeltas() {
if (MOZ_UNLIKELY(!mShouldAlwaysUseLineDeltasInitialized)) {
mShouldAlwaysUseLineDeltasInitialized = true;
mShouldAlwaysUseLineDeltas =
!StaticPrefs::dom_event_wheel_deltaMode_lines_disabled();
if (!mShouldAlwaysUseLineDeltas && mDocument) {
if (nsIPrincipal* principal =
mDocument->GetPrincipalForPrefBasedHacks()) {
mShouldAlwaysUseLineDeltas = principal->IsURIInPrefList(
"dom.event.wheel-deltaMode-lines.always-enabled");
}
}
}
return mShouldAlwaysUseLineDeltas;
}
EventStateManager::~EventStateManager() {
ReleaseCurrentIMEContentObserver();
if (sActiveESM == this) {
sActiveESM = nullptr;
}
if (StaticPrefs::ui_click_hold_context_menus()) {
KillClickHoldTimer();
}
if (sCursorSettingManager == this) {
sCursorSettingManager = nullptr;
}
--sESMInstanceCount;
if (sESMInstanceCount == 0) {
WheelTransaction::Shutdown();
if (gUserInteractionTimerCallback) {
gUserInteractionTimerCallback->Notify(nullptr);
NS_RELEASE(gUserInteractionTimerCallback);
}
if (gUserInteractionTimer) {
gUserInteractionTimer->Cancel();
NS_RELEASE(gUserInteractionTimer);
}
WheelPrefs::Shutdown();
DeltaAccumulator::Shutdown();
}
if (sDragOverContent && sDragOverContent->OwnerDoc() == mDocument) {
sDragOverContent = nullptr;
}
if (!m_haveShutdown) {
Shutdown();
// Don't remove from Observer service in Shutdown because Shutdown also
// gets called from xpcom shutdown observer. And we don't want to remove
// from the service in that case.
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService) {
observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
}
}
}
nsresult EventStateManager::Shutdown() {
m_haveShutdown = true;
return NS_OK;
}
NS_IMETHODIMP
EventStateManager::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* someData) {
if (!nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
Shutdown();
}
return NS_OK;
}
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EventStateManager)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsIObserver)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(EventStateManager)
NS_IMPL_CYCLE_COLLECTING_RELEASE(EventStateManager)
NS_IMPL_CYCLE_COLLECTION_WEAK(EventStateManager, mCurrentTargetContent,
mGestureDownContent, mGestureDownFrameOwner,
mLastLeftMouseDownInfo.mLastMouseDownContent,
mLastMiddleMouseDownInfo.mLastMouseDownContent,
mLastRightMouseDownInfo.mLastMouseDownContent,
mActiveContent, mHoverContent, mURLTargetContent,
mPopoverPointerDownTarget, mMouseEnterLeaveHelper,
mPointersEnterLeaveHelper, mDocument,
mIMEContentObserver, mAccessKeys)
void EventStateManager::ReleaseCurrentIMEContentObserver() {
if (mIMEContentObserver) {
mIMEContentObserver->DisconnectFromEventStateManager();
}
mIMEContentObserver = nullptr;
}
void EventStateManager::OnStartToObserveContent(
IMEContentObserver* aIMEContentObserver) {
if (mIMEContentObserver == aIMEContentObserver) {
return;
}
ReleaseCurrentIMEContentObserver();
mIMEContentObserver = aIMEContentObserver;
}
void EventStateManager::OnStopObservingContent(
IMEContentObserver* aIMEContentObserver) {
aIMEContentObserver->DisconnectFromEventStateManager();
NS_ENSURE_TRUE_VOID(mIMEContentObserver == aIMEContentObserver);
mIMEContentObserver = nullptr;
}
void EventStateManager::TryToFlushPendingNotificationsToIME() {
if (mIMEContentObserver) {
mIMEContentObserver->TryToFlushPendingNotifications(true);
}
}
static bool IsMessageMouseUserActivity(EventMessage aMessage) {
return aMessage == eMouseMove || aMessage == eMouseUp ||
aMessage == eMouseDown || aMessage == ePointerAuxClick ||
aMessage == eMouseDoubleClick || aMessage == ePointerClick ||
aMessage == eMouseActivate || aMessage == eMouseLongTap;
}
static bool IsMessageGamepadUserActivity(EventMessage aMessage) {
return aMessage == eGamepadButtonDown || aMessage == eGamepadButtonUp ||
aMessage == eGamepadAxisMove;
}
// static
bool EventStateManager::IsKeyboardEventUserActivity(WidgetEvent* aEvent) {
// We ignore things that shouldn't cause popups, but also things that look
// like shortcut presses. In some obscure cases these may actually be
// website input, but any meaningful website will have other input anyway,
// and we can't very well tell whether shortcut input was supposed to be
// directed at chrome or the document.
WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent();
// Access keys should be treated as page interaction.
if (keyEvent->ModifiersMatchWithAccessKey(AccessKeyType::eContent)) {
return true;
}
if (!keyEvent->CanTreatAsUserInput() || keyEvent->IsControl() ||
keyEvent->IsMeta() || keyEvent->IsAlt()) {
return false;
}
// Deal with function keys:
switch (keyEvent->mKeyNameIndex) {
case KEY_NAME_INDEX_F1:
case KEY_NAME_INDEX_F2:
case KEY_NAME_INDEX_F3:
case KEY_NAME_INDEX_F4:
case KEY_NAME_INDEX_F5:
case KEY_NAME_INDEX_F6:
case KEY_NAME_INDEX_F7:
case KEY_NAME_INDEX_F8:
case KEY_NAME_INDEX_F9:
case KEY_NAME_INDEX_F10:
case KEY_NAME_INDEX_F11:
case KEY_NAME_INDEX_F12:
case KEY_NAME_INDEX_F13:
case KEY_NAME_INDEX_F14:
case KEY_NAME_INDEX_F15:
case KEY_NAME_INDEX_F16:
case KEY_NAME_INDEX_F17:
case KEY_NAME_INDEX_F18:
case KEY_NAME_INDEX_F19:
case KEY_NAME_INDEX_F20:
case KEY_NAME_INDEX_F21:
case KEY_NAME_INDEX_F22:
case KEY_NAME_INDEX_F23:
case KEY_NAME_INDEX_F24:
return false;
default:
return true;
}
}
static void OnTypingInteractionEnded() {
// We don't consider a single keystroke to be typing.
if (gTypingInteractionKeyPresses > 1) {
gTypingInteraction.mInteractionCount += gTypingInteractionKeyPresses;
gTypingInteraction.mInteractionTimeInMilliseconds += static_cast<uint32_t>(
std::ceil((gTypingEndTime - gTypingStartTime).ToMilliseconds()));
}
gTypingInteractionKeyPresses = 0;
gTypingStartTime = TimeStamp();
gTypingEndTime = TimeStamp();
}
static void HandleKeyUpInteraction(WidgetKeyboardEvent* aKeyEvent) {
if (EventStateManager::IsKeyboardEventUserActivity(aKeyEvent)) {
TimeStamp now = TimeStamp::Now();
if (gTypingEndTime.IsNull()) {
gTypingEndTime = now;
}
TimeDuration delay = now - gTypingEndTime;
// Has it been too long since the last keystroke to be considered typing?
if (gTypingInteractionKeyPresses > 0 &&
delay >
TimeDuration::FromMilliseconds(
StaticPrefs::browser_places_interactions_typing_timeout_ms())) {
OnTypingInteractionEnded();
}
gTypingInteractionKeyPresses++;
if (gTypingStartTime.IsNull()) {
gTypingStartTime = now;
}
gTypingEndTime = now;
}
}
nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
WidgetEvent* aEvent,
nsIFrame* aTargetFrame,
nsIContent* aTargetContent,
nsEventStatus* aStatus,
nsIContent* aOverrideClickTarget) {
AUTO_PROFILER_LABEL("EventStateManager::PreHandleEvent", DOM);
NS_ENSURE_ARG_POINTER(aStatus);
NS_ENSURE_ARG(aPresContext);
if (!aEvent) {
NS_ERROR("aEvent is null. This should never happen.");
return NS_ERROR_NULL_POINTER;
}
NS_WARNING_ASSERTION(
!aTargetFrame || !aTargetFrame->GetContent() ||
aTargetFrame->GetContent() == aTargetContent ||
aTargetFrame->GetContent()->GetFlattenedTreeParent() ==
aTargetContent ||
aTargetFrame->IsGeneratedContentFrame(),
"aTargetFrame should be related with aTargetContent");
#if DEBUG
if (aTargetFrame && aTargetFrame->IsGeneratedContentFrame()) {
nsCOMPtr<nsIContent> targetContent;
aTargetFrame->GetContentForEvent(aEvent, getter_AddRefs(targetContent));
MOZ_ASSERT(aTargetContent == targetContent,
"Unexpected target for generated content frame!");
}
#endif
mCurrentTarget = aTargetFrame;
mCurrentTargetContent = nullptr;
// Do not take account eMouseEnterIntoWidget/ExitFromWidget so that loading
// a page when user is not active doesn't change the state to active.
WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
if (aEvent->IsTrusted() &&
((mouseEvent && mouseEvent->IsReal() &&
IsMessageMouseUserActivity(mouseEvent->mMessage)) ||
aEvent->mClass == eWheelEventClass ||
aEvent->mClass == ePointerEventClass ||
aEvent->mClass == eTouchEventClass ||
aEvent->mClass == eKeyboardEventClass ||
(aEvent->mClass == eDragEventClass && aEvent->mMessage == eDrop) ||
IsMessageGamepadUserActivity(aEvent->mMessage))) {
if (gMouseOrKeyboardEventCounter == 0) {
nsCOMPtr<nsIObserverService> obs =
mozilla::services::GetObserverService();
if (obs) {
obs->NotifyObservers(nullptr, "user-interaction-active", nullptr);
UpdateUserActivityTimer();
}
}
++gMouseOrKeyboardEventCounter;
nsCOMPtr<nsINode> node = aTargetContent;
if (node &&
((aEvent->mMessage == eKeyUp && IsKeyboardEventUserActivity(aEvent)) ||
aEvent->mMessage == eMouseUp || aEvent->mMessage == eWheel ||
aEvent->mMessage == eTouchEnd || aEvent->mMessage == ePointerUp ||
aEvent->mMessage == eDrop)) {
Document* doc = node->OwnerDoc();
while (doc) {
doc->SetUserHasInteracted();
doc = nsContentUtils::IsChildOfSameType(doc)
? doc->GetInProcessParentDocument()
: nullptr;
}
}
}
WheelTransaction::OnEvent(aEvent);
// Focus events don't necessarily need a frame.
if (!mCurrentTarget && !aTargetContent) {
NS_ERROR("mCurrentTarget and aTargetContent are null");
return NS_ERROR_NULL_POINTER;
}
#ifdef DEBUG
if (aEvent->HasDragEventMessage() && PointerLockManager::IsLocked()) {
NS_ASSERTION(PointerLockManager::IsLocked(),
"Pointer is locked. Drag events should be suppressed when "
"the pointer is locked.");
}
#endif
// Store last known screenPoint and clientPoint so pointer lock
// can use these values as constants.
if (aEvent->IsTrusted() &&
((mouseEvent && mouseEvent->IsReal()) ||
aEvent->mClass == eWheelEventClass) &&
!PointerLockManager::IsLocked()) {
// XXX Probably doesn't matter much, but storing these in CSS pixels instead
// of device pixels means behavior can be a bit odd if you zoom while
// pointer-locked.
sLastScreenPoint =
Event::GetScreenCoords(aPresContext, aEvent, aEvent->mRefPoint)
.extract();
sLastClientPoint = Event::GetClientCoords(
aPresContext, aEvent, aEvent->mRefPoint, CSSIntPoint(0, 0));
}
*aStatus = nsEventStatus_eIgnore;
if (aEvent->mClass == eQueryContentEventClass) {
HandleQueryContentEvent(aEvent->AsQueryContentEvent());
return NS_OK;
}
WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent();
if (touchEvent && mInTouchDrag) {
if (touchEvent->mMessage == eTouchMove) {
GenerateDragGesture(aPresContext, touchEvent);
} else {
mInTouchDrag = false;
StopTrackingDragGesture(true);
}
}
if (mMouseEnterLeaveHelper && aEvent->IsTrusted()) {
// When the last `mouseover` event target is removed from the document,
// we makes mMouseEnterLeaveHelper update the last deepest `mouseenter`
// event target to the removed node parent and mark it as not the following
// `mouseout` event target. However, the other browsers may dispatch
// `mouseout` on it if it's restored "immediately". Therefore, we use
// the next animation frame as the deadline. ContentRemoved() enqueues a
// synthesized `mousemove` to dispatch mouse boundary events under the
// mouse cursor soon and the synthesized event (or eMouseExitFromWidget if
// our window is moved) will reach here at latest the next animation frame.
// Therefore, we can use the event as the deadline. If the removed last
// `mouseover` target is reconnected before a synthesized mouse event or
// a real mouse event, let's restore it as the following `mouseout` event
// target. Otherwise, e.g., a keyboard event, let's forget it.
mMouseEnterLeaveHelper->TryToRestorePendingRemovedOverTarget(aEvent);
}
static constexpr auto const allowSynthesisForTests = []() -> bool {
nsCOMPtr<nsIDragService> dragService =
do_GetService("@mozilla.org/widget/dragservice;1");
return dragService &&
!dragService->GetNeverAllowSessionIsSynthesizedForTests();
};
switch (aEvent->mMessage) {
case eContextMenu:
if (PointerLockManager::IsLocked()) {
return NS_ERROR_DOM_INVALID_STATE_ERR;
}
break;
case eMouseTouchDrag:
mInTouchDrag = true;
BeginTrackingDragGesture(aPresContext, mouseEvent, aTargetFrame);
break;
case eMouseDown: {
switch (mouseEvent->mButton) {
case MouseButton::ePrimary:
BeginTrackingDragGesture(aPresContext, mouseEvent, aTargetFrame);
mLastLeftMouseDownInfo.mClickCount = mouseEvent->mClickCount;
SetClickCount(mouseEvent, aStatus);
sNormalLMouseEventInProcess = true;
break;
case MouseButton::eMiddle:
mLastMiddleMouseDownInfo.mClickCount = mouseEvent->mClickCount;
SetClickCount(mouseEvent, aStatus);
break;
case MouseButton::eSecondary:
mLastRightMouseDownInfo.mClickCount = mouseEvent->mClickCount;
SetClickCount(mouseEvent, aStatus);
break;
}
if (!StaticPrefs::dom_popup_experimental()) {
NotifyTargetUserActivation(aEvent, aTargetContent);
}
break;
}
case eMouseUp: {
switch (mouseEvent->mButton) {
case MouseButton::ePrimary:
if (StaticPrefs::ui_click_hold_context_menus()) {
KillClickHoldTimer();
}
mInTouchDrag = false;
StopTrackingDragGesture(true);
sNormalLMouseEventInProcess = false;
// then fall through...
[[fallthrough]];
case MouseButton::eSecondary:
case MouseButton::eMiddle:
RefPtr<EventStateManager> esm =
ESMFromContentOrThis(aOverrideClickTarget);
esm->SetClickCount(mouseEvent, aStatus, aOverrideClickTarget);
break;
}
break;
}
case eMouseEnterIntoWidget:
PointerEventHandler::UpdateActivePointerState(mouseEvent, aTargetContent);
// In some cases on e10s eMouseEnterIntoWidget
// event was sent twice into child process of content.
// (From specific widget code (sending is not permanent) and
// from ESM::DispatchMouseOrPointerBoundaryEvent (sending is permanent)).
// IsCrossProcessForwardingStopped() helps to suppress sending accidental
// event from widget code.
aEvent->StopCrossProcessForwarding();
break;
case eMouseExitFromWidget:
// If this is a remote frame, we receive eMouseExitFromWidget from the
// parent the mouse exits our content. Since the parent may update the
// cursor while the mouse is outside our frame, and since PuppetWidget
// caches the current cursor internally, re-entering our content (say from
// over a window edge) wont update the cursor if the cached value and the
// current cursor match. So when the mouse exits a remote frame, clear the
// cached widget cursor so a proper update will occur when the mouse
// re-enters.
if (XRE_IsContentProcess()) {
ClearCachedWidgetCursor(mCurrentTarget);
}
// IsCrossProcessForwardingStopped() helps to suppress double event
// sending into process of content. For more information see comment
// above, at eMouseEnterIntoWidget case.
aEvent->StopCrossProcessForwarding();
// If the event is not a top-level window or puppet widget exit, then it's
// not really an exit --- we may have traversed widget boundaries but
// we're still in our toplevel window or puppet widget.
if (mouseEvent->mExitFrom.value() !=
WidgetMouseEvent::ePlatformTopLevel &&
mouseEvent->mExitFrom.value() != WidgetMouseEvent::ePuppet) {
// Treat it as a synthetic move so we don't generate spurious
// "exit" or "move" events. Any necessary "out" or "over" events
// will be generated by GenerateMouseEnterExit
mouseEvent->mMessage = eMouseMove;
mouseEvent->mReason = WidgetMouseEvent::eSynthesized;
// then fall through...
} else {
MOZ_ASSERT_IF(XRE_IsParentProcess(),
mouseEvent->mExitFrom.value() ==
WidgetMouseEvent::ePlatformTopLevel);
MOZ_ASSERT_IF(XRE_IsContentProcess(), mouseEvent->mExitFrom.value() ==
WidgetMouseEvent::ePuppet);
// We should synthetize corresponding pointer events
GeneratePointerEnterExit(ePointerLeave, mouseEvent);
GenerateMouseEnterExit(mouseEvent);
// This is really an exit and should stop here
aEvent->mMessage = eVoidEvent;
break;
}
[[fallthrough]];
case eMouseMove:
case ePointerDown:
if (aEvent->mMessage == ePointerDown) {
PointerEventHandler::UpdateActivePointerState(mouseEvent,
aTargetContent);
PointerEventHandler::ImplicitlyCapturePointer(aTargetFrame, aEvent);
if (StaticPrefs::dom_popup_experimental()) {
if (mouseEvent->mInputSource ==
MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
NotifyTargetUserActivation(aEvent, aTargetContent);
}
} else if (mouseEvent->mInputSource !=
MouseEvent_Binding::MOZ_SOURCE_TOUCH) {
NotifyTargetUserActivation(aEvent, aTargetContent);
}
LightDismissOpenPopovers(aEvent, aTargetContent);
}
[[fallthrough]];
case ePointerMove: {
if (!mInTouchDrag &&
PointerEventHandler::IsDragAndDropEnabled(*mouseEvent)) {
GenerateDragGesture(aPresContext, mouseEvent);
}
// on the Mac, GenerateDragGesture() may not return until the drag
// has completed and so |aTargetFrame| may have been deleted (moving
// a bookmark, for example). If this is the case, however, we know
// that ClearFrameRefs() has been called and it cleared out
// |mCurrentTarget|. As a result, we should pass |mCurrentTarget|
// into UpdateCursor().
UpdateCursor(aPresContext, mouseEvent, mCurrentTarget, aStatus);