/* 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 "EventStateManager.h" #include "ContentEventHandler.h" #include "IMEContentObserver.h" #include "RemoteDragStartData.h" #include "Units.h" #include "WheelHandlingHelper.h" #include "imgIContainer.h" #include "mozilla/AppShutdown.h" #include "mozilla/AsyncEventDispatcher.h" #include "mozilla/Attributes.h" #include "mozilla/ConnectedAncestorTracker.h" #include "mozilla/EditorBase.h" #include "mozilla/EventDispatcher.h" #include "mozilla/EventForwards.h" #include "mozilla/FocusModel.h" #include "mozilla/HTMLEditor.h" #include "mozilla/Hal.h" #include "mozilla/IMEStateManager.h" #include "mozilla/Likely.h" #include "mozilla/Logging.h" #include "mozilla/LookAndFeel.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/MiscEvents.h" #include "mozilla/MouseEvents.h" #include "mozilla/PointerLockManager.h" #include "mozilla/Preferences.h" #include "mozilla/PresShell.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/ScopeExit.h" #include "mozilla/ScrollContainerFrame.h" #include "mozilla/ScrollTypes.h" #include "mozilla/Services.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 "mozilla/TextComposition.h" #include "mozilla/TextControlElement.h" #include "mozilla/TextEditor.h" #include "mozilla/TextEvents.h" #include "mozilla/TouchEvents.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/BrowserBridgeChild.h" #include "mozilla/dom/BrowserChild.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/CanonicalBrowsingContext.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/DOMIntersectionObserver.h" #include "mozilla/dom/DataTransfer.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DragEvent.h" #include "mozilla/dom/EditContext.h" #include "mozilla/dom/ElementInlines.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/FrameLoaderBinding.h" #include "mozilla/dom/HTMLDialogElement.h" #include "mozilla/dom/HTMLInputElement.h" #include "mozilla/dom/HTMLLabelElement.h" #include "mozilla/dom/MouseEventBinding.h" #include "mozilla/dom/PerformanceMainThread.h" #include "mozilla/dom/PointerEventHandler.h" #include "mozilla/dom/PopoverData.h" #include "mozilla/dom/Record.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/UIEvent.h" #include "mozilla/dom/UIEventBinding.h" #include "mozilla/dom/UserActivation.h" #include "mozilla/dom/WheelEventBinding.h" #include "mozilla/glean/ProcesstoolsMetrics.h" #include "nsCOMPtr.h" #include "nsComboboxControlFrame.h" #include "nsCommandParams.h" #include "nsContentAreaDragDrop.h" #include "nsContentUtils.h" #include "nsCopySupport.h" #include "nsFocusManager.h" #include "nsFontMetrics.h" #include "nsFrameLoaderOwner.h" #include "nsFrameManager.h" #include "nsFrameSelection.h" #include "nsGenericHTMLElement.h" #include "nsGkAtoms.h" #include "nsIBaseWindow.h" #include "nsIBrowserChild.h" #include "nsIClipboard.h" #include "nsIContent.h" #include "nsIContentInlines.h" #include "nsIController.h" #include "nsICookieJarSettings.h" #include "nsIDOMXULControlElement.h" #include "nsIDocShell.h" #include "nsIDocumentViewer.h" #include "nsIDragService.h" #include "nsIDragSession.h" #include "nsIFormControl.h" #include "nsIFrame.h" #include "nsIInterfaceRequestorUtils.h" #include "nsIObserverService.h" #include "nsIProperties.h" #include "nsISupportsPrimitives.h" #include "nsITimer.h" #include "nsIWeakReferenceUtils.h" #include "nsIWebNavigation.h" #include "nsIWidget.h" #include "nsLayoutUtils.h" #include "nsLiteralString.h" #include "nsMenuPopupFrame.h" #include "nsNameSpaceManager.h" #include "nsPIDOMWindow.h" #include "nsPIWindowRoot.h" #include "nsPresContext.h" #include "nsServiceManagerUtils.h" #include "nsSubDocumentFrame.h" #include "nsTArray.h" #include "nsTreeBodyFrame.h" #include "nsUnicharUtils.h" #ifdef XP_MACOSX # import #endif namespace mozilla { using namespace dom; // Log the mouse cursor updates. That should be updated only by the events for // the last pointer which is actually handled as a user input. I.e., should not // be updated by synthesized mouse/pointer move events which are not for the // last pointer. // - MouseCursorUpdate:3 logs only when EventStateManager and BrowserParent // updated the cursor. // - MouseCursorUpdate:4 logs any results when BrowserParent handles that. // - MouseCursorUpdate:5 logs when UpdateCursor() stopped updating the cursor. // // NOTE: This can work only on debug builds for avoiding to the damage to the // performance. LazyLogModule gMouseCursorUpdates("MouseCursorUpdates"); static const LayoutDeviceIntPoint kInvalidRefPoint = LayoutDeviceIntPoint(-1, -1); static uint32_t gMouseOrKeyboardEventCounter = 0; // Like gMouseOrKeyboardEventCounter, but excludes synthesized mouse/pointer // events (e.g. synthesized pointer moves dispatched when content shifts under a // stationary cursor). Drives the "non-synthesized" active-tick notifications so // telemetry can record a corrected active tick alongside the legacy one. static uint32_t gNonSynthesizedMouseOrKeyboardEventCounter = 0; static nsITimer* gUserInteractionTimer = nullptr; static nsITimerCallback* gUserInteractionTimerCallback = nullptr; static const double kCursorLoadingTimeout = 1000; // ms constinit static AutoWeakFrame gLastCursorSourceFrame; static TimeStamp gLastCursorUpdateTime; static TimeStamp gTypingStartTime; static TimeStamp gTypingEndTime; static int32_t gTypingInteractionKeyPresses = 0; constinit static dom::InteractionData gTypingInteraction = {}; static inline int32_t RoundDown(double aDouble) { return (aDouble > 0) ? static_cast(floor(aDouble)) : static_cast(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 CreateMouseOrPointerWidgetEvent( const 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, const Maybe& aLastMouseDownInputControlType) { if (!aCurrentMouseUpTarget || !aLastMouseDownTarget) { return nullptr; } if (aCurrentMouseUpTarget == aLastMouseDownTarget) { return aCurrentMouseUpTarget; } // Build the chain of parents AutoTArray parents1; do { parents1.AppendElement(aCurrentMouseUpTarget); aCurrentMouseUpTarget = aCurrentMouseUpTarget->GetFlattenedTreeParentNode(); } while (aCurrentMouseUpTarget); AutoTArray 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; } static bool HasNativeKeyBindings(nsIContent* aContent, WidgetKeyboardEvent* aEvent) { MOZ_ASSERT(aEvent->mMessage == eKeyPress); if (!aContent) { return false; } const RefPtr targetElement = aContent->AsElement(); if (!targetElement) { return false; } const auto type = [&]() -> Maybe { if (BrowserParent::GetFrom(targetElement)) { const nsCOMPtr widget = aEvent->mWidget; if (MOZ_UNLIKELY(!widget)) { return Nothing(); } widget::InputContext context = widget->GetInputContext(); return context.mIMEState.IsEditable() ? Some(context.GetNativeKeyBindingsType()) : Nothing(); } const auto* const textControlElement = TextControlElement::FromNode(targetElement); if (textControlElement && textControlElement->IsSingleLineTextControlOrTextArea() && !textControlElement->IsInDesignMode()) { return textControlElement->IsTextArea() ? Some(NativeKeyBindingsType::MultiLineEditor) : Some(NativeKeyBindingsType::SingleLineEditor); } return targetElement->IsEditable() ? Some(NativeKeyBindingsType::RichTextEditor) : Nothing(); }(); if (type.isNothing()) { return false; } const nsTArray& commands = aEvent->EditCommandsConstRef(type.value()); return !commands.IsEmpty(); } LazyLogModule sMouseBoundaryLog("MouseBoundaryEvents"); LazyLogModule sPointerBoundaryLog("PointerBoundaryEvents"); /******************************************************************/ /* mozilla::UITimerCallback */ /******************************************************************/ class UITimerCallback final : public nsITimerCallback, public nsINamed { public: UITimerCallback() : mPreviousCount(0), mPreviousNonSynthesizedCount(0) {} NS_DECL_ISUPPORTS NS_DECL_NSITIMERCALLBACK NS_DECL_NSINAMED private: ~UITimerCallback() = default; uint32_t mPreviousCount; uint32_t mPreviousNonSynthesizedCount; }; 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 obs = mozilla::services::GetObserverService(); // ObserverService shutdown happens after XPCOMShutdownThreads. if (!obs || AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { return NS_ERROR_FAILURE; } if ((gMouseOrKeyboardEventCounter == mPreviousCount) || !aTimer) { gMouseOrKeyboardEventCounter = 0; gNonSynthesizedMouseOrKeyboardEventCounter = 0; obs->NotifyObservers(nullptr, "user-interaction-inactive", nullptr); obs->NotifyObservers(nullptr, "user-interaction-inactive-non-synthesized", nullptr); if (gUserInteractionTimer) { gUserInteractionTimer->Cancel(); NS_RELEASE(gUserInteractionTimer); } } else { obs->NotifyObservers(nullptr, "user-interaction-active", nullptr); // The corrected active tick only stays active while non-synthesized events // keep arriving, even if synthesized events alone kept the legacy tick // active during this interval. if (gNonSynthesizedMouseOrKeyboardEventCounter == mPreviousNonSynthesizedCount) { obs->NotifyObservers(nullptr, "user-interaction-inactive-non-synthesized", nullptr); } else { obs->NotifyObservers(nullptr, "user-interaction-active-non-synthesized", 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; mPreviousNonSynthesizedCount = gNonSynthesizedMouseOrKeyboardEventCounter; 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 OverOutElementsWrapper::GetLastOverWidget() const { nsCOMPtr 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 (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 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 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 (!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 if ( !StaticPrefs:: dom_event_mouse_boundary_restore_last_over_target_from_temporary_removal()) { // The spec of UI Events do not define that browsers should keep storing // the last `mouseover` target when it's removed temporarily and // reconnected immediately. We've decided to follow Chrome's behavior for // now. However, there is a pref to bring back the old behavior if // needed. mPendingRemovingOverEventTarget = nullptr; } else { // However, Safari and old Chrome restore the last `mouseover` target when // it's temporarily removed and reconnected immediately. Therefore, we // should follow them by default. However, we should keep the old // behavior for making it easier to backout the new behavior with // disabling the pref. 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; constinit AutoWeakFrame EventStateManager::sLastDragOverFrame{}; LayoutDeviceIntPoint EventStateManager::sPreLockScreenPoint = kInvalidRefPoint; LayoutDeviceIntPoint EventStateManager::sLastRefPoint = kInvalidRefPoint; LayoutDeviceIntPoint EventStateManager::sLastRefPointOfRawUpdate = kInvalidRefPoint; CSSIntPoint EventStateManager::sLastScreenPoint = CSSIntPoint(0, 0); LayoutDeviceIntPoint EventStateManager::sSynthCenteringPoint = kInvalidRefPoint; CSSIntPoint EventStateManager::sLastClientPoint = CSSIntPoint(0, 0); constinit nsCOMPtr EventStateManager::sDragOverContent; EventStateManager::WheelPrefs* EventStateManager::WheelPrefs::sInstance = nullptr; EventStateManager::DeltaAccumulator* EventStateManager::DeltaAccumulator::sInstance = nullptr; constexpr const StyleCursorKind kInvalidCursorKind = static_cast(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), mInTouchDrag(false), m_haveShutdown(false) { if (sESMInstanceCount == 0) { gUserInteractionTimerCallback = new UITimerCallback(); if (gUserInteractionTimerCallback) NS_ADDREF(gUserInteractionTimerCallback); UpdateUserActivityTimer(); } ++sESMInstanceCount; } // static LazyLogModule& EventStateManager::MouseCursorUpdateLogRef() { return gMouseCursorUpdates; } 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; } void EventStateManager::Init() { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); } } 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 observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); } } } void EventStateManager::Shutdown() { m_haveShutdown = true; } 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, mLastPrimaryButtonPressInfo.mConnectedDownContent, mLastPrimaryButtonPressInfo.mDownContent, mLastPrimaryButtonPressInfo.mUpContent, mLastMiddleButtonPressInfo.mConnectedDownContent, mLastMiddleButtonPressInfo.mDownContent, mLastMiddleButtonPressInfo.mUpContent, mLastSecondaryButtonPressInfo.mConnectedDownContent, mLastSecondaryButtonPressInfo.mDownContent, mLastSecondaryButtonPressInfo.mUpContent, 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( 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; } } static bool NeedsActiveContentChange(const WidgetMouseEvent* aMouseEvent) { // If the mouse event is a synthesized mouse event due to a touch, do // not set/clear the activation state. Element activation is handled by APZ. return !aMouseEvent || aMouseEvent->mInputSource != MouseEvent_Binding::MOZ_SOURCE_TOUCH; } 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()) { MOZ_ASSERT( aTargetContent == aTargetFrame->GetExplicitEventTargetContent(aEvent), "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 obs = mozilla::services::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "user-interaction-active", nullptr); UpdateUserActivityTimer(); } } ++gMouseOrKeyboardEventCounter; // Synthesized mouse/pointer events (e.g. a synthesized pointer move when // content moves under a stationary cursor) are not real user activity, so // they are excluded from the corrected active tick. if (!mouseEvent || mouseEvent->IsReal()) { if (gNonSynthesizedMouseOrKeyboardEventCounter == 0) { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->NotifyObservers( nullptr, "user-interaction-active-non-synthesized", nullptr); } } ++gNonSynthesizedMouseOrKeyboardEventCounter; } nsCOMPtr 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 = RoundedToInt( Event::GetScreenCoords(aPresContext, aEvent, aEvent->mRefPoint) .extract()); sLastClientPoint = RoundedToInt(Event::GetClientCoords( aPresContext, aEvent, aEvent->mRefPoint, CSSDoublePoint{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 { MOZ_ASSERT(touchEvent->mMessage != eTouchRawUpdate); 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 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); mLastPrimaryButtonPressInfo.mClickCount = mouseEvent->mClickCount; PrepareForFollowingClickEvent(*mouseEvent); sNormalLMouseEventInProcess = true; break; case MouseButton::eMiddle: mLastMiddleButtonPressInfo.mClickCount = mouseEvent->mClickCount; PrepareForFollowingClickEvent(*mouseEvent); break; case MouseButton::eSecondary: mLastSecondaryButtonPressInfo.mClickCount = mouseEvent->mClickCount; PrepareForFollowingClickEvent(*mouseEvent); break; case MouseButton::eX1: case MouseButton::eX2: // XXX FIXME: We won't dispatch `auxclick` for 4th nor 5th button. break; default: break; } 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 esm = ESMFromContentOrThis(aOverrideClickTarget); esm->PrepareForFollowingClickEvent(*mouseEvent, aOverrideClickTarget); break; } case MouseButton::eX1: case MouseButton::eX2: // XXX FIXME: We won't dispatch `auxclick` for 4th nor 5th button. break; default: break; } break; } case eMouseEnterIntoWidget: PointerEventHandler::UpdatePointerActiveState(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; // We need to generate pointer boundary events here because there is no // preceding pointer event dispatched for the eMouseExitFromWidget // event. GeneratePointerEnterExit(ePointerMove, mouseEvent); // 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); // Remove the pointer from the active pointerId table. PointerEventHandler::UpdatePointerActiveState(mouseEvent); // This is really an exit and should stop here aEvent->mMessage = eVoidEvent; break; } [[fallthrough]]; case ePointerDown: if (aEvent->mMessage == ePointerDown) { PointerEventHandler::UpdatePointerActiveState(mouseEvent, aTargetContent); PointerEventHandler::ImplicitlyCapturePointer(aTargetFrame, *aEvent); // https://html.spec.whatwg.org/multipage/interaction.html#activation-triggering-input-event if (mouseEvent->mInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) { NotifyTargetUserActivation(aEvent, aTargetContent); } LightDismissOpenPopovers(aEvent, aTargetContent); LightDismissOpenDialogs(aEvent, aTargetContent); } [[fallthrough]]; case eMouseMove: case ePointerMove: case ePointerRawUpdate: { if (aEvent->mMessage == ePointerMove) { PointerEventHandler::UpdatePointerActiveState(mouseEvent, aTargetContent); } 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); UpdateLastRefPointOfMouseEvent(mouseEvent); ResetPointerToWindowCenterWhilePointerLocked(mouseEvent); UpdateLastPointerPosition(mouseEvent); GenerateMouseEnterExit(mouseEvent); // Flush pending layout changes, so that later mouse move events // will go to the right nodes. FlushLayout(aPresContext); if (aEvent->mMessage == ePointerDown && NeedsActiveContentChange(mouseEvent)) { nsCOMPtr activeContent = mCurrentTarget ? mCurrentTarget->GetContent() : nullptr; if (activeContent && !activeContent->IsElement()) { if (nsIContent* parent = activeContent->GetFlattenedTreeParent()) { activeContent = parent; } } SetActiveManager(this, activeContent); } break; } case ePointerUp: LightDismissOpenPopovers(aEvent, aTargetContent); LightDismissOpenDialogs(aEvent, aTargetContent); GenerateMouseEnterExit(mouseEvent); if (mouseEvent->mInputSource != MouseEvent_Binding::MOZ_SOURCE_MOUSE) { NotifyTargetUserActivation(aEvent, aTargetContent); } if (NeedsActiveContentChange(mouseEvent)) { ClearGlobalActiveContent(this); } break; case ePointerGotCapture: GenerateMouseEnterExit(mouseEvent); break; case eDragStart: if (StaticPrefs::ui_click_hold_context_menus()) { // an external drag gesture event came in, not generated internally // by Gecko. Make sure we get rid of the click-hold timer. KillClickHoldTimer(); } break; case eDragOver: { WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); MOZ_ASSERT(dragEvent); if (dragEvent->mFlags.mIsSynthesizedForTests && allowSynthesisForTests()) { dragEvent->InitDropEffectForTests(); } // Send the enter/exit events before eDrop. GenerateDragDropEnterExit(aPresContext, *dragEvent); break; } case eDrop: { if (aEvent->mFlags.mIsSynthesizedForTests && allowSynthesisForTests()) { MOZ_ASSERT(aEvent->AsDragEvent()); aEvent->AsDragEvent()->InitDropEffectForTests(); } break; } case eKeyPress: { WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); if ((keyEvent->ModifiersMatchWithAccessKey(AccessKeyType::eChrome) || keyEvent->ModifiersMatchWithAccessKey(AccessKeyType::eContent)) && // If the key binding of this event is a native key binding, we // prioritize it. !HasNativeKeyBindings(aTargetContent, keyEvent)) { // If the eKeyPress event will be sent to a remote process, this // process needs to wait reply from the remote process for checking if // preceding eKeyDown event is consumed. If preceding eKeyDown event // is consumed in the remote process, BrowserChild won't send the event // back to this process. So, only when this process receives a reply // eKeyPress event in BrowserParent, we should handle accesskey in this // process. if (IsTopLevelRemoteTarget(GetFocusedElement())) { // However, if there is no accesskey target for the key combination, // we don't need to wait reply from the remote process. Otherwise, // Mark the event as waiting reply from remote process and stop // propagation in this process. if (CheckIfEventMatchesAccessKey(keyEvent, aPresContext)) { keyEvent->StopPropagation(); keyEvent->MarkAsWaitingReplyFromRemoteProcess(); } } // If the event target is in this process, we can handle accesskey now // since if preceding eKeyDown event was consumed, eKeyPress event // won't be dispatched by widget. So, coming eKeyPress event means // that the preceding eKeyDown event wasn't consumed in this case. else { AutoTArray accessCharCodes; keyEvent->GetAccessKeyCandidates(accessCharCodes); if (HandleAccessKey(keyEvent, aPresContext, accessCharCodes)) { *aStatus = nsEventStatus_eConsumeNoDefault; } } } } // then fall through... [[fallthrough]]; case eKeyDown: if (aEvent->mMessage == eKeyDown) { NotifyTargetUserActivation(aEvent, aTargetContent); } [[fallthrough]]; case eKeyUp: { Element* element = GetFocusedElement(); if (element) { mCurrentTargetContent = element; } // NOTE: Don't refer TextComposition::IsComposing() since UI Events // defines that KeyboardEvent.isComposing is true when it's // dispatched after compositionstart and compositionend. // TextComposition::IsComposing() is false even before // compositionend if there is no composing string. // And also don't expose other document's composition state. // A native IME context is typically shared by multiple documents. // So, don't use GetTextCompositionFor(nsIWidget*) here. RefPtr composition = IMEStateManager::GetTextCompositionFor(aPresContext); aEvent->AsKeyboardEvent()->mIsComposing = !!composition; // Widget may need to perform default action for specific keyboard // event if it's not consumed. In this case, widget has already marked // the event as "waiting reply from remote process". However, we need // to reset it if the target (focused content) isn't in a remote process // because PresShell needs to check if it's marked as so before // dispatching events into the DOM tree. if (aEvent->IsWaitingReplyFromRemoteProcess() && !aEvent->PropagationStopped() && !IsTopLevelRemoteTarget(element)) { aEvent->ResetWaitingReplyFromRemoteProcessState(); } } break; case eWheel: case eWheelOperationStart: case eWheelOperationEnd: { NS_ASSERTION(aEvent->IsTrusted(), "Untrusted wheel event shouldn't be here"); using DeltaModeCheckingState = WidgetWheelEvent::DeltaModeCheckingState; if (Element* element = GetFocusedElement()) { mCurrentTargetContent = element; } if (aEvent->mMessage != eWheel) { break; } WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent(); WheelPrefs::GetInstance()->ApplyUserPrefsToDelta(wheelEvent); // If we won't dispatch a DOM event for this event, nothing to do anymore. if (!wheelEvent->IsAllowedToDispatchDOMEvent()) { break; } if (StaticPrefs::dom_event_wheel_deltaMode_lines_always_disabled()) { wheelEvent->mDeltaModeCheckingState = DeltaModeCheckingState::Unchecked; } else if (ShouldAlwaysUseLineDeltas()) { wheelEvent->mDeltaModeCheckingState = DeltaModeCheckingState::Checked; } else { wheelEvent->mDeltaModeCheckingState = DeltaModeCheckingState::Unknown; } // Init lineOrPageDelta values for line scroll events for some devices // on some platforms which might dispatch wheel events which don't // have lineOrPageDelta values. And also, if delta values are // customized by prefs, this recomputes them. DeltaAccumulator::GetInstance()->InitLineOrPageDelta(aTargetFrame, this, wheelEvent); } break; case eSetSelection: { RefPtr focuedElement = GetFocusedElement(); IMEStateManager::HandleSelectionEvent(aPresContext, focuedElement, aEvent->AsSelectionEvent()); break; } case eContentCommandCut: case eContentCommandCopy: case eContentCommandPaste: case eContentCommandDelete: case eContentCommandUndo: case eContentCommandRedo: case eContentCommandPasteTransferable: case eContentCommandLookUpDictionary: DoContentCommandEvent(aEvent->AsContentCommandEvent()); break; case eContentCommandInsertText: DoContentCommandInsertTextEvent(aEvent->AsContentCommandEvent()); break; case eContentCommandReplaceText: DoContentCommandReplaceTextEvent(aEvent->AsContentCommandEvent()); break; case eContentCommandScroll: DoContentCommandScrollEvent(aEvent->AsContentCommandEvent()); break; case eCompositionStart: if (aEvent->IsTrusted()) { // If the event is trusted event, set the selected text to data of // composition event. WidgetCompositionEvent* compositionEvent = aEvent->AsCompositionEvent(); WidgetQueryContentEvent querySelectedTextEvent( true, eQuerySelectedText, compositionEvent->mWidget); HandleQueryContentEvent(&querySelectedTextEvent); if (querySelectedTextEvent.FoundSelection()) { compositionEvent->mData = querySelectedTextEvent.mReply->DataRef(); } NS_ASSERTION(querySelectedTextEvent.Succeeded(), "Failed to get selected text"); } break; case eTouchStart: SetGestureDownPoint(*aEvent->AsTouchEvent()); break; default: break; } return NS_OK; } // Returns true if this event is likely an user activation for a link or // a link-like button, where modifier keys are likely be used for controlling // where the link is opened. // // The modifiers associated with the user activation is used for controlling // where the `window.open` is opened into. static bool CanReflectModifiersToUserActivation(WidgetInputEvent* aEvent) { MOZ_ASSERT(aEvent->mMessage == eKeyDown || aEvent->mMessage == ePointerDown || aEvent->mMessage == ePointerUp); WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); if (keyEvent) { return keyEvent->CanReflectModifiersToUserActivation(); } return true; } void EventStateManager::NotifyTargetUserActivation(WidgetEvent* aEvent, nsIContent* aTargetContent) { if (!aEvent->IsTrusted()) { return; } WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); if (mouseEvent && !mouseEvent->IsReal()) { return; } nsCOMPtr node = aTargetContent; if (!node) { return; } Document* doc = node->OwnerDoc(); if (!doc) { return; } // Don't gesture activate for key events for keys which are likely // to be interaction with the browser, OS. WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); if (keyEvent && !keyEvent->CanUserGestureActivateTarget()) { return; } // Do not treat the click on scrollbar as a user interaction with the web // content. if (StaticPrefs::dom_user_activation_ignore_scrollbars() && (aEvent->mMessage == ePointerDown || aEvent->mMessage == ePointerUp) && aTargetContent->IsInNativeAnonymousSubtree()) { nsIContent* current = aTargetContent; do { nsIContent* root = current->GetClosestNativeAnonymousSubtreeRoot(); if (!root) { break; } if (root->IsXULElement(nsGkAtoms::scrollbar)) { return; } current = root->GetParent(); } while (current); } MOZ_ASSERT(aEvent->mMessage == eKeyDown || aEvent->mMessage == ePointerDown || aEvent->mMessage == ePointerUp); UserActivation::Modifiers modifiers; if (WidgetInputEvent* inputEvent = aEvent->AsInputEvent()) { if (CanReflectModifiersToUserActivation(inputEvent)) { if (inputEvent->IsShift()) { modifiers.SetShift(); } if (inputEvent->IsMeta()) { modifiers.SetMeta(); } if (inputEvent->IsControl()) { modifiers.SetControl(); } if (inputEvent->IsAlt()) { modifiers.SetAlt(); } WidgetMouseEvent* mouseEvent = inputEvent->AsMouseEvent(); if (mouseEvent) { if (mouseEvent->mButton == MouseButton::eMiddle) { modifiers.SetMiddleMouse(); } } } } doc->NotifyUserGestureActivation(modifiers); } // https://html.spec.whatwg.org/multipage/popover.html#popover-light-dismiss void EventStateManager::LightDismissOpenPopovers(WidgetEvent* aEvent, nsIContent* aTargetContent) { MOZ_ASSERT(aEvent->mMessage == ePointerDown || aEvent->mMessage == ePointerUp, "Light dismiss must be called for pointer up/down only"); // 1. Assert: event's isTrusted attribute is true. if (!aEvent->IsTrusted() || !aTargetContent) { return; } // 2. Let target be event's target. // 3. Let document be target's node document. RefPtr targetDoc(aTargetContent->OwnerDoc()); // 4. If the result of running topmost auto or hint popover given document is // null, then return. RefPtr topmostPopover = targetDoc->GetTopmostPopoverOf(PopoverAttributeState::Hint); if (!topmostPopover) { topmostPopover = targetDoc->GetTopmostPopoverOf(PopoverAttributeState::Auto); } if (!topmostPopover) { return; } // 5. If event's type is "pointerdown": set document's popover pointerdown // target to the result of running topmost clicked popover given target. if (aEvent->mMessage == ePointerDown) { mPopoverPointerDownTarget = aTargetContent->GetTopmostClickedPopover(); return; } // 6. If event's type is "pointerup": // 6.1. Let ancestor be the result of running topmost clicked popover given // target. RefPtr ancestor = aTargetContent->GetTopmostClickedPopover(); // 6.2. Let sameTarget be true if ancestor is document's popover pointerdown // target. bool sameTarget = mPopoverPointerDownTarget == static_cast(ancestor.get()); // 6.3. Set document's popover pointerdown target to null. mPopoverPointerDownTarget = nullptr; // 6.4. If sameTarget is false, then return. if (!sameTarget) { return; } // 6.5. Run hide popovers until given document, ancestor, false, and true. targetDoc->HidePopoversUntil(ancestor, false, true); } // https://html.spec.whatwg.org/multipage/interactive-elements.html#run-light-dismiss-activities // https://html.spec.whatwg.org/multipage/interactive-elements.html#light-dismiss-open-dialogs void EventStateManager::LightDismissOpenDialogs(WidgetEvent* aEvent, nsIContent* aTargetContent) { // 1. Assert: event's isTrusted attribute is true. // 2. Let document be event's target's node document. // (Skipped - not applicable) if (!StaticPrefs::dom_dialog_light_dismiss_enabled()) { return; } MOZ_ASSERT(aEvent->mMessage == ePointerDown || aEvent->mMessage == ePointerUp, "Light dismiss must be called for pointer up/down only"); if (aEvent->mFlags.mDefaultPrevented || !aEvent->IsTrusted() || !aTargetContent) { return; } auto* doc = aTargetContent->OwnerDoc(); // 3. If document's open dialogs list is empty, then return. if (!doc->HasOpenDialogs()) { return; } // 4. Let ancestor be the result of running nearest clicked dialog given // event. RefPtr ancestor = aTargetContent->NearestClickedDialog(aEvent); // 5. If event's type is "pointerdown", then set document's dialog pointerdown // target to ancestor. if (aEvent->mMessage == ePointerDown) { // XXX: "document's dialog pointerdown target" can be null, but // `SetLastDialogPointerdownTarget` takes `&` to avoid incidental nullptrs, // meaning we need to nullcheck `ancestor` & call // `ClearLastDialogPointerdownTarget` instead. if (!ancestor) { doc->ClearLastDialogPointerdownTarget(); } else { doc->SetLastDialogPointerdownTarget(*ancestor); } return; } MOZ_ASSERT(aEvent->mMessage == ePointerUp); // 6.1 Let sameTarget be true if ancestor is document's dialog pointerdown // target. RefPtr lastDialog = doc->GetLastDialogPointerdownTarget(); bool sameTarget = ancestor == lastDialog; // 6.2 Set document's dialog pointerdown target to null. doc->ClearLastDialogPointerdownTarget(); // 6.3 If sameTarget is false, then return. if (!sameTarget) { return; } // 6.4 Let topmostDialog be the last element of document's open dialogs list. RefPtr topmostDialog = doc->GetTopMostOpenDialog(); // 6.5 If ancestor is topmostDialog, then return. if (ancestor == topmostDialog) { return; } // 6.6 If topmostDialog's computed closed-by state is not Any, then return. if (!topmostDialog || topmostDialog->GetClosedBy() != HTMLDialogElement::ClosedBy::Any) { return; } // 7. Assert: topmostDialog's close watcher is not null. // 8. Request to close topmostDialog's close watcher with false. const mozilla::dom::Optional returnValue; topmostDialog->RequestClose(returnValue); } already_AddRefed EventStateManager::ESMFromContentOrThis( nsIContent* aContent) { if (aContent) { PresShell* presShell = aContent->OwnerDoc()->GetPresShell(); if (presShell) { nsPresContext* prescontext = presShell->GetPresContext(); if (prescontext) { RefPtr esm = prescontext->EventStateManager(); if (esm) { return esm.forget(); } } } } RefPtr esm = this; return esm.forget(); } auto EventStateManager::GetLastMouseButtonPressInfo(int16_t aButton) const -> const LastMouseButtonPressInfo& { switch (aButton) { case MouseButton::ePrimary: return mLastPrimaryButtonPressInfo; case MouseButton::eMiddle: return mLastMiddleButtonPressInfo; case MouseButton::eSecondary: return mLastSecondaryButtonPressInfo; default: MOZ_ASSERT_UNREACHABLE("This button shouldn't use this method"); return mLastPrimaryButtonPressInfo; } } void EventStateManager::HandleQueryContentEvent( WidgetQueryContentEvent* aEvent) { switch (aEvent->mMessage) { case eQuerySelectedText: case eQueryTextContent: case eQueryCaretRect: case eQueryTextRect: case eQueryEditorRect: if (!IsTargetCrossProcess(aEvent)) { break; } // Will not be handled locally, remote the event GetCrossProcessTarget()->HandleQueryContentEvent(*aEvent); return; // Following events have not been supported in e10s mode yet. case eQueryContentState: case eQuerySelectionAsTransferable: case eQueryCharacterAtPoint: case eQueryDOMWidgetHittest: case eQueryTextRectArray: case eQueryDropTargetHittest: break; default: return; } // If there is an IMEContentObserver, we need to handle QueryContentEvent // with it. // eQueryDropTargetHittest is not really an IME event, though if (mIMEContentObserver && aEvent->mMessage != eQueryDropTargetHittest) { RefPtr contentObserver = mIMEContentObserver; contentObserver->HandleQueryContentEvent(aEvent); return; } ContentEventHandler handler(mPresContext); handler.HandleQueryContentEvent(aEvent); } static AccessKeyType GetAccessKeyTypeFor(nsISupports* aDocShell) { nsCOMPtr treeItem(do_QueryInterface(aDocShell)); if (!treeItem) { return AccessKeyType::eNone; } switch (treeItem->ItemType()) { case nsIDocShellTreeItem::typeChrome: return AccessKeyType::eChrome; case nsIDocShellTreeItem::typeContent: return AccessKeyType::eContent; default: return AccessKeyType::eNone; } } static bool IsAccessKeyTarget(Element* aElement, nsAString& aKey) { // Use GetAttr because we want Unicode case=insensitive matching // XXXbz shouldn't this be case-sensitive, per spec? nsString contentKey; if (!aElement || !aElement->GetAttr(nsGkAtoms::accesskey, contentKey) || !contentKey.Equals(aKey, nsCaseInsensitiveStringComparator)) { return false; } if (!aElement->IsXULElement()) { return true; } // For XUL we do visibility checks. nsIFrame* frame = aElement->GetPrimaryFrame(); if (!frame) { return false; } if (frame->IsFocusable()) { return true; } if (!frame->IsVisibleConsideringAncestors()) { return false; } // XUL controls can be activated. nsCOMPtr control = aElement->AsXULControl(); if (control) { return true; } // XUL label elements are never focusable, so we need to check for them // explicitly before giving up. if (aElement->IsXULElement(nsGkAtoms::label)) { return true; } return false; } bool EventStateManager::CheckIfEventMatchesAccessKey( WidgetKeyboardEvent* aEvent, nsPresContext* aPresContext) { AutoTArray accessCharCodes; aEvent->GetAccessKeyCandidates(accessCharCodes); return WalkESMTreeToHandleAccessKey(aEvent, aPresContext, accessCharCodes, nullptr, eAccessKeyProcessingNormal, false); } bool EventStateManager::LookForAccessKeyAndExecute( nsTArray& aAccessCharCodes, bool aIsTrustedEvent, bool aIsRepeat, bool aExecute) { int32_t count, start = -1; if (Element* focusedElement = GetFocusedElement()) { start = mAccessKeys.IndexOf(focusedElement); if (start == -1 && focusedElement->IsInNativeAnonymousSubtree()) { start = mAccessKeys.IndexOf(Element::FromNodeOrNull( focusedElement->GetClosestNativeAnonymousSubtreeRootParentOrHost())); } } RefPtr element; int32_t length = mAccessKeys.Count(); for (uint32_t i = 0; i < aAccessCharCodes.Length(); ++i) { uint32_t ch = aAccessCharCodes[i]; nsAutoString accessKey; AppendUCS4ToUTF16(ch, accessKey); for (count = 1; count <= length; ++count) { // mAccessKeys always stores Element instances. MOZ_DIAGNOSTIC_ASSERT(length == mAccessKeys.Count()); element = mAccessKeys[(start + count) % length]; if (IsAccessKeyTarget(element, accessKey)) { if (!aExecute) { return true; } Document* doc = element->OwnerDoc(); const bool shouldActivate = [&] { if (!StaticPrefs::accessibility_accesskeycausesactivation()) { return false; } if (aIsRepeat && nsContentUtils::IsChromeDoc(doc)) { return false; } // XXXedgar, Bug 1700646, maybe we could use other data structure to // make searching target with same accesskey easier, and current setup // could not ensure we cycle the target with tree order. int32_t j = 0; while (++j < length) { Element* el = mAccessKeys[(start + count + j) % length]; if (IsAccessKeyTarget(el, accessKey)) { return false; } } return true; }(); // TODO(bug 1641171): This shouldn't be needed if we considered the // accesskey combination properly. if (aIsTrustedEvent) { doc->NotifyUserGestureActivation(); } auto result = element->PerformAccesskey(shouldActivate, aIsTrustedEvent); if (result.isOk()) { if (result.unwrap() && aIsTrustedEvent) { // If this is a child process, inform the parent that we want the // focus, but pass false since we don't want to change the window // order. nsIDocShell* docShell = mPresContext->GetDocShell(); nsCOMPtr child = docShell ? docShell->GetBrowserChild() : nullptr; if (child) { child->SendRequestFocus(false, CallerType::System); } } return true; } } } } return false; } // static void EventStateManager::GetAccessKeyLabelPrefix(Element* aElement, nsAString& aPrefix) { aPrefix.Truncate(); nsAutoString separator, modifierText; nsContentUtils::GetModifierSeparatorText(separator); AccessKeyType accessKeyType = GetAccessKeyTypeFor(aElement->OwnerDoc()->GetDocShell()); if (accessKeyType == AccessKeyType::eNone) { return; } Modifiers modifiers = WidgetKeyboardEvent::AccessKeyModifiers(accessKeyType); if (modifiers == MODIFIER_NONE) { return; } if (modifiers & MODIFIER_CONTROL) { nsContentUtils::GetControlText(modifierText); aPrefix.Append(modifierText + separator); } if (modifiers & MODIFIER_META) { nsContentUtils::GetCommandOrWinText(modifierText); aPrefix.Append(modifierText + separator); } if (modifiers & MODIFIER_ALT) { nsContentUtils::GetAltText(modifierText); aPrefix.Append(modifierText + separator); } if (modifiers & MODIFIER_SHIFT) { nsContentUtils::GetShiftText(modifierText); aPrefix.Append(modifierText + separator); } } struct MOZ_STACK_CLASS AccessKeyInfo { WidgetKeyboardEvent* event; nsTArray& charCodes; AccessKeyInfo(WidgetKeyboardEvent* aEvent, nsTArray& aCharCodes) : event(aEvent), charCodes(aCharCodes) {} }; bool EventStateManager::WalkESMTreeToHandleAccessKey( WidgetKeyboardEvent* aEvent, nsPresContext* aPresContext, nsTArray& aAccessCharCodes, nsIDocShellTreeItem* aBubbledFrom, ProcessingAccessKeyState aAccessKeyState, bool aExecute) { EnsureDocument(mPresContext); nsCOMPtr docShell = aPresContext->GetDocShell(); if (NS_WARN_IF(!docShell) || NS_WARN_IF(!mDocument)) { return false; } AccessKeyType accessKeyType = GetAccessKeyTypeFor(docShell); if (accessKeyType == AccessKeyType::eNone) { return false; } // Alt or other accesskey modifier is down, we may need to do an accesskey. if (mAccessKeys.Count() > 0 && aEvent->ModifiersMatchWithAccessKey(accessKeyType)) { // Someone registered an accesskey. Find and activate it. if (LookForAccessKeyAndExecute(aAccessCharCodes, aEvent->IsTrusted(), aEvent->mIsRepeat, aExecute)) { return true; } } int32_t childCount; docShell->GetInProcessChildCount(&childCount); for (int32_t counter = 0; counter < childCount; counter++) { // Not processing the child which bubbles up the handling nsCOMPtr subShellItem; docShell->GetInProcessChildAt(counter, getter_AddRefs(subShellItem)); if (aAccessKeyState == eAccessKeyProcessingUp && subShellItem == aBubbledFrom) { continue; } nsCOMPtr subDS = do_QueryInterface(subShellItem); if (subDS && IsShellVisible(subDS)) { // Guarantee subPresShell lifetime while we're handling access key // since somebody may assume that it won't be deleted before the // corresponding nsPresContext and EventStateManager. RefPtr subPresShell = subDS->GetPresShell(); // Docshells need not have a presshell (eg. display:none // iframes, docshells in transition between documents, etc). if (!subPresShell) { // Oh, well. Just move on to the next child continue; } RefPtr subPresContext = subPresShell->GetPresContext(); RefPtr esm = static_cast(subPresContext->EventStateManager()); if (esm && esm->WalkESMTreeToHandleAccessKey( aEvent, subPresContext, aAccessCharCodes, nullptr, eAccessKeyProcessingDown, aExecute)) { return true; } } } // if end . checking all sub docshell ends here. // bubble up the process to the parent docshell if necessary if (eAccessKeyProcessingDown != aAccessKeyState) { nsCOMPtr parentShellItem; docShell->GetInProcessParent(getter_AddRefs(parentShellItem)); nsCOMPtr parentDS = do_QueryInterface(parentShellItem); if (parentDS) { // Guarantee parentPresShell lifetime while we're handling access key // since somebody may assume that it won't be deleted before the // corresponding nsPresContext and EventStateManager. RefPtr parentPresShell = parentDS->GetPresShell(); NS_ASSERTION(parentPresShell, "Our PresShell exists but the parent's does not?"); RefPtr parentPresContext = parentPresShell->GetPresContext(); NS_ASSERTION(parentPresContext, "PresShell without PresContext"); RefPtr esm = static_cast( parentPresContext->EventStateManager()); if (esm && esm->WalkESMTreeToHandleAccessKey( aEvent, parentPresContext, aAccessCharCodes, docShell, eAccessKeyProcessingDown, aExecute)) { return true; } } } // if end. bubble up process // If the content access key modifier is pressed, try remote children if (aExecute && aEvent->ModifiersMatchWithAccessKey(AccessKeyType::eContent) && mDocument && mDocument->GetWindow()) { // If the focus is currently on a node with a BrowserParent, the key event // should've gotten forwarded to the child process and HandleAccessKey // called from there. if (BrowserParent::GetFrom(GetFocusedElement())) { // If access key may be only in remote contents, this method won't handle // access key synchronously. In this case, only reply event should reach // here. MOZ_ASSERT(aEvent->IsHandledInRemoteProcess() || !aEvent->IsWaitingReplyFromRemoteProcess()); } // If focus is somewhere else, then we need to check the remote children. // However, if the event has already been handled in a remote process, // then, focus is moved from the remote process after posting the event. // In such case, we shouldn't retry to handle access keys in remote // processes. else if (!aEvent->IsHandledInRemoteProcess()) { AccessKeyInfo accessKeyInfo(aEvent, aAccessCharCodes); nsContentUtils::CallOnAllRemoteChildren( mDocument->GetWindow(), [&accessKeyInfo](BrowserParent* aBrowserParent) -> CallState { // Only forward accesskeys for the active tab. if (aBrowserParent->GetDocShellIsActive()) { // Even if there is no target for the accesskey in this process, // the event may match with a content accesskey. If so, the // keyboard event should be handled with reply event for // preventing double action. (e.g., Alt+Shift+F on Windows may // focus a content in remote and open "File" menu.) accessKeyInfo.event->StopPropagation(); accessKeyInfo.event->MarkAsWaitingReplyFromRemoteProcess(); aBrowserParent->HandleAccessKey(*accessKeyInfo.event, accessKeyInfo.charCodes); return CallState::Stop; } return CallState::Continue; }); } } return false; } // end of HandleAccessKey static BrowserParent* GetBrowserParentAncestor(BrowserParent* aBrowserParent) { MOZ_ASSERT(aBrowserParent); BrowserBridgeParent* bbp = aBrowserParent->GetBrowserBridgeParent(); if (!bbp) { return nullptr; } return bbp->Manager(); } static void DispatchCrossProcessMouseExitEvents(WidgetMouseEvent* aMouseEvent, BrowserParent* aRemoteTarget, BrowserParent* aStopAncestor, bool aIsReallyExit) { MOZ_ASSERT(aMouseEvent); MOZ_ASSERT(aRemoteTarget); MOZ_ASSERT(aRemoteTarget != aStopAncestor); MOZ_ASSERT_IF(aStopAncestor, nsContentUtils::GetCommonBrowserParentAncestor( aRemoteTarget, aStopAncestor)); while (aRemoteTarget != aStopAncestor) { UniquePtr mouseExitEvent = CreateMouseOrPointerWidgetEvent(aMouseEvent, eMouseExitFromWidget, aMouseEvent->mRelatedTarget); mouseExitEvent->mExitFrom = Some(aIsReallyExit ? WidgetMouseEvent::ePuppet : WidgetMouseEvent::ePuppetParentToPuppetChild); auto ContentReactsToPointerEvents = [](BrowserParent* aRemoteTarget) { if (Element* owner = aRemoteTarget->GetOwnerElement()) { if (nsSubDocumentFrame* subDocFrame = do_QueryFrame(owner->GetPrimaryFrame())) { return subDocFrame->ContentReactsToPointerEvents(); } } return true; }; if (ContentReactsToPointerEvents(aRemoteTarget)) { aRemoteTarget->SendRealMouseEvent(*mouseExitEvent); } aRemoteTarget = GetBrowserParentAncestor(aRemoteTarget); } } void EventStateManager::DispatchCrossProcessEvent(WidgetEvent* aEvent, BrowserParent* aRemoteTarget, nsEventStatus* aStatus) { MOZ_ASSERT(aEvent); MOZ_ASSERT(aRemoteTarget); MOZ_ASSERT(aStatus); BrowserParent* remote = aRemoteTarget; WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); bool isContextMenuKey = mouseEvent && mouseEvent->IsContextMenuKeyEvent(); if (aEvent->mClass == eKeyboardEventClass || isContextMenuKey) { // APZ attaches a LayersId to hit-testable events, for keyboard events, // we use focus. BrowserParent* preciseRemote = BrowserParent::GetFocused(); if (preciseRemote) { remote = preciseRemote; } // else there is a race between layout and focus tracking, // so fall back to delivering the event to the topmost child process. } else if (aEvent->mLayersId.IsValid()) { BrowserParent* preciseRemote = BrowserParent::GetBrowserParentFromLayersId(aEvent->mLayersId); if (preciseRemote) { remote = preciseRemote; } // else there is a race between APZ and the LayersId to BrowserParent // mapping, so fall back to delivering the event to the topmost child // process. } MOZ_ASSERT(aEvent->mMessage != ePointerClick); MOZ_ASSERT(aEvent->mMessage != ePointerAuxClick); // SendReal* will transform the coordinate to the child process coordinate // space. So restore the coordinate after the event has been dispatched to the // child process to avoid using the transformed coordinate afterward. AutoRestore restore(aEvent->mRefPoint); switch (aEvent->mClass) { case ePointerEventClass: MOZ_ASSERT(aEvent->mMessage == eContextMenu); [[fallthrough]]; case eMouseEventClass: { BrowserParent* oldRemote = BrowserParent::GetLastMouseRemoteTarget(); // If this is a eMouseExitFromWidget event, need to redirect the event to // the last remote and and notify all its ancestors about the exit, if // any. if (mouseEvent->mMessage == eMouseExitFromWidget) { MOZ_ASSERT(mouseEvent->mExitFrom.value() == WidgetMouseEvent::ePuppet); MOZ_ASSERT(mouseEvent->mReason == WidgetMouseEvent::eReal); MOZ_ASSERT(!mouseEvent->mLayersId.IsValid()); MOZ_ASSERT(remote->GetBrowserHost()); if (oldRemote && oldRemote != remote) { (void)NS_WARN_IF(nsContentUtils::GetCommonBrowserParentAncestor( remote, oldRemote) != remote); remote = oldRemote; } DispatchCrossProcessMouseExitEvents(mouseEvent, remote, nullptr, true); return; } if (BrowserParent* pointerLockedRemote = PointerLockManager::GetLockedRemoteTarget()) { remote = pointerLockedRemote; } else if (BrowserParent* pointerCapturedRemote = PointerEventHandler::GetPointerCapturingRemoteTarget( mouseEvent->pointerId)) { remote = pointerCapturedRemote; } else if (BrowserParent* capturingRemote = PresShell::GetCapturingRemoteTarget()) { remote = capturingRemote; } // If a mouse is over a remote target A, and then moves to // remote target B, we'd deliver the event directly to remote target B // after the moving, A would never get notified that the mouse left. // So we generate a exit event to notify A after the move. // XXXedgar, if the synthesized mouse events could deliver to the correct // process directly (see // https://bugzilla.mozilla.org/show_bug.cgi?id=1549355), we probably // don't need to check mReason then. if (mouseEvent->mReason == WidgetMouseEvent::eReal && remote != oldRemote) { MOZ_ASSERT(mouseEvent->mMessage != eMouseExitFromWidget); if (oldRemote) { BrowserParent* commonAncestor = nsContentUtils::GetCommonBrowserParentAncestor(remote, oldRemote); if (commonAncestor == oldRemote) { // Mouse moves to the inner OOP frame, it is not a really exit. DispatchCrossProcessMouseExitEvents( mouseEvent, GetBrowserParentAncestor(remote), GetBrowserParentAncestor(commonAncestor), false); } else if (commonAncestor == remote) { // Mouse moves to the outer OOP frame, it is a really exit. DispatchCrossProcessMouseExitEvents(mouseEvent, oldRemote, commonAncestor, true); } else { // Mouse moves to OOP frame in other subtree, it is a really exit, // need to notify all its ancestors before common ancestor about the // exit. DispatchCrossProcessMouseExitEvents(mouseEvent, oldRemote, commonAncestor, true); if (commonAncestor) { UniquePtr mouseExitEvent = CreateMouseOrPointerWidgetEvent(mouseEvent, eMouseExitFromWidget, mouseEvent->mRelatedTarget); mouseExitEvent->mExitFrom = Some(WidgetMouseEvent::ePuppetParentToPuppetChild); commonAncestor->SendRealMouseEvent(*mouseExitEvent); } } } if (mouseEvent->mMessage != eMouseExitFromWidget && mouseEvent->mMessage != eMouseEnterIntoWidget) { // This is to make cursor would be updated correctly. remote->MouseEnterIntoWidget(); } } remote->SendRealMouseEvent(*mouseEvent); return; } case eKeyboardEventClass: { auto* keyboardEvent = aEvent->AsKeyboardEvent(); if (aEvent->mMessage == eKeyUp) { HandleKeyUpInteraction(keyboardEvent); } remote->SendRealKeyEvent(*keyboardEvent); return; } case eWheelEventClass: { if (BrowserParent* pointerLockedRemote = PointerLockManager::GetLockedRemoteTarget()) { remote = pointerLockedRemote; } remote->SendMouseWheelEvent(*aEvent->AsWheelEvent()); return; } case eTouchEventClass: { // Let the child process synthesize a mouse event if needed, and // ensure we don't synthesize one in this process. *aStatus = nsEventStatus_eConsumeNoDefault; remote->SendRealTouchEvent(*aEvent->AsTouchEvent()); return; } case eDragEventClass: { RefPtr browserParent = remote; browserParent->MaybeInvokeDragSession(aEvent->mMessage); RefPtr widget = browserParent->GetTopLevelWidget(); nsCOMPtr dragSession = nsContentUtils::GetDragSession(widget); uint32_t dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; uint32_t action = nsIDragService::DRAGDROP_ACTION_NONE; nsCOMPtr principal; nsCOMPtr policyContainer; if (dragSession) { dragSession->DragEventDispatchedToChildProcess(); dragSession->GetDragAction(&action); dragSession->GetTriggeringPrincipal(getter_AddRefs(principal)); dragSession->GetPolicyContainer(getter_AddRefs(policyContainer)); RefPtr initialDataTransfer = dragSession->GetDataTransfer(); if (initialDataTransfer) { dropEffect = initialDataTransfer->DropEffectInt(); } } browserParent->SendRealDragEvent(*aEvent->AsDragEvent(), action, dropEffect, principal, policyContainer); return; } default: { MOZ_CRASH("Attempt to send non-whitelisted event?"); } } } bool EventStateManager::IsRemoteTarget(nsIContent* target) { return BrowserParent::GetFrom(target) || BrowserBridgeChild::GetFrom(target); } bool EventStateManager::IsTopLevelRemoteTarget(nsIContent* target) { return !!BrowserParent::GetFrom(target); } bool EventStateManager::HandleCrossProcessEvent(WidgetEvent* aEvent, nsEventStatus* aStatus) { if (!aEvent->CanBeSentToRemoteProcess()) { return false; } MOZ_ASSERT(!aEvent->HasBeenPostedToRemoteProcess(), "Why do we need to post same event to remote processes again?"); // Collect the remote event targets we're going to forward this // event to. // // NB: the elements of |remoteTargets| must be unique, for correctness. AutoTArray, 1> remoteTargets; if (aEvent->mClass != eTouchEventClass || aEvent->mMessage == eTouchStart) { // If this event only has one target, and it's remote, add it to // the array. nsIFrame* frame = aEvent->mMessage == eDragExit ? sLastDragOverFrame.GetFrame() : GetEventTarget(); nsIContent* target = frame ? frame->GetContent() : nullptr; if (BrowserParent* remoteTarget = BrowserParent::GetFrom(target)) { remoteTargets.AppendElement(remoteTarget); } } else { // This is a touch event with possibly multiple touch points. // Each touch point may have its own target. So iterate through // all of them and collect the unique set of targets for event // forwarding. // // This loop is similar to the one used in // PresShell::DispatchTouchEvent(). const WidgetTouchEvent::TouchArray& touches = aEvent->AsTouchEvent()->mTouches; for (uint32_t i = 0; i < touches.Length(); ++i) { Touch* touch = touches[i]; // NB: the |mChanged| check is an optimization, subprocesses can // compute this for themselves. If the touch hasn't changed, we // may be able to avoid forwarding the event entirely (which is // not free). if (!touch || !touch->mChanged) { continue; } nsCOMPtr targetPtr = touch->mTarget; if (!targetPtr) { continue; } nsCOMPtr target = do_QueryInterface(targetPtr); BrowserParent* remoteTarget = BrowserParent::GetFrom(target); if (remoteTarget && !remoteTargets.Contains(remoteTarget)) { remoteTargets.AppendElement(remoteTarget); } } } if (remoteTargets.Length() == 0) { return false; } // Dispatch the event to the remote target. for (uint32_t i = 0; i < remoteTargets.Length(); ++i) { DispatchCrossProcessEvent(aEvent, remoteTargets[i], aStatus); } return aEvent->HasBeenPostedToRemoteProcess(); } // // CreateClickHoldTimer // // Fire off a timer for determining if the user wants click-hold. This timer // is a one-shot that will be cancelled when the user moves enough to fire // a drag. // void EventStateManager::CreateClickHoldTimer(nsPresContext* inPresContext, nsIFrame* inDownFrame, WidgetGUIEvent* inMouseDownEvent) { if (!inMouseDownEvent->IsTrusted() || IsTopLevelRemoteTarget(mGestureDownContent) || PointerLockManager::IsLocked()) { return; } // just to be anal (er, safe) if (mClickHoldTimer) { mClickHoldTimer->Cancel(); mClickHoldTimer = nullptr; } // if content clicked on has a popup, don't even start the timer // since we'll end up conflicting and both will show. if (mGestureDownContent && nsContentUtils::HasNonEmptyAttr(mGestureDownContent, kNameSpaceID_None, nsGkAtoms::popup)) { return; } int32_t clickHoldDelay = StaticPrefs::ui_click_hold_context_menus_delay(); NS_NewTimerWithFuncCallback( getter_AddRefs(mClickHoldTimer), sClickHoldCallback, this, clickHoldDelay, nsITimer::TYPE_ONE_SHOT, "EventStateManager::CreateClickHoldTimer"_ns); } // CreateClickHoldTimer // // KillClickHoldTimer // // Stop the timer that would show the context menu dead in its tracks // void EventStateManager::KillClickHoldTimer() { if (mClickHoldTimer) { mClickHoldTimer->Cancel(); mClickHoldTimer = nullptr; } } // // sClickHoldCallback // // This fires after the mouse has been down for a certain length of time. // void EventStateManager::sClickHoldCallback(nsITimer* aTimer, void* aESM) { RefPtr self = static_cast(aESM); if (self) { self->FireContextClick(); } // NOTE: |aTimer| and |self->mAutoHideTimer| are invalid after calling // ClosePopup(); } // sAutoHideCallback // // FireContextClick // // If we're this far, our timer has fired, which means the mouse has been down // for a certain period of time and has not moved enough to generate a // dragGesture. We can be certain the user wants a context-click at this stage, // so generate a dom event and fire it in. // // After the event fires, check if PreventDefault() has been set on the event // which means that someone either ate the event or put up a context menu. This // is our cue to stop tracking the drag gesture. If we always did this, // draggable items w/out a context menu wouldn't be draggable after a certain // length of time, which is _not_ what we want. // void EventStateManager::FireContextClick() { if (!mGestureDownContent || !mPresContext || PointerLockManager::IsLocked()) { return; } #ifdef XP_MACOSX // Hack to ensure that we don't show a context menu when the user // let go of the mouse after a long cpu-hogging operation prevented // us from handling any OS events. See bug 117589. if (!CGEventSourceButtonState(kCGEventSourceStateCombinedSessionState, kCGMouseButtonLeft)) return; #endif nsEventStatus status = nsEventStatus_eIgnore; // Dispatch to the DOM. We have to fake out the ESM and tell it that the // current target frame is actually where the mouseDown occurred, otherwise it // will use the frame the mouse is currently over which may or may not be // the same. (Note: saari and I have decided that we don't have to reset // |mCurrentTarget| when we're through because no one else is doing anything // more with this event and it will get reset on the very next event to the // correct frame). mCurrentTarget = mPresContext->GetPrimaryFrameFor(mGestureDownContent); // make sure the widget sticks around nsCOMPtr targetWidget; if (mCurrentTarget && (targetWidget = mCurrentTarget->GetNearestWidget())) { NS_ASSERTION( mPresContext == mCurrentTarget->PresContext(), "a prescontext returned a primary frame that didn't belong to it?"); // before dispatching, check that we're not on something that // doesn't get a context menu bool allowedToDispatch = true; if (mGestureDownContent->IsAnyOfXULElements(nsGkAtoms::scrollbar, nsGkAtoms::scrollbarbutton, nsGkAtoms::button)) { allowedToDispatch = false; } else if (mGestureDownContent->IsXULElement(nsGkAtoms::toolbarbutton)) { // a that has the container attribute set // will already have its own dropdown. if (nsContentUtils::HasNonEmptyAttr( mGestureDownContent, kNameSpaceID_None, nsGkAtoms::container)) { allowedToDispatch = false; } else { // If the toolbar button has an open menu, don't attempt to open // a second menu if (mGestureDownContent->IsElement() && mGestureDownContent->AsElement()->AttrValueIs( kNameSpaceID_None, nsGkAtoms::open, nsGkAtoms::_true, eCaseMatters)) { allowedToDispatch = false; } } } else if (mGestureDownContent->IsHTMLElement()) { if (const auto* formCtrl = nsIFormControl::FromNode(mGestureDownContent)) { allowedToDispatch = formCtrl->IsTextControl(/*aExcludePassword*/ false) || formCtrl->ControlType() == FormControlType::InputFile; } else if (mGestureDownContent->IsAnyOfHTMLElements( nsGkAtoms::embed, nsGkAtoms::object, nsGkAtoms::label)) { allowedToDispatch = false; } } if (allowedToDispatch) { // init the event while mCurrentTarget is still good WidgetPointerEvent event(true, eContextMenu, targetWidget); event.mClickCount = 1; FillInEventFromGestureDown(&event); // we need to forget the clicking content and click count for the // following eMouseUp event when click-holding context menus GetLastMouseButtonPressInfo(event.mButton).Clear(); // stop selection tracking, we're in control now if (mCurrentTarget) { RefPtr frameSel = mCurrentTarget->GetFrameSelection(); if (frameSel && frameSel->GetDragState()) { // note that this can cause selection changed events to fire if we're // in a text field, which will null out mCurrentTarget frameSel->SetDragState(false); } } AutoHandlingUserInputStatePusher userInpStatePusher(true, &event); // dispatch to DOM RefPtr presContext = mPresContext; // The contextmenu event handled by PresShell will apply to elements (not // all nodes) correctly and will be dispatched to EventStateManager for // further handling preventing click event and stopping tracking drag // gesture. if (RefPtr presShell = presContext->GetPresShell()) { presShell->HandleEvent(mCurrentTarget, &event, false, &status); } // We don't need to dispatch to frame handling because no frames // watch eContextMenu except for nsMenuFrame and that's only for // dismissal. That's just as well since we don't really know // which frame to send it to. } } // stop tracking a drag whatever the event has been handled or not. StopTrackingDragGesture(true); KillClickHoldTimer(); } // FireContextClick // // BeginTrackingDragGesture // // Record that the mouse has gone down and that we should move to TRACKING state // of d&d gesture tracker. // // We also use this to track click-hold context menus. When the mouse goes down, // fire off a short timer. If the timer goes off and we have yet to fire the // drag gesture (ie, the mouse hasn't moved a certain distance), then we can // assume the user wants a click-hold, so fire a context-click event. We only // want to cancel the drag gesture if the context-click event is handled. // void EventStateManager::BeginTrackingDragGesture( nsPresContext* aPresContext, WidgetMouseEvent& aMouseDownOrTouchDragEvent, nsIFrame* aMouseDownOrTouchDragFrame) { MOZ_ASSERT(aMouseDownOrTouchDragEvent.mMessage == eMouseDown || aMouseDownOrTouchDragEvent.mMessage == eMouseTouchDrag); if (!aMouseDownOrTouchDragEvent.mWidget) [[unlikely]] { return; } // Note that |inDownEvent| could be either a mouse down event or a // synthesized mouse move event. SetGestureDownPoint(aMouseDownOrTouchDragEvent); if (aMouseDownOrTouchDragFrame) { // We need to store the explicit target of the drag gesture start content, // i.e., it may be a `Text` even though the event target should be its // flattened tree parent element because we want to maintain `Selection` // with the `Text`. E.g., we want to allow to extending selection in a // draggable editing host, check whether the `Text` is selectable or not // like in a `Text` of a