diff options
Diffstat (limited to 'dom/events/EventStateManager.cpp')
-rw-r--r-- | dom/events/EventStateManager.cpp | 6696 |
1 files changed, 6696 insertions, 0 deletions
diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp new file mode 100644 index 0000000000..1ba56447dc --- /dev/null +++ b/dom/events/EventStateManager.cpp @@ -0,0 +1,6696 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "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/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/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/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 "nsIWidget.h" +#include "nsLiteralString.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsIFormControl.h" +#include "nsComboboxControlFrame.h" +#include "nsIScrollableFrame.h" +#include "nsIDOMXULControlElement.h" +#include "nsNameSpaceManager.h" +#include "nsIBaseWindow.h" +#include "nsFrameSelection.h" +#include "nsPIDOMWindow.h" +#include "nsPIWindowRoot.h" +#include "nsIWebNavigation.h" +#include "nsIContentViewer.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); + +/******************************************************************/ +/* 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.AccumulateSamples( + {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 */ +/******************************************************************/ + +OverOutElementsWrapper::OverOutElementsWrapper() : mLastOverFrame(nullptr) {} + +OverOutElementsWrapper::~OverOutElementsWrapper() = default; + +NS_IMPL_CYCLE_COLLECTION(OverOutElementsWrapper, mLastOverElement, + mFirstOverEventElement, mFirstOutEventElement) +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 + +/******************************************************************/ +/* mozilla::EventStateManager */ +/******************************************************************/ + +static uint32_t sESMInstanceCount = 0; + +bool EventStateManager::sNormalLMouseEventInProcess = false; +int16_t EventStateManager::sCurrentMouseBtn = MouseButton::eNotPressed; +EventStateManager* EventStateManager::sActiveESM = nullptr; +Document* EventStateManager::sMouseOverDocument = 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), + mLastFrameConsumedSetCursor(false), + mCurrentTarget(nullptr), + // init d&d gesture state machine variables + mGestureDownPoint(0, 0), + mGestureModifiers(0), + mGestureDownButtons(0), + mPresContext(nullptr), + mLClickCount(0), + mMClickCount(0), + mRClickCount(0), + 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 (mDocument == sMouseOverDocument) { + sMouseOverDocument = 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, + mLastLeftMouseDownContent, + mLastMiddleMouseDownContent, + mLastRightMouseDownContent, mActiveContent, + mHoverContent, mURLTargetContent, + 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 == eMouseAuxClick || + aMessage == eMouseDoubleClick || aMessage == eMouseClick || + 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->IsOS() || 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) { + 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); + } + } + + 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); + mLClickCount = mouseEvent->mClickCount; + SetClickCount(mouseEvent, aStatus); + sNormalLMouseEventInProcess = true; + break; + case MouseButton::eMiddle: + mMClickCount = mouseEvent->mClickCount; + SetClickCount(mouseEvent, aStatus); + break; + case MouseButton::eSecondary: + mRClickCount = mouseEvent->mClickCount; + SetClickCount(mouseEvent, aStatus); + break; + } + 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::DispatchMouseOrPointerEvent (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 (mouseEvent->mInputSource != MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + NotifyTargetUserActivation(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, aEvent, mCurrentTarget, aStatus); + + UpdateLastRefPointOfMouseEvent(mouseEvent); + if (PointerLockManager::IsLocked()) { + ResetPointerToWindowCenterWhilePointerLocked(mouseEvent); + } + UpdateLastPointerPosition(mouseEvent); + + GenerateMouseEnterExit(mouseEvent); + // Flush pending layout changes, so that later mouse move events + // will go to the right nodes. + FlushLayout(aPresContext); + 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) { + dragEvent->InitDropEffectForTests(); + } + // Send the enter/exit events before eDrop. + GenerateDragDropEnterExit(aPresContext, dragEvent); + break; + } + case eDrop: + if (aEvent->mFlags.mIsSynthesizedForTests) { + 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 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<uint32_t, 10> 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<TextComposition> 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<Element> 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 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; + case eTouchEnd: + NotifyTargetUserActivation(aEvent, aTargetContent); + break; + default: + break; + } + return NS_OK; +} + +void EventStateManager::NotifyTargetUserActivation(WidgetEvent* aEvent, + nsIContent* aTargetContent) { + if (!aEvent->IsTrusted()) { + return; + } + + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (mouseEvent && !mouseEvent->IsReal()) { + return; + } + + nsCOMPtr<nsINode> 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; + } + + // Touch gestures that end outside the drag target were touches that turned + // into scroll/pan/swipe actions. We don't want to gesture activate on such + // actions, we want to only gesture activate on touches that are taps. + // That is, touches that end in roughly the same place that they started. + if (aEvent->mMessage == eTouchEnd && aEvent->AsTouchEvent() && + IsEventOutsideDragThreshold(aEvent->AsTouchEvent())) { + 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 == eMouseDown || aEvent->mMessage == ePointerDown) && + 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 == eMouseDown || + aEvent->mMessage == ePointerDown || aEvent->mMessage == eTouchEnd); + doc->NotifyUserGestureActivation(); +} + +already_AddRefed<EventStateManager> EventStateManager::ESMFromContentOrThis( + nsIContent* aContent) { + if (aContent) { + PresShell* presShell = aContent->OwnerDoc()->GetPresShell(); + if (presShell) { + nsPresContext* prescontext = presShell->GetPresContext(); + if (prescontext) { + RefPtr<EventStateManager> esm = prescontext->EventStateManager(); + if (esm) { + return esm.forget(); + } + } + } + } + + RefPtr<EventStateManager> esm = this; + return esm.forget(); +} + +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: + break; + default: + return; + } + + // If there is an IMEContentObserver, we need to handle QueryContentEvent + // with it. + if (mIMEContentObserver) { + RefPtr<IMEContentObserver> contentObserver = mIMEContentObserver; + contentObserver->HandleQueryContentEvent(aEvent); + return; + } + + ContentEventHandler handler(mPresContext); + handler.HandleQueryContentEvent(aEvent); +} + +static AccessKeyType GetAccessKeyTypeFor(nsISupports* aDocShell) { + nsCOMPtr<nsIDocShellTreeItem> 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(kNameSpaceID_None, 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<nsIDOMXULControlElement> 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<uint32_t, 10> accessCharCodes; + aEvent->GetAccessKeyCandidates(accessCharCodes); + return WalkESMTreeToHandleAccessKey(aEvent, aPresContext, accessCharCodes, + nullptr, eAccessKeyProcessingNormal, + false); +} + +bool EventStateManager::LookForAccessKeyAndExecute( + nsTArray<uint32_t>& 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> 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<nsIBrowserChild> 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::GetMetaText(modifierText); + aPrefix.Append(modifierText + separator); + } + if (modifiers & MODIFIER_OS) { + nsContentUtils::GetOSText(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<uint32_t>& charCodes; + + AccessKeyInfo(WidgetKeyboardEvent* aEvent, nsTArray<uint32_t>& aCharCodes) + : event(aEvent), charCodes(aCharCodes) {} +}; + +bool EventStateManager::WalkESMTreeToHandleAccessKey( + WidgetKeyboardEvent* aEvent, nsPresContext* aPresContext, + nsTArray<uint32_t>& aAccessCharCodes, nsIDocShellTreeItem* aBubbledFrom, + ProcessingAccessKeyState aAccessKeyState, bool aExecute) { + EnsureDocument(mPresContext); + nsCOMPtr<nsIDocShell> 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<nsIDocShellTreeItem> subShellItem; + docShell->GetInProcessChildAt(counter, getter_AddRefs(subShellItem)); + if (aAccessKeyState == eAccessKeyProcessingUp && + subShellItem == aBubbledFrom) { + continue; + } + + nsCOMPtr<nsIDocShell> 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<PresShell> 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<nsPresContext> subPresContext = subPresShell->GetPresContext(); + + RefPtr<EventStateManager> esm = + static_cast<EventStateManager*>(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<nsIDocShellTreeItem> parentShellItem; + docShell->GetInProcessParent(getter_AddRefs(parentShellItem)); + nsCOMPtr<nsIDocShell> 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<PresShell> parentPresShell = parentDS->GetPresShell(); + NS_ASSERTION(parentPresShell, + "Our PresShell exists but the parent's does not?"); + + RefPtr<nsPresContext> parentPresContext = + parentPresShell->GetPresContext(); + NS_ASSERTION(parentPresContext, "PresShell without PresContext"); + + RefPtr<EventStateManager> esm = static_cast<EventStateManager*>( + 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<WidgetMouseEvent> mouseExitEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, eMouseExitFromWidget, + aMouseEvent->mRelatedTarget); + mouseExitEvent->mExitFrom = + Some(aIsReallyExit ? WidgetMouseEvent::ePuppet + : WidgetMouseEvent::ePuppetParentToPuppetChild); + 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. + } + + switch (aEvent->mClass) { + 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) { + Unused << 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<WidgetMouseEvent> 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> browserParent = remote; + browserParent->Manager()->MaybeInvokeDragSession(browserParent); + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + uint32_t dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; + uint32_t action = nsIDragService::DRAGDROP_ACTION_NONE; + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIContentSecurityPolicy> csp; + + if (dragSession) { + dragSession->DragEventDispatchedToChildProcess(); + dragSession->GetDragAction(&action); + dragSession->GetTriggeringPrincipal(getter_AddRefs(principal)); + dragSession->GetCsp(getter_AddRefs(csp)); + RefPtr<DataTransfer> initialDataTransfer = + dragSession->GetDataTransfer(); + if (initialDataTransfer) { + dropEffect = initialDataTransfer->DropEffectInt(); + } + } + + browserParent->SendRealDragEvent(*aEvent->AsDragEvent(), action, + dropEffect, principal, csp); + 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<RefPtr<BrowserParent>, 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<EventTarget> targetPtr = touch->mTarget; + if (!targetPtr) { + continue; + } + nsCOMPtr<nsIContent> 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"); +} // 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<EventStateManager> self = static_cast<EventStateManager*>(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<nsIWidget> 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 <toolbarbutton> 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()) { + nsCOMPtr<nsIFormControl> formCtrl(do_QueryInterface(mGestureDownContent)); + + if (formCtrl) { + 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 + WidgetMouseEvent event(true, eContextMenu, targetWidget, + WidgetMouseEvent::eReal); + event.mClickCount = 1; + FillInEventFromGestureDown(&event); + + // stop selection tracking, we're in control now + if (mCurrentTarget) { + RefPtr<nsFrameSelection> 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<nsIContent> gestureDownContent = mGestureDownContent; + RefPtr<nsPresContext> presContext = mPresContext; + EventDispatcher::Dispatch(gestureDownContent, presContext, &event, + nullptr, &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. + } + } + + // now check if the event has been handled. If so, stop tracking a drag + if (status == nsEventStatus_eConsumeNoDefault) { + 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* inDownEvent, + nsIFrame* inDownFrame) { + if (!inDownEvent->mWidget) { + return; + } + + // Note that |inDownEvent| could be either a mouse down event or a + // synthesized mouse move event. + SetGestureDownPoint(inDownEvent); + + if (inDownFrame) { + inDownFrame->GetContentForEvent(inDownEvent, + getter_AddRefs(mGestureDownContent)); + + mGestureDownFrameOwner = inDownFrame->GetContent(); + if (!mGestureDownFrameOwner) { + mGestureDownFrameOwner = mGestureDownContent; + } + } + mGestureModifiers = inDownEvent->mModifiers; + mGestureDownButtons = inDownEvent->mButtons; + + if (inDownEvent->mMessage != eMouseTouchDrag && + StaticPrefs::ui_click_hold_context_menus()) { + // fire off a timer to track click-hold + CreateClickHoldTimer(aPresContext, inDownFrame, inDownEvent); + } +} + +void EventStateManager::SetGestureDownPoint(WidgetGUIEvent* aEvent) { + mGestureDownPoint = + GetEventRefPoint(aEvent) + aEvent->mWidget->WidgetToScreenOffset(); +} + +LayoutDeviceIntPoint EventStateManager::GetEventRefPoint( + WidgetEvent* aEvent) const { + auto touchEvent = aEvent->AsTouchEvent(); + return (touchEvent && !touchEvent->mTouches.IsEmpty()) + ? aEvent->AsTouchEvent()->mTouches[0]->mRefPoint + : aEvent->mRefPoint; +} + +void EventStateManager::BeginTrackingRemoteDragGesture( + nsIContent* aContent, RemoteDragStartData* aDragStartData) { + mGestureDownContent = aContent; + mGestureDownFrameOwner = aContent; + mGestureDownInTextControl = + aContent && aContent->IsInNativeAnonymousSubtree() && + TextControlElement::FromNodeOrNull( + aContent->GetClosestNativeAnonymousSubtreeRootParentOrHost()); + mGestureDownDragStartData = aDragStartData; +} + +// +// StopTrackingDragGesture +// +// Record that the mouse has gone back up so that we should leave the TRACKING +// state of d&d gesture tracker and return to the START state. +// +void EventStateManager::StopTrackingDragGesture(bool aClearInChildProcesses) { + mGestureDownContent = nullptr; + mGestureDownFrameOwner = nullptr; + mGestureDownInTextControl = false; + mGestureDownDragStartData = nullptr; + + // If a content process starts a drag but the mouse is released before the + // parent starts the actual drag, the content process will think a drag is + // still happening. Inform any child processes with active drags that the drag + // should be stopped. + if (aClearInChildProcesses) { + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (dragService) { + nsCOMPtr<nsIDragSession> dragSession; + dragService->GetCurrentSession(getter_AddRefs(dragSession)); + if (!dragSession) { + // Only notify if there isn't a drag session active. + dragService->RemoveAllChildProcesses(); + } + } + } +} + +void EventStateManager::FillInEventFromGestureDown(WidgetMouseEvent* aEvent) { + NS_ASSERTION(aEvent->mWidget == mCurrentTarget->GetNearestWidget(), + "Incorrect widget in event"); + + // Set the coordinates in the new event to the coordinates of + // the old event, adjusted for the fact that the widget might be + // different + aEvent->mRefPoint = + mGestureDownPoint - aEvent->mWidget->WidgetToScreenOffset(); + aEvent->mModifiers = mGestureModifiers; + aEvent->mButtons = mGestureDownButtons; +} + +void EventStateManager::MaybeFirePointerCancel(WidgetInputEvent* aEvent) { + RefPtr<PresShell> presShell = mPresContext->GetPresShell(); + AutoWeakFrame targetFrame = mCurrentTarget; + + if (!presShell || !targetFrame) { + return; + } + + nsCOMPtr<nsIContent> content; + targetFrame->GetContentForEvent(aEvent, getter_AddRefs(content)); + if (!content) { + return; + } + + nsEventStatus status = nsEventStatus_eIgnore; + + if (WidgetMouseEvent* aMouseEvent = aEvent->AsMouseEvent()) { + WidgetPointerEvent event(*aMouseEvent); + PointerEventHandler::InitPointerEventFromMouse(&event, aMouseEvent, + ePointerCancel); + + event.convertToPointer = false; + presShell->HandleEventWithTarget(&event, targetFrame, content, &status); + } else if (WidgetTouchEvent* aTouchEvent = aEvent->AsTouchEvent()) { + WidgetPointerEvent event(aTouchEvent->IsTrusted(), ePointerCancel, + aTouchEvent->mWidget); + + PointerEventHandler::InitPointerEventFromTouch( + event, *aTouchEvent, *aTouchEvent->mTouches[0], true); + + event.convertToPointer = false; + presShell->HandleEventWithTarget(&event, targetFrame, content, &status); + } else { + MOZ_ASSERT(false); + } + + // HandleEventWithTarget clears out mCurrentTarget, which may be used in the + // caller GenerateDragGesture. We have to restore mCurrentTarget. + mCurrentTarget = targetFrame; +} + +bool EventStateManager::IsEventOutsideDragThreshold( + WidgetInputEvent* aEvent) const { + static int32_t sPixelThresholdX = 0; + static int32_t sPixelThresholdY = 0; + + if (!sPixelThresholdX) { + sPixelThresholdX = + LookAndFeel::GetInt(LookAndFeel::IntID::DragThresholdX, 0); + sPixelThresholdY = + LookAndFeel::GetInt(LookAndFeel::IntID::DragThresholdY, 0); + if (sPixelThresholdX <= 0) { + sPixelThresholdX = 5; + } + if (sPixelThresholdY <= 0) { + sPixelThresholdY = 5; + } + } + + LayoutDeviceIntPoint pt = + aEvent->mWidget->WidgetToScreenOffset() + GetEventRefPoint(aEvent); + LayoutDeviceIntPoint distance = pt - mGestureDownPoint; + return Abs(distance.x) > sPixelThresholdX || + Abs(distance.y) > sPixelThresholdY; +} + +// +// GenerateDragGesture +// +// If we're in the TRACKING state of the d&d gesture tracker, check the current +// position of the mouse in relation to the old one. If we've moved a sufficient +// amount from the mouse down, then fire off a drag gesture event. +void EventStateManager::GenerateDragGesture(nsPresContext* aPresContext, + WidgetInputEvent* aEvent) { + NS_ASSERTION(aPresContext, "This shouldn't happen."); + if (!IsTrackingDragGesture()) { + return; + } + + AutoWeakFrame targetFrameBefore = mCurrentTarget; + auto autoRestore = MakeScopeExit([&] { mCurrentTarget = targetFrameBefore; }); + mCurrentTarget = mGestureDownFrameOwner->GetPrimaryFrame(); + + if (!mCurrentTarget || !mCurrentTarget->GetNearestWidget()) { + StopTrackingDragGesture(true); + return; + } + + // Check if selection is tracking drag gestures, if so + // don't interfere! + if (mCurrentTarget) { + RefPtr<nsFrameSelection> frameSel = mCurrentTarget->GetFrameSelection(); + if (frameSel && frameSel->GetDragState()) { + StopTrackingDragGesture(true); + return; + } + } + + // If non-native code is capturing the mouse don't start a drag. + if (PresShell::IsMouseCapturePreventingDrag()) { + StopTrackingDragGesture(true); + return; + } + + if (!IsEventOutsideDragThreshold(aEvent)) { + // To keep the old behavior, flush layout even if we don't start dnd. + FlushLayout(aPresContext); + return; + } + + if (StaticPrefs::ui_click_hold_context_menus()) { + // stop the click-hold before we fire off the drag gesture, in case + // it takes a long time + KillClickHoldTimer(); + } + + nsCOMPtr<nsIDocShell> docshell = aPresContext->GetDocShell(); + if (!docshell) { + return; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = docshell->GetWindow(); + if (!window) return; + + RefPtr<DataTransfer> dataTransfer = + new DataTransfer(window, eDragStart, false, -1); + auto protectDataTransfer = MakeScopeExit([&] { + if (dataTransfer) { + dataTransfer->Disconnect(); + } + }); + + RefPtr<Selection> selection; + RefPtr<RemoteDragStartData> remoteDragStartData; + nsCOMPtr<nsIContent> eventContent, targetContent; + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIContentSecurityPolicy> csp; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + bool allowEmptyDataTransfer = false; + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(eventContent)); + if (eventContent) { + // If the content is a text node in a password field, we shouldn't + // allow to drag its raw text. Note that we've supported drag from + // password fields but dragging data was masked text. So, it doesn't + // make sense anyway. + if (eventContent->IsText() && eventContent->HasFlag(NS_MAYBE_MASKED)) { + // However, it makes sense to allow to drag selected password text + // when copying selected password is allowed because users may want + // to use drag and drop rather than copy and paste when web apps + // request to input password twice for conforming new password but + // they used password generator. + TextEditor* textEditor = + nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation( + eventContent); + if (!textEditor || !textEditor->IsCopyToClipboardAllowed()) { + StopTrackingDragGesture(true); + return; + } + } + DetermineDragTargetAndDefaultData( + window, eventContent, dataTransfer, &allowEmptyDataTransfer, + getter_AddRefs(selection), getter_AddRefs(remoteDragStartData), + getter_AddRefs(targetContent), getter_AddRefs(principal), + getter_AddRefs(csp), getter_AddRefs(cookieJarSettings)); + } + + // Stop tracking the drag gesture now. This should stop us from + // reentering GenerateDragGesture inside DOM event processing. + // Pass false to avoid clearing the child process state since a real + // drag should be starting. + StopTrackingDragGesture(false); + + if (!targetContent) return; + + // Use our targetContent, now that we've determined it, as the + // parent object of the DataTransfer. + nsCOMPtr<nsIContent> parentContent = + targetContent->FindFirstNonChromeOnlyAccessContent(); + dataTransfer->SetParentObject(parentContent); + + sLastDragOverFrame = nullptr; + nsCOMPtr<nsIWidget> widget = mCurrentTarget->GetNearestWidget(); + + // get the widget from the target frame + WidgetDragEvent startEvent(aEvent->IsTrusted(), eDragStart, widget); + startEvent.mFlags.mIsSynthesizedForTests = + aEvent->mFlags.mIsSynthesizedForTests; + FillInEventFromGestureDown(&startEvent); + + startEvent.mDataTransfer = dataTransfer; + if (aEvent->AsMouseEvent()) { + startEvent.mInputSource = aEvent->AsMouseEvent()->mInputSource; + } else if (aEvent->AsTouchEvent()) { + startEvent.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH; + } else { + MOZ_ASSERT(false); + } + + // Dispatch to the DOM. By setting mCurrentTarget we are faking + // out the ESM and telling 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. + + // Hold onto old target content through the event and reset after. + nsCOMPtr<nsIContent> targetBeforeEvent = mCurrentTargetContent; + + // Set the current target to the content for the mouse down + mCurrentTargetContent = targetContent; + + // Dispatch the dragstart event to the DOM. + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(targetContent, aPresContext, &startEvent, nullptr, + &status); + + WidgetDragEvent* event = &startEvent; + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + // Emit observer event to allow addons to modify the DataTransfer + // object. + if (observerService) { + observerService->NotifyObservers(dataTransfer, "on-datatransfer-available", + nullptr); + } + + if (status != nsEventStatus_eConsumeNoDefault) { + bool dragStarted = DoDefaultDragStart(aPresContext, event, dataTransfer, + allowEmptyDataTransfer, targetContent, + selection, remoteDragStartData, + principal, csp, cookieJarSettings); + if (dragStarted) { + sActiveESM = nullptr; + MaybeFirePointerCancel(aEvent); + aEvent->StopPropagation(); + } + } + + // Reset mCurretTargetContent to what it was + mCurrentTargetContent = targetBeforeEvent; + + // Now flush all pending notifications, for better responsiveness + // while dragging. + FlushLayout(aPresContext); +} // GenerateDragGesture + +void EventStateManager::DetermineDragTargetAndDefaultData( + nsPIDOMWindowOuter* aWindow, nsIContent* aSelectionTarget, + DataTransfer* aDataTransfer, bool* aAllowEmptyDataTransfer, + Selection** aSelection, RemoteDragStartData** aRemoteDragStartData, + nsIContent** aTargetNode, nsIPrincipal** aPrincipal, + nsIContentSecurityPolicy** aCsp, + nsICookieJarSettings** aCookieJarSettings) { + *aTargetNode = nullptr; + *aAllowEmptyDataTransfer = false; + nsCOMPtr<nsIContent> dragDataNode; + + nsIContent* editingElement = aSelectionTarget->IsEditable() + ? aSelectionTarget->GetEditingHost() + : nullptr; + + // In chrome, only allow dragging inside editable areas. + bool isChromeContext = !aWindow->GetBrowsingContext()->IsContent(); + if (isChromeContext && !editingElement) { + if (mGestureDownDragStartData) { + // A child process started a drag so use any data it assigned for the dnd + // session. + mGestureDownDragStartData->AddInitialDnDDataTo(aDataTransfer, aPrincipal, + aCsp, aCookieJarSettings); + mGestureDownDragStartData.forget(aRemoteDragStartData); + *aAllowEmptyDataTransfer = true; + } + } else { + mGestureDownDragStartData = nullptr; + + // GetDragData determines if a selection, link or image in the content + // should be dragged, and places the data associated with the drag in the + // data transfer. + // mGestureDownContent is the node where the mousedown event for the drag + // occurred, and aSelectionTarget is the node to use when a selection is + // used + bool canDrag; + bool wasAlt = (mGestureModifiers & MODIFIER_ALT) != 0; + nsresult rv = nsContentAreaDragDrop::GetDragData( + aWindow, mGestureDownContent, aSelectionTarget, wasAlt, aDataTransfer, + &canDrag, aSelection, getter_AddRefs(dragDataNode), aPrincipal, aCsp, + aCookieJarSettings); + if (NS_FAILED(rv) || !canDrag) { + return; + } + } + + // if GetDragData returned a node, use that as the node being dragged. + // Otherwise, if a selection is being dragged, use the node within the + // selection that was dragged. Otherwise, just use the mousedown target. + nsIContent* dragContent = mGestureDownContent; + if (dragDataNode) + dragContent = dragDataNode; + else if (*aSelection) + dragContent = aSelectionTarget; + + nsIContent* originalDragContent = dragContent; + + // If a selection isn't being dragged, look for an ancestor with the + // draggable property set. If one is found, use that as the target of the + // drag instead of the node that was clicked on. If a draggable node wasn't + // found, just use the clicked node. + if (!*aSelection) { + while (dragContent) { + if (auto htmlElement = nsGenericHTMLElement::FromNode(dragContent)) { + if (htmlElement->Draggable()) { + // We let draggable elements to trigger dnd even if there is no data + // in the DataTransfer. + *aAllowEmptyDataTransfer = true; + break; + } + } else { + if (dragContent->IsXULElement()) { + // All XUL elements are draggable, so if a XUL element is + // encountered, stop looking for draggable nodes and just use the + // original clicked node instead. + // XXXndeakin + // In the future, we will want to improve this so that XUL has a + // better way to specify whether something is draggable than just + // on/off. + dragContent = mGestureDownContent; + break; + } + // otherwise, it's not an HTML or XUL element, so just keep looking + } + dragContent = dragContent->GetFlattenedTreeParent(); + } + } + + // if no node in the hierarchy was found to drag, but the GetDragData method + // returned a node, use that returned node. Otherwise, nothing is draggable. + if (!dragContent && dragDataNode) dragContent = dragDataNode; + + if (dragContent) { + // if an ancestor node was used instead, clear the drag data + // XXXndeakin rework this a bit. Find a way to just not call GetDragData if + // we don't need to. + if (dragContent != originalDragContent) aDataTransfer->ClearAll(); + *aTargetNode = dragContent; + NS_ADDREF(*aTargetNode); + } +} + +bool EventStateManager::DoDefaultDragStart( + nsPresContext* aPresContext, WidgetDragEvent* aDragEvent, + DataTransfer* aDataTransfer, bool aAllowEmptyDataTransfer, + nsIContent* aDragTarget, Selection* aSelection, + RemoteDragStartData* aDragStartData, nsIPrincipal* aPrincipal, + nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings) { + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (!dragService) return false; + + // Default handling for the dragstart event. + // + // First, check if a drag session already exists. This means that the drag + // service was called directly within a draggesture handler. In this case, + // don't do anything more, as it is assumed that the handler is managing + // drag and drop manually. Make sure to return true to indicate that a drag + // began. However, if we're handling drag session for synthesized events, + // we need to initialize some information of the session. Therefore, we + // need to keep going for synthesized case. + nsCOMPtr<nsIDragSession> dragSession; + dragService->GetCurrentSession(getter_AddRefs(dragSession)); + if (dragSession && !dragSession->IsSynthesizedForTests()) { + return true; + } + + // No drag session is currently active, so check if a handler added + // any items to be dragged. If not, there isn't anything to drag. + uint32_t count = 0; + if (aDataTransfer) { + count = aDataTransfer->MozItemCount(); + } + if (!aAllowEmptyDataTransfer && !count) { + return false; + } + + // Get the target being dragged, which may not be the same as the + // target of the mouse event. If one wasn't set in the + // aDataTransfer during the event handler, just use the original + // target instead. + nsCOMPtr<nsIContent> dragTarget = aDataTransfer->GetDragTarget(); + if (!dragTarget) { + dragTarget = aDragTarget; + if (!dragTarget) { + return false; + } + } + + // check which drag effect should initially be used. If the effect was not + // set, just use all actions, otherwise Windows won't allow a drop. + uint32_t action = aDataTransfer->EffectAllowedInt(); + if (action == nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) { + action = nsIDragService::DRAGDROP_ACTION_COPY | + nsIDragService::DRAGDROP_ACTION_MOVE | + nsIDragService::DRAGDROP_ACTION_LINK; + } + + // get any custom drag image that was set + int32_t imageX, imageY; + RefPtr<Element> dragImage = aDataTransfer->GetDragImage(&imageX, &imageY); + + nsCOMPtr<nsIArray> transArray = aDataTransfer->GetTransferables(dragTarget); + if (!transArray) { + return false; + } + + RefPtr<DataTransfer> dataTransfer; + if (!dragSession) { + // After this function returns, the DataTransfer will be cleared so it + // appears empty to content. We need to pass a DataTransfer into the Drag + // Session, so we need to make a copy. + aDataTransfer->Clone(aDragTarget, eDrop, aDataTransfer->MozUserCancelled(), + false, getter_AddRefs(dataTransfer)); + + // Copy over the drop effect, as Clone doesn't copy it for us. + dataTransfer->SetDropEffectInt(aDataTransfer->DropEffectInt()); + } else { + MOZ_ASSERT(dragSession->IsSynthesizedForTests()); + MOZ_ASSERT(aDragEvent->mFlags.mIsSynthesizedForTests); + // If we're initializing synthesized drag session, we should use given + // DataTransfer as is because it'll be used with following drag events + // in any tests, therefore it should be set to nsIDragSession.dataTransfer + // because it and DragEvent.dataTransfer should be same instance. + dataTransfer = aDataTransfer; + } + + // XXXndeakin don't really want to create a new drag DOM event + // here, but we need something to pass to the InvokeDragSession + // methods. + RefPtr<DragEvent> event = + NS_NewDOMDragEvent(dragTarget, aPresContext, aDragEvent); + + // Use InvokeDragSessionWithSelection if a selection is being dragged, + // such that the image can be generated from the selected text. However, + // use InvokeDragSessionWithImage if a custom image was set or something + // other than a selection is being dragged. + if (!dragImage && aSelection) { + dragService->InvokeDragSessionWithSelection(aSelection, aPrincipal, aCsp, + aCookieJarSettings, transArray, + action, event, dataTransfer); + } else if (aDragStartData) { + MOZ_ASSERT(XRE_IsParentProcess()); + dragService->InvokeDragSessionWithRemoteImage( + dragTarget, aPrincipal, aCsp, aCookieJarSettings, transArray, action, + aDragStartData, event, dataTransfer); + } else { + dragService->InvokeDragSessionWithImage( + dragTarget, aPrincipal, aCsp, aCookieJarSettings, transArray, action, + dragImage, imageX, imageY, event, dataTransfer); + } + + return true; +} + +void EventStateManager::ChangeZoom(bool aIncrease) { + // Send the zoom change to the top level browser so it will be handled by the + // front end in the same way as other zoom actions. + nsIDocShell* docShell = mDocument->GetDocShell(); + if (!docShell) { + return; + } + + BrowsingContext* bc = docShell->GetBrowsingContext(); + if (!bc) { + return; + } + + if (XRE_IsParentProcess()) { + bc->Canonical()->DispatchWheelZoomChange(aIncrease); + } else if (BrowserChild* child = BrowserChild::GetFrom(docShell)) { + child->SendWheelZoomChange(aIncrease); + } +} + +void EventStateManager::DoScrollHistory(int32_t direction) { + nsCOMPtr<nsISupports> pcContainer(mPresContext->GetContainerWeak()); + if (pcContainer) { + nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(pcContainer)); + if (webNav) { + // positive direction to go back one step, nonpositive to go forward + // This is doing user-initiated history traversal, hence we want + // to require that history entries we navigate to have user interaction. + if (direction > 0) + webNav->GoBack(StaticPrefs::browser_navigation_requireUserInteraction(), + true); + else + webNav->GoForward( + StaticPrefs::browser_navigation_requireUserInteraction(), true); + } + } +} + +void EventStateManager::DoScrollZoom(nsIFrame* aTargetFrame, + int32_t adjustment) { + // Exclude content in chrome docshells. + nsIContent* content = aTargetFrame->GetContent(); + if (content && !nsContentUtils::IsInChromeDocshell(content->OwnerDoc())) { + // Positive adjustment to decrease zoom, negative to increase + const bool increase = adjustment <= 0; + EnsureDocument(mPresContext); + ChangeZoom(increase); + } +} + +static nsIFrame* GetParentFrameToScroll(nsIFrame* aFrame) { + if (!aFrame) return nullptr; + + if (aFrame->StyleDisplay()->mPosition == StylePositionProperty::Fixed && + nsLayoutUtils::IsReallyFixedPos(aFrame)) + return aFrame->PresContext()->GetPresShell()->GetRootScrollFrame(); + + return aFrame->GetParent(); +} + +void EventStateManager::DispatchLegacyMouseScrollEvents( + nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent, nsEventStatus* aStatus) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aStatus); + + if (!aTargetFrame || *aStatus == nsEventStatus_eConsumeNoDefault) { + return; + } + + // Ignore mouse wheel transaction for computing legacy mouse wheel + // events' delta value. + // DOM event's delta vales are computed from CSS pixels. + auto scrollAmountInCSSPixels = + CSSIntSize::FromAppUnitsRounded(aEvent->mScrollAmount); + + // XXX We don't deal with fractional amount in legacy event, though the + // default action handler (DoScrollText()) deals with it. + // If we implemented such strict computation, we would need additional + // accumulated delta values. It would made the code more complicated. + // And also it would computes different delta values from older version. + // It doesn't make sense to implement such code for legacy events and + // rare cases. + int32_t scrollDeltaX, scrollDeltaY, pixelDeltaX, pixelDeltaY; + switch (aEvent->mDeltaMode) { + case WheelEvent_Binding::DOM_DELTA_PAGE: + scrollDeltaX = !aEvent->mLineOrPageDeltaX + ? 0 + : (aEvent->mLineOrPageDeltaX > 0 + ? UIEvent_Binding::SCROLL_PAGE_DOWN + : UIEvent_Binding::SCROLL_PAGE_UP); + scrollDeltaY = !aEvent->mLineOrPageDeltaY + ? 0 + : (aEvent->mLineOrPageDeltaY > 0 + ? UIEvent_Binding::SCROLL_PAGE_DOWN + : UIEvent_Binding::SCROLL_PAGE_UP); + pixelDeltaX = RoundDown(aEvent->mDeltaX * scrollAmountInCSSPixels.width); + pixelDeltaY = RoundDown(aEvent->mDeltaY * scrollAmountInCSSPixels.height); + break; + + case WheelEvent_Binding::DOM_DELTA_LINE: + scrollDeltaX = aEvent->mLineOrPageDeltaX; + scrollDeltaY = aEvent->mLineOrPageDeltaY; + pixelDeltaX = RoundDown(aEvent->mDeltaX * scrollAmountInCSSPixels.width); + pixelDeltaY = RoundDown(aEvent->mDeltaY * scrollAmountInCSSPixels.height); + break; + + case WheelEvent_Binding::DOM_DELTA_PIXEL: + scrollDeltaX = aEvent->mLineOrPageDeltaX; + scrollDeltaY = aEvent->mLineOrPageDeltaY; + pixelDeltaX = RoundDown(aEvent->mDeltaX); + pixelDeltaY = RoundDown(aEvent->mDeltaY); + break; + + default: + MOZ_CRASH("Invalid deltaMode value comes"); + } + + // Send the legacy events in following order: + // 1. Vertical scroll + // 2. Vertical pixel scroll (even if #1 isn't consumed) + // 3. Horizontal scroll (even if #1 and/or #2 are consumed) + // 4. Horizontal pixel scroll (even if #3 isn't consumed) + + AutoWeakFrame targetFrame(aTargetFrame); + + MOZ_ASSERT(*aStatus != nsEventStatus_eConsumeNoDefault && + !aEvent->DefaultPrevented(), + "If you make legacy events dispatched for default prevented wheel " + "event, you need to initialize stateX and stateY"); + EventState stateX, stateY; + if (scrollDeltaY) { + SendLineScrollEvent(aTargetFrame, aEvent, stateY, scrollDeltaY, + DELTA_DIRECTION_Y); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (pixelDeltaY) { + SendPixelScrollEvent(aTargetFrame, aEvent, stateY, pixelDeltaY, + DELTA_DIRECTION_Y); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (scrollDeltaX) { + SendLineScrollEvent(aTargetFrame, aEvent, stateX, scrollDeltaX, + DELTA_DIRECTION_X); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (pixelDeltaX) { + SendPixelScrollEvent(aTargetFrame, aEvent, stateX, pixelDeltaX, + DELTA_DIRECTION_X); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (stateY.mDefaultPrevented) { + *aStatus = nsEventStatus_eConsumeNoDefault; + aEvent->PreventDefault(!stateY.mDefaultPreventedByContent); + } + + if (stateX.mDefaultPrevented) { + *aStatus = nsEventStatus_eConsumeNoDefault; + aEvent->PreventDefault(!stateX.mDefaultPreventedByContent); + } +} + +void EventStateManager::SendLineScrollEvent(nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent, + EventState& aState, int32_t aDelta, + DeltaDirection aDeltaDirection) { + nsCOMPtr<nsIContent> targetContent = aTargetFrame->GetContent(); + if (!targetContent) { + targetContent = GetFocusedElement(); + if (!targetContent) { + return; + } + } + + while (targetContent->IsText()) { + targetContent = targetContent->GetFlattenedTreeParent(); + } + + WidgetMouseScrollEvent event(aEvent->IsTrusted(), + eLegacyMouseLineOrPageScroll, aEvent->mWidget); + event.mFlags.mDefaultPrevented = aState.mDefaultPrevented; + event.mFlags.mDefaultPreventedByContent = aState.mDefaultPreventedByContent; + event.mRefPoint = aEvent->mRefPoint; + event.mTimeStamp = aEvent->mTimeStamp; + event.mModifiers = aEvent->mModifiers; + event.mButtons = aEvent->mButtons; + event.mIsHorizontal = (aDeltaDirection == DELTA_DIRECTION_X); + event.mDelta = aDelta; + event.mInputSource = aEvent->mInputSource; + + RefPtr<nsPresContext> presContext = aTargetFrame->PresContext(); + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(targetContent, presContext, &event, nullptr, + &status); + aState.mDefaultPrevented = + event.DefaultPrevented() || status == nsEventStatus_eConsumeNoDefault; + aState.mDefaultPreventedByContent = event.DefaultPreventedByContent(); +} + +void EventStateManager::SendPixelScrollEvent(nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent, + EventState& aState, + int32_t aPixelDelta, + DeltaDirection aDeltaDirection) { + nsCOMPtr<nsIContent> targetContent = aTargetFrame->GetContent(); + if (!targetContent) { + targetContent = GetFocusedElement(); + if (!targetContent) { + return; + } + } + + while (targetContent->IsText()) { + targetContent = targetContent->GetFlattenedTreeParent(); + } + + WidgetMouseScrollEvent event(aEvent->IsTrusted(), eLegacyMousePixelScroll, + aEvent->mWidget); + event.mFlags.mDefaultPrevented = aState.mDefaultPrevented; + event.mFlags.mDefaultPreventedByContent = aState.mDefaultPreventedByContent; + event.mRefPoint = aEvent->mRefPoint; + event.mTimeStamp = aEvent->mTimeStamp; + event.mModifiers = aEvent->mModifiers; + event.mButtons = aEvent->mButtons; + event.mIsHorizontal = (aDeltaDirection == DELTA_DIRECTION_X); + event.mDelta = aPixelDelta; + event.mInputSource = aEvent->mInputSource; + + RefPtr<nsPresContext> presContext = aTargetFrame->PresContext(); + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(targetContent, presContext, &event, nullptr, + &status); + aState.mDefaultPrevented = + event.DefaultPrevented() || status == nsEventStatus_eConsumeNoDefault; + aState.mDefaultPreventedByContent = event.DefaultPreventedByContent(); +} + +nsIFrame* EventStateManager::ComputeScrollTargetAndMayAdjustWheelEvent( + nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent, + ComputeScrollTargetOptions aOptions) { + return ComputeScrollTargetAndMayAdjustWheelEvent( + aTargetFrame, aEvent->mDeltaX, aEvent->mDeltaY, aEvent, aOptions); +} + +// Overload ComputeScrollTargetAndMayAdjustWheelEvent method to allow passing +// "test" dx and dy when looking for which scrollbarmediators to activate when +// two finger down on trackpad and before any actual motion +nsIFrame* EventStateManager::ComputeScrollTargetAndMayAdjustWheelEvent( + nsIFrame* aTargetFrame, double aDirectionX, double aDirectionY, + WidgetWheelEvent* aEvent, ComputeScrollTargetOptions aOptions) { + bool isAutoDir = false; + bool honoursRoot = false; + if (MAY_BE_ADJUSTED_BY_AUTO_DIR & aOptions) { + // If the scroll is respected as auto-dir, aDirection* should always be + // equivalent to the event's delta vlaues(Currently, there are only one case + // where aDirection*s have different values from the widget wheel event's + // original delta values and the only case isn't auto-dir, see + // ScrollbarsForWheel::TemporarilyActivateAllPossibleScrollTargets). + MOZ_ASSERT(aDirectionX == aEvent->mDeltaX && + aDirectionY == aEvent->mDeltaY); + + WheelDeltaAdjustmentStrategy strategy = + GetWheelDeltaAdjustmentStrategy(*aEvent); + switch (strategy) { + case WheelDeltaAdjustmentStrategy::eAutoDir: + isAutoDir = true; + honoursRoot = false; + break; + case WheelDeltaAdjustmentStrategy::eAutoDirWithRootHonour: + isAutoDir = true; + honoursRoot = true; + break; + default: + break; + } + } + + if (aOptions & PREFER_MOUSE_WHEEL_TRANSACTION) { + // If the user recently scrolled with the mousewheel, then they probably + // want to scroll the same view as before instead of the view under the + // cursor. WheelTransaction tracks the frame currently being + // scrolled with the mousewheel. We consider the transaction ended when the + // mouse moves more than "mousewheel.transaction.ignoremovedelay" + // milliseconds after the last scroll operation, or any time the mouse moves + // out of the frame, or when more than "mousewheel.transaction.timeout" + // milliseconds have passed after the last operation, even if the mouse + // hasn't moved. + nsIFrame* lastScrollFrame = WheelTransaction::GetScrollTargetFrame(); + if (lastScrollFrame) { + nsIScrollableFrame* scrollableFrame = + lastScrollFrame->GetScrollTargetFrame(); + if (scrollableFrame) { + nsIFrame* frameToScroll = do_QueryFrame(scrollableFrame); + MOZ_ASSERT(frameToScroll); + if (isAutoDir) { + ESMAutoDirWheelDeltaAdjuster adjuster(*aEvent, *lastScrollFrame, + honoursRoot); + // Note that calling this function will not always cause the delta to + // be adjusted, it only adjusts the delta when it should, because + // Adjust() internally calls ShouldBeAdjusted() before making + // adjustment. + adjuster.Adjust(); + } + return frameToScroll; + } + } + } + + // If the event doesn't cause scroll actually, we cannot find scroll target + // because we check if the event can cause scroll actually on each found + // scrollable frame. + if (!aDirectionX && !aDirectionY) { + return nullptr; + } + + bool checkIfScrollableX; + bool checkIfScrollableY; + if (isAutoDir) { + // Always check the frame's scrollability in both the two directions for an + // auto-dir scroll. That is, for an auto-dir scroll, + // PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_X_AXIS and + // PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_Y_AXIS should be ignored. + checkIfScrollableX = true; + checkIfScrollableY = true; + } else { + checkIfScrollableX = + aDirectionX && + (aOptions & PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_X_AXIS); + checkIfScrollableY = + aDirectionY && + (aOptions & PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_Y_AXIS); + } + + nsIFrame* scrollFrame = !(aOptions & START_FROM_PARENT) + ? aTargetFrame + : GetParentFrameToScroll(aTargetFrame); + for (; scrollFrame; scrollFrame = GetParentFrameToScroll(scrollFrame)) { + // Check whether the frame wants to provide us with a scrollable view. + nsIScrollableFrame* scrollableFrame = scrollFrame->GetScrollTargetFrame(); + if (!scrollableFrame) { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(scrollFrame); + if (menuPopupFrame) { + return nullptr; + } + continue; + } + + nsIFrame* frameToScroll = do_QueryFrame(scrollableFrame); + MOZ_ASSERT(frameToScroll); + + if (!checkIfScrollableX && !checkIfScrollableY) { + return frameToScroll; + } + + // If the frame disregards the direction the user is trying to scroll, then + // it should just bubbles the scroll event up to its parental scroll frame + + Maybe<layers::ScrollDirection> disregardedDirection = + WheelHandlingUtils::GetDisregardedWheelScrollDirection(scrollFrame); + if (disregardedDirection) { + switch (disregardedDirection.ref()) { + case layers::ScrollDirection::eHorizontal: + if (checkIfScrollableX) { + continue; + } + break; + case layers::ScrollDirection::eVertical: + if (checkIfScrollableY) { + continue; + } + break; + } + } + + layers::ScrollDirections directions = + scrollableFrame->GetAvailableScrollingDirectionsForUserInputEvents(); + if ((!(directions.contains(layers::ScrollDirection::eVertical)) && + !(directions.contains(layers::ScrollDirection::eHorizontal))) || + (checkIfScrollableY && !checkIfScrollableX && + !(directions.contains(layers::ScrollDirection::eVertical))) || + (checkIfScrollableX && !checkIfScrollableY && + !(directions.contains(layers::ScrollDirection::eHorizontal)))) { + continue; + } + + // Computes whether the currently checked frame is scrollable by this wheel + // event. + bool canScroll = false; + if (isAutoDir) { + ESMAutoDirWheelDeltaAdjuster adjuster(*aEvent, *scrollFrame, honoursRoot); + if (adjuster.ShouldBeAdjusted()) { + adjuster.Adjust(); + canScroll = true; + } else if (WheelHandlingUtils::CanScrollOn(scrollableFrame, aDirectionX, + aDirectionY)) { + canScroll = true; + } + } else if (WheelHandlingUtils::CanScrollOn(scrollableFrame, aDirectionX, + aDirectionY)) { + canScroll = true; + } + + if (canScroll) { + return frameToScroll; + } + + // Where we are at is the block ending in a for loop. + // The current frame has been checked to be unscrollable by this wheel + // event, continue the loop to check its parent, if any. + } + + nsIFrame* newFrame = nsLayoutUtils::GetCrossDocParentFrameInProcess( + aTargetFrame->PresShell()->GetRootFrame()); + aOptions = + static_cast<ComputeScrollTargetOptions>(aOptions & ~START_FROM_PARENT); + if (!newFrame) { + return nullptr; + } + return ComputeScrollTargetAndMayAdjustWheelEvent(newFrame, aEvent, aOptions); +} + +nsSize EventStateManager::GetScrollAmount( + nsPresContext* aPresContext, WidgetWheelEvent* aEvent, + nsIScrollableFrame* aScrollableFrame) { + MOZ_ASSERT(aPresContext); + MOZ_ASSERT(aEvent); + + const bool isPage = aEvent->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PAGE; + if (!aScrollableFrame) { + // If there is no scrollable frame, we should use root, see below. + aScrollableFrame = + aPresContext->PresShell()->GetRootScrollFrameAsScrollable(); + } + + if (aScrollableFrame) { + return isPage ? aScrollableFrame->GetPageScrollAmount() + : aScrollableFrame->GetLineScrollAmount(); + } + + // If there is no scrollable frame and page scrolling, use viewport size. + if (isPage) { + return aPresContext->GetVisibleArea().Size(); + } + + // Otherwise use root frame's font metrics. + // + // FIXME(emilio): Should this use the root element's style frame? The root + // frame will always have the initial font. Then again it should never matter + // for content, we should always have a root scrollable frame in html + // documents. + nsIFrame* rootFrame = aPresContext->PresShell()->GetRootFrame(); + if (!rootFrame) { + return nsSize(0, 0); + } + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetInflatedFontMetricsForFrame(rootFrame); + NS_ENSURE_TRUE(fm, nsSize(0, 0)); + return nsSize(fm->AveCharWidth(), fm->MaxHeight()); +} + +void EventStateManager::DoScrollText(nsIScrollableFrame* aScrollableFrame, + WidgetWheelEvent* aEvent) { + MOZ_ASSERT(aScrollableFrame); + MOZ_ASSERT(aEvent); + + nsIFrame* scrollFrame = do_QueryFrame(aScrollableFrame); + MOZ_ASSERT(scrollFrame); + + AutoWeakFrame scrollFrameWeak(scrollFrame); + AutoWeakFrame eventFrameWeak(mCurrentTarget); + if (!WheelTransaction::WillHandleDefaultAction(aEvent, scrollFrameWeak, + eventFrameWeak)) { + return; + } + + // Default action's actual scroll amount should be computed from device + // pixels. + nsPresContext* pc = scrollFrame->PresContext(); + nsSize scrollAmount = GetScrollAmount(pc, aEvent, aScrollableFrame); + nsIntSize scrollAmountInDevPixels( + pc->AppUnitsToDevPixels(scrollAmount.width), + pc->AppUnitsToDevPixels(scrollAmount.height)); + nsIntPoint actualDevPixelScrollAmount = + DeltaAccumulator::GetInstance()->ComputeScrollAmountForDefaultAction( + aEvent, scrollAmountInDevPixels); + + // Don't scroll around the axis whose overflow style is hidden. + ScrollStyles overflowStyle = aScrollableFrame->GetScrollStyles(); + if (overflowStyle.mHorizontal == StyleOverflow::Hidden) { + actualDevPixelScrollAmount.x = 0; + } + if (overflowStyle.mVertical == StyleOverflow::Hidden) { + actualDevPixelScrollAmount.y = 0; + } + + ScrollSnapFlags snapFlags = ScrollSnapFlags::Disabled; + mozilla::ScrollOrigin origin = mozilla::ScrollOrigin::NotSpecified; + switch (aEvent->mDeltaMode) { + case WheelEvent_Binding::DOM_DELTA_LINE: + origin = mozilla::ScrollOrigin::MouseWheel; + snapFlags = ScrollSnapFlags::IntendedDirection; + break; + case WheelEvent_Binding::DOM_DELTA_PAGE: + origin = mozilla::ScrollOrigin::Pages; + snapFlags = ScrollSnapFlags::IntendedDirection | + ScrollSnapFlags::IntendedEndPosition; + break; + case WheelEvent_Binding::DOM_DELTA_PIXEL: + origin = mozilla::ScrollOrigin::Pixels; + break; + default: + MOZ_CRASH("Invalid deltaMode value comes"); + } + + // We shouldn't scroll more one page at once except when over one page scroll + // is allowed for the event. + nsSize pageSize = aScrollableFrame->GetPageScrollAmount(); + nsIntSize devPixelPageSize(pc->AppUnitsToDevPixels(pageSize.width), + pc->AppUnitsToDevPixels(pageSize.height)); + if (!WheelPrefs::GetInstance()->IsOverOnePageScrollAllowedX(aEvent) && + DeprecatedAbs(actualDevPixelScrollAmount.x.value) > + devPixelPageSize.width) { + actualDevPixelScrollAmount.x = (actualDevPixelScrollAmount.x >= 0) + ? devPixelPageSize.width + : -devPixelPageSize.width; + } + + if (!WheelPrefs::GetInstance()->IsOverOnePageScrollAllowedY(aEvent) && + DeprecatedAbs(actualDevPixelScrollAmount.y.value) > + devPixelPageSize.height) { + actualDevPixelScrollAmount.y = (actualDevPixelScrollAmount.y >= 0) + ? devPixelPageSize.height + : -devPixelPageSize.height; + } + + bool isDeltaModePixel = + (aEvent->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL); + + ScrollMode mode; + switch (aEvent->mScrollType) { + case WidgetWheelEvent::SCROLL_DEFAULT: + if (isDeltaModePixel) { + mode = ScrollMode::Normal; + } else if (aEvent->mFlags.mHandledByAPZ) { + mode = ScrollMode::SmoothMsd; + } else { + mode = ScrollMode::Smooth; + } + break; + case WidgetWheelEvent::SCROLL_SYNCHRONOUSLY: + mode = ScrollMode::Instant; + break; + case WidgetWheelEvent::SCROLL_ASYNCHRONOUSLY: + mode = ScrollMode::Normal; + break; + case WidgetWheelEvent::SCROLL_SMOOTHLY: + mode = ScrollMode::Smooth; + break; + default: + MOZ_CRASH("Invalid mScrollType value comes"); + } + + nsIScrollableFrame::ScrollMomentum momentum = + aEvent->mIsMomentum ? nsIScrollableFrame::SYNTHESIZED_MOMENTUM_EVENT + : nsIScrollableFrame::NOT_MOMENTUM; + + nsIntPoint overflow; + aScrollableFrame->ScrollBy(actualDevPixelScrollAmount, + ScrollUnit::DEVICE_PIXELS, mode, &overflow, origin, + momentum, snapFlags); + + if (!scrollFrameWeak.IsAlive()) { + // If the scroll causes changing the layout, we can think that the event + // has been completely consumed by the content. Then, users probably don't + // want additional action. + aEvent->mOverflowDeltaX = aEvent->mOverflowDeltaY = 0; + } else if (isDeltaModePixel) { + aEvent->mOverflowDeltaX = overflow.x; + aEvent->mOverflowDeltaY = overflow.y; + } else { + aEvent->mOverflowDeltaX = + static_cast<double>(overflow.x) / scrollAmountInDevPixels.width; + aEvent->mOverflowDeltaY = + static_cast<double>(overflow.y) / scrollAmountInDevPixels.height; + } + + // If CSS overflow properties caused not to scroll, the overflowDelta* values + // should be same as delta* values since they may be used as gesture event by + // widget. However, if there is another scrollable element in the ancestor + // along the axis, probably users don't want the operation to cause + // additional action such as moving history. In such case, overflowDelta + // values should stay zero. + if (scrollFrameWeak.IsAlive()) { + if (aEvent->mDeltaX && overflowStyle.mHorizontal == StyleOverflow::Hidden && + !ComputeScrollTargetAndMayAdjustWheelEvent( + scrollFrame, aEvent, + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_X_AXIS_WITH_AUTO_DIR)) { + aEvent->mOverflowDeltaX = aEvent->mDeltaX; + } + if (aEvent->mDeltaY && overflowStyle.mVertical == StyleOverflow::Hidden && + !ComputeScrollTargetAndMayAdjustWheelEvent( + scrollFrame, aEvent, + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_Y_AXIS_WITH_AUTO_DIR)) { + aEvent->mOverflowDeltaY = aEvent->mDeltaY; + } + } + + NS_ASSERTION( + aEvent->mOverflowDeltaX == 0 || + (aEvent->mOverflowDeltaX > 0) == (aEvent->mDeltaX > 0), + "The sign of mOverflowDeltaX is different from the scroll direction"); + NS_ASSERTION( + aEvent->mOverflowDeltaY == 0 || + (aEvent->mOverflowDeltaY > 0) == (aEvent->mDeltaY > 0), + "The sign of mOverflowDeltaY is different from the scroll direction"); + + WheelPrefs::GetInstance()->CancelApplyingUserPrefsFromOverflowDelta(aEvent); +} + +void EventStateManager::DecideGestureEvent(WidgetGestureNotifyEvent* aEvent, + nsIFrame* targetFrame) { + NS_ASSERTION(aEvent->mMessage == eGestureNotify, + "DecideGestureEvent called with a non-gesture event"); + + /* Check the ancestor tree to decide if any frame is willing* to receive + * a MozPixelScroll event. If that's the case, the current touch gesture + * will be used as a pan gesture; otherwise it will be a regular + * mousedown/mousemove/click event. + * + * *willing: determine if it makes sense to pan the element using scroll + * events: + * - For web content: if there are any visible scrollbars on the touch point + * - For XUL: if it's an scrollable element that can currently scroll in some + * direction. + * + * Note: we'll have to one-off various cases to ensure a good usable behavior + */ + WidgetGestureNotifyEvent::PanDirection panDirection = + WidgetGestureNotifyEvent::ePanNone; + bool displayPanFeedback = false; + for (nsIFrame* current = targetFrame; current; + current = nsLayoutUtils::GetCrossDocParentFrame(current)) { + // e10s - mark remote content as pannable. This is a work around since + // we don't have access to remote frame scroll info here. Apz data may + // assist is solving this. + if (current && IsTopLevelRemoteTarget(current->GetContent())) { + panDirection = WidgetGestureNotifyEvent::ePanBoth; + // We don't know when we reach bounds, so just disable feedback for now. + displayPanFeedback = false; + break; + } + + LayoutFrameType currentFrameType = current->Type(); + + // Scrollbars should always be draggable + if (currentFrameType == LayoutFrameType::Scrollbar) { + panDirection = WidgetGestureNotifyEvent::ePanNone; + break; + } + + // Special check for trees + if (nsTreeBodyFrame* treeFrame = do_QueryFrame(current)) { + if (treeFrame->GetHorizontalOverflow()) { + panDirection = WidgetGestureNotifyEvent::ePanHorizontal; + } + if (treeFrame->GetVerticalOverflow()) { + panDirection = WidgetGestureNotifyEvent::ePanVertical; + } + break; + } + + if (nsIScrollableFrame* scrollableFrame = do_QueryFrame(current)) { + layers::ScrollDirections scrollbarVisibility = + scrollableFrame->GetScrollbarVisibility(); + + // Check if we have visible scrollbars + if (scrollbarVisibility.contains(layers::ScrollDirection::eVertical)) { + panDirection = WidgetGestureNotifyEvent::ePanVertical; + displayPanFeedback = true; + break; + } + + if (scrollbarVisibility.contains(layers::ScrollDirection::eHorizontal)) { + panDirection = WidgetGestureNotifyEvent::ePanHorizontal; + displayPanFeedback = true; + } + } // scrollableFrame + } // ancestor chain + aEvent->mDisplayPanFeedback = displayPanFeedback; + aEvent->mPanDirection = panDirection; +} + +#ifdef XP_MACOSX +static nsINode* GetCrossDocParentNode(nsINode* aChild) { + MOZ_ASSERT(aChild, "The child is null!"); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsINode* parent = aChild->GetParentNode(); + if (parent && parent->IsContent() && aChild->IsContent()) { + parent = aChild->AsContent()->GetFlattenedTreeParent(); + } + + if (parent || !aChild->IsDocument()) { + return parent; + } + + return aChild->AsDocument()->GetEmbedderElement(); +} + +static bool NodeAllowsClickThrough(nsINode* aNode) { + while (aNode) { + if (aNode->IsAnyOfXULElements(nsGkAtoms::browser, nsGkAtoms::tree)) { + return false; + } + if (aNode->IsAnyOfXULElements(nsGkAtoms::scrollbar, nsGkAtoms::resizer)) { + return true; + } + aNode = GetCrossDocParentNode(aNode); + } + return true; +} +#endif + +void EventStateManager::PostHandleKeyboardEvent( + WidgetKeyboardEvent* aKeyboardEvent, nsIFrame* aTargetFrame, + nsEventStatus& aStatus) { + if (aStatus == nsEventStatus_eConsumeNoDefault) { + return; + } + + RefPtr<nsPresContext> presContext = mPresContext; + + if (!aKeyboardEvent->HasBeenPostedToRemoteProcess()) { + if (aKeyboardEvent->IsWaitingReplyFromRemoteProcess()) { + RefPtr<BrowserParent> remote = + aTargetFrame ? BrowserParent::GetFrom(aTargetFrame->GetContent()) + : nullptr; + if (remote) { + // remote is null-checked above in order to let pre-existing event + // targeting code's chrome vs. content decision override in case of + // disagreement in order not to disrupt non-Fission e10s mode in case + // there are still bugs in the Fission-mode code. That is, if remote + // is nullptr, the pre-existing event targeting code has deemed this + // event to belong to chrome rather than content. + BrowserParent* preciseRemote = BrowserParent::GetFocused(); + if (preciseRemote) { + remote = preciseRemote; + } + // else there was a race between layout and focus tracking + } + if (remote && !remote->IsReadyToHandleInputEvents()) { + // We need to dispatch the event to the browser element again if we were + // waiting for the key reply but the event wasn't sent to the content + // process due to the remote browser wasn't ready. + WidgetKeyboardEvent keyEvent(*aKeyboardEvent); + aKeyboardEvent->MarkAsHandledInRemoteProcess(); + RefPtr<Element> ownerElement = remote->GetOwnerElement(); + EventDispatcher::Dispatch(ownerElement, presContext, &keyEvent); + if (keyEvent.DefaultPrevented()) { + aKeyboardEvent->PreventDefault(!keyEvent.DefaultPreventedByContent()); + aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + } + // The widget expects a reply for every keyboard event. If the event wasn't + // dispatched to a content process (non-e10s or no content process + // running), we need to short-circuit here. Otherwise, we need to wait for + // the content process to handle the event. + if (aKeyboardEvent->mWidget) { + aKeyboardEvent->mWidget->PostHandleKeyEvent(aKeyboardEvent); + } + if (aKeyboardEvent->DefaultPrevented()) { + aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + // XXX Currently, our automated tests don't support mKeyNameIndex. + // Therefore, we still need to handle this with keyCode. + switch (aKeyboardEvent->mKeyCode) { + case NS_VK_TAB: + case NS_VK_F6: + // This is to prevent keyboard scrolling while alt modifier in use. + if (!aKeyboardEvent->IsAlt()) { + aStatus = nsEventStatus_eConsumeNoDefault; + + // Handling the tab event after it was sent to content is bad, + // because to the FocusManager the remote-browser looks like one + // element, so we would just move the focus to the next element + // in chrome, instead of handling it in content. + if (aKeyboardEvent->HasBeenPostedToRemoteProcess()) { + break; + } + + EnsureDocument(presContext); + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm && mDocument) { + // Shift focus forward or back depending on shift key + bool isDocMove = aKeyboardEvent->IsControl() || + aKeyboardEvent->mKeyCode == NS_VK_F6; + uint32_t dir = + aKeyboardEvent->IsShift() + ? (isDocMove ? static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_BACKWARDDOC) + : static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_BACKWARD)) + : (isDocMove ? static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_FORWARDDOC) + : static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_FORWARD)); + RefPtr<Element> result; + fm->MoveFocus(mDocument->GetWindow(), nullptr, dir, + nsIFocusManager::FLAG_BYKEY, getter_AddRefs(result)); + } + } + return; + case 0: + // We handle keys with no specific keycode value below. + break; + default: + return; + } + + switch (aKeyboardEvent->mKeyNameIndex) { + case KEY_NAME_INDEX_ZoomIn: + case KEY_NAME_INDEX_ZoomOut: + ChangeZoom(aKeyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_ZoomIn); + aStatus = nsEventStatus_eConsumeNoDefault; + break; + default: + break; + } +} + +nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext, + WidgetEvent* aEvent, + nsIFrame* aTargetFrame, + nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget) { + NS_ENSURE_ARG(aPresContext); + NS_ENSURE_ARG_POINTER(aStatus); + + mCurrentTarget = aTargetFrame; + mCurrentTargetContent = nullptr; + + HandleCrossProcessEvent(aEvent, aStatus); + // NOTE: the above call may have destroyed aTargetFrame, please use + // mCurrentTarget henceforth. This is to avoid using it accidentally: + aTargetFrame = nullptr; + + // Most of the events we handle below require a frame. + // Add special cases here. + if (!mCurrentTarget && aEvent->mMessage != eMouseUp && + aEvent->mMessage != eMouseDown && aEvent->mMessage != eDragEnter && + aEvent->mMessage != eDragOver && aEvent->mMessage != ePointerUp && + aEvent->mMessage != ePointerCancel) { + return NS_OK; + } + + // Keep the prescontext alive, we might need it after event dispatch + RefPtr<nsPresContext> presContext = aPresContext; + nsresult ret = NS_OK; + + switch (aEvent->mMessage) { + case eMouseDown: { + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (mouseEvent->mButton == MouseButton::ePrimary && + !sNormalLMouseEventInProcess) { + // We got a mouseup event while a mousedown event was being processed. + // Make sure that the capturing content is cleared. + PresShell::ReleaseCapturingContent(); + break; + } + + // For remote content, capture the event in the parent process at the + // <xul:browser remote> element. This will ensure that subsequent + // mousemove/mouseup events will continue to be dispatched to this element + // and therefore forwarded to the child. + if (aEvent->HasBeenPostedToRemoteProcess() && + !PresShell::GetCapturingContent()) { + if (nsIContent* content = + mCurrentTarget ? mCurrentTarget->GetContent() : nullptr) { + PresShell::SetCapturingContent(content, CaptureFlags::None, aEvent); + } else { + PresShell::ReleaseCapturingContent(); + } + } + + // If MouseEvent::PreventClickEvent() was called by chrome script, + // we need to forget the clicking content and click count for the + // following eMouseUp event. + if (mouseEvent->mClickEventPrevented) { + RefPtr<EventStateManager> esm = + ESMFromContentOrThis(aOverrideClickTarget); + switch (mouseEvent->mButton) { + case MouseButton::ePrimary: + esm->mLastLeftMouseDownContent = nullptr; + esm->mLClickCount = 0; + break; + case MouseButton::eSecondary: + esm->mLastMiddleMouseDownContent = nullptr; + esm->mMClickCount = 0; + break; + case MouseButton::eMiddle: + esm->mLastRightMouseDownContent = nullptr; + esm->mRClickCount = 0; + break; + default: + break; + } + } + + nsCOMPtr<nsIContent> activeContent; + // When content calls PreventDefault on pointerdown, we also call + // PreventDefault on the subsequent mouse events to suppress default + // behaviors. Normally, aStatus should be nsEventStatus_eConsumeNoDefault + // when the event is DefaultPrevented but it's reset to + // nsEventStatus_eIgnore in EventStateManager::PreHandleEvent. So we also + // check if the event is DefaultPrevented. + if (nsEventStatus_eConsumeNoDefault != *aStatus && + !aEvent->DefaultPrevented()) { + nsCOMPtr<nsIContent> newFocus; + bool suppressBlur = false; + if (mCurrentTarget) { + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(newFocus)); + activeContent = mCurrentTarget->GetContent(); + + // In some cases, we do not want to even blur the current focused + // element. Those cases are: + // 1. -moz-user-focus CSS property is set to 'ignore'; + // 2. XUL control element has the disabled property set to 'true'. + // + // We can't use nsIFrame::IsFocusable() because we want to blur when + // we click on a visibility: none element. + // We can't use nsIContent::IsFocusable() because we want to blur when + // we click on a non-focusable element like a <div>. + // We have to use |aEvent->mTarget| to not make sure we do not check + // an anonymous node of the targeted element. + suppressBlur = + mCurrentTarget->StyleUI()->UserFocus() == StyleUserFocus::Ignore; + + if (!suppressBlur) { + if (Element* element = + Element::FromEventTargetOrNull(aEvent->mTarget)) { + if (nsCOMPtr<nsIDOMXULControlElement> xulControl = + element->AsXULControl()) { + bool disabled = false; + xulControl->GetDisabled(&disabled); + suppressBlur = disabled; + } + } + } + } + + // When a root content which isn't editable but has an editable HTML + // <body> element is clicked, we should redirect the focus to the + // the <body> element. E.g., when an user click bottom of the editor + // where is outside of the <body> element, the <body> should be focused + // and the user can edit immediately after that. + // + // NOTE: The newFocus isn't editable that also means it's not in + // designMode. In designMode, all contents are not focusable. + if (newFocus && !newFocus->IsEditable()) { + Document* doc = newFocus->GetComposedDoc(); + if (doc && newFocus == doc->GetRootElement()) { + nsIContent* bodyContent = + nsLayoutUtils::GetEditableRootContentByContentEditable(doc); + if (bodyContent && bodyContent->GetPrimaryFrame()) { + newFocus = bodyContent; + } + } + } + + // When the mouse is pressed, the default action is to focus the + // target. Look for the nearest enclosing focusable frame. + // + // TODO: Probably this should be moved to Element::PostHandleEvent. + for (; newFocus; newFocus = newFocus->GetFlattenedTreeParent()) { + if (!newFocus->IsElement()) { + continue; + } + + nsIFrame* frame = newFocus->GetPrimaryFrame(); + if (!frame) { + continue; + } + + // If the mousedown happened inside a popup, don't try to set focus on + // one of its containing elements + if (frame->IsMenuPopupFrame()) { + newFocus = nullptr; + break; + } + + if (frame->IsFocusable(/* aWithMouse = */ true)) { + break; + } + + if (ShadowRoot* root = newFocus->GetShadowRoot()) { + if (root->DelegatesFocus()) { + if (Element* firstFocusable = + root->GetFocusDelegate(/* aWithMouse */ true)) { + newFocus = firstFocusable; + break; + } + } + } + } + + MOZ_ASSERT_IF(newFocus, newFocus->IsElement()); + + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + // if something was found to focus, focus it. Otherwise, if the + // element that was clicked doesn't have -moz-user-focus: ignore, + // clear the existing focus. For -moz-user-focus: ignore, the focus + // is just left as is. + // Another effect of mouse clicking, handled in Selection, is that + // it should update the caret position to where the mouse was + // clicked. Because the focus is cleared when clicking on a + // non-focusable node, the next press of the tab key will cause + // focus to be shifted from the caret position instead of the root. + if (newFocus) { + // use the mouse flag and the noscroll flag so that the content + // doesn't unexpectedly scroll when clicking an element that is + // only half visible + uint32_t flags = + nsIFocusManager::FLAG_BYMOUSE | nsIFocusManager::FLAG_NOSCROLL; + // If this was a touch-generated event, pass that information: + if (mouseEvent->mInputSource == + MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + flags |= nsIFocusManager::FLAG_BYTOUCH; + } + fm->SetFocus(MOZ_KnownLive(newFocus->AsElement()), flags); + } else if (!suppressBlur) { + // clear the focus within the frame and then set it as the + // focused frame + EnsureDocument(mPresContext); + if (mDocument) { + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = mDocument->GetWindow(); +#ifdef XP_MACOSX + if (!activeContent || !activeContent->IsXULElement()) +#endif + fm->ClearFocus(outerWindow); + // Prevent switch frame if we're already not in the foreground tab + // and we're in a content process. + // TODO: If we were inactive frame in this tab, and now in + // background tab, we shouldn't make the tab foreground, but + // we should set focus to clicked document in the background + // tab. However, nsFocusManager does not have proper method + // for doing this. Therefore, we should skip setting focus + // to clicked document for now. + if (XRE_IsParentProcess() || IsInActiveTab(mDocument)) { + fm->SetFocusedWindow(outerWindow); + } + } + } + } + + // The rest is left button-specific. + if (mouseEvent->mButton != MouseButton::ePrimary) { + break; + } + + // The nearest enclosing element goes into the :active state. If we're + // not an element (so we're text or something) we need to obtain + // our parent element and put it into :active instead. + if (activeContent && !activeContent->IsElement()) { + if (nsIContent* par = activeContent->GetFlattenedTreeParent()) { + activeContent = par; + } + } + } else { + // if we're here, the event handler returned false, so stop + // any of our own processing of a drag. Workaround for bug 43258. + StopTrackingDragGesture(true); + } + // XXX Why do we always set this is active? Active window may be changed + // by a mousedown event listener. + SetActiveManager(this, activeContent); + } break; + case ePointerCancel: + case ePointerUp: { + WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent(); + MOZ_ASSERT(pointerEvent); + // Implicitly releasing capture for given pointer. ePointerLostCapture + // should be send after ePointerUp or ePointerCancel. + PointerEventHandler::ImplicitlyReleasePointerCapture(pointerEvent); + PointerEventHandler::UpdateActivePointerState(pointerEvent); + + if (pointerEvent->mMessage == ePointerCancel || + pointerEvent->mInputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + // After pointercancel, pointer becomes invalid so we can remove + // relevant helper from table. Regarding pointerup with non-hoverable + // device, the pointer also becomes invalid. Hoverable (mouse/pen) + // pointers are valid all the time (not only between down/up). + GenerateMouseEnterExit(pointerEvent); + mPointersEnterLeaveHelper.Remove(pointerEvent->pointerId); + } + break; + } + case eMouseUp: { + // We can unconditionally stop capturing because + // we should never be capturing when the mouse button is up + PresShell::ReleaseCapturingContent(); + + ClearGlobalActiveContent(this); + WidgetMouseEvent* mouseUpEvent = aEvent->AsMouseEvent(); + if (mouseUpEvent && EventCausesClickEvents(*mouseUpEvent)) { + // Make sure to dispatch the click even if there is no frame for + // the current target element. This is required for Web compatibility. + RefPtr<EventStateManager> esm = + ESMFromContentOrThis(aOverrideClickTarget); + ret = + esm->PostHandleMouseUp(mouseUpEvent, aStatus, aOverrideClickTarget); + } + + if (PresShell* presShell = presContext->GetPresShell()) { + RefPtr<nsFrameSelection> frameSelection = presShell->FrameSelection(); + frameSelection->SetDragState(false); + } + } break; + case eWheelOperationEnd: { + MOZ_ASSERT(aEvent->IsTrusted()); + ScrollbarsForWheel::MayInactivate(); + WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent(); + nsIScrollableFrame* scrollTarget = + do_QueryFrame(ComputeScrollTargetAndMayAdjustWheelEvent( + mCurrentTarget, wheelEvent, + COMPUTE_DEFAULT_ACTION_TARGET_WITH_AUTO_DIR)); + // If the wheel event was handled by APZ, APZ will perform the scroll + // snap. + if (scrollTarget && !WheelTransaction::HandledByApz()) { + scrollTarget->ScrollSnap(); + } + } break; + case eWheel: + case eWheelOperationStart: { + MOZ_ASSERT(aEvent->IsTrusted()); + + if (*aStatus == nsEventStatus_eConsumeNoDefault) { + ScrollbarsForWheel::Inactivate(); + break; + } + + WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent(); + MOZ_ASSERT(wheelEvent); + + // When APZ is enabled, the actual scroll animation might be handled by + // the compositor. + WheelPrefs::Action action = + wheelEvent->mFlags.mHandledByAPZ + ? WheelPrefs::ACTION_NONE + : WheelPrefs::GetInstance()->ComputeActionFor(wheelEvent); + + WheelDeltaAdjustmentStrategy strategy = + GetWheelDeltaAdjustmentStrategy(*wheelEvent); + // Adjust the delta values of the wheel event if the current default + // action is to horizontalize scrolling. I.e., deltaY values are set to + // deltaX and deltaY and deltaZ values are set to 0. + // If horizontalized, the delta values will be restored and its overflow + // deltaX will become 0 when the WheelDeltaHorizontalizer instance is + // being destroyed. + WheelDeltaHorizontalizer horizontalizer(*wheelEvent); + if (WheelDeltaAdjustmentStrategy::eHorizontalize == strategy) { + horizontalizer.Horizontalize(); + } + + // Since ComputeScrollTargetAndMayAdjustWheelEvent() may adjust the delta + // if the event is auto-dir. So we use |ESMAutoDirWheelDeltaRestorer| + // here. + // An instance of |ESMAutoDirWheelDeltaRestorer| is used to monitor + // auto-dir adjustment which may happen during its lifetime. If the delta + // values is adjusted during its lifetime, the instance will restore the + // adjusted delta when it's being destrcuted. + ESMAutoDirWheelDeltaRestorer restorer(*wheelEvent); + nsIFrame* frameToScroll = ComputeScrollTargetAndMayAdjustWheelEvent( + mCurrentTarget, wheelEvent, + COMPUTE_DEFAULT_ACTION_TARGET_WITH_AUTO_DIR); + + switch (action) { + case WheelPrefs::ACTION_SCROLL: + case WheelPrefs::ACTION_HORIZONTALIZED_SCROLL: { + // For scrolling of default action, we should honor the mouse wheel + // transaction. + + ScrollbarsForWheel::PrepareToScrollText(this, mCurrentTarget, + wheelEvent); + + if (aEvent->mMessage != eWheel || + (!wheelEvent->mDeltaX && !wheelEvent->mDeltaY)) { + break; + } + + nsIScrollableFrame* scrollTarget = do_QueryFrame(frameToScroll); + ScrollbarsForWheel::SetActiveScrollTarget(scrollTarget); + + nsIFrame* rootScrollFrame = + !mCurrentTarget + ? nullptr + : mCurrentTarget->PresShell()->GetRootScrollFrame(); + nsIScrollableFrame* rootScrollableFrame = nullptr; + if (rootScrollFrame) { + rootScrollableFrame = do_QueryFrame(rootScrollFrame); + } + if (!scrollTarget || scrollTarget == rootScrollableFrame) { + wheelEvent->mViewPortIsOverscrolled = true; + } + wheelEvent->mOverflowDeltaX = wheelEvent->mDeltaX; + wheelEvent->mOverflowDeltaY = wheelEvent->mDeltaY; + WheelPrefs::GetInstance()->CancelApplyingUserPrefsFromOverflowDelta( + wheelEvent); + if (scrollTarget) { + DoScrollText(scrollTarget, wheelEvent); + } else { + WheelTransaction::EndTransaction(); + ScrollbarsForWheel::Inactivate(); + } + break; + } + case WheelPrefs::ACTION_HISTORY: { + // If this event doesn't cause eLegacyMouseLineOrPageScroll event or + // the direction is oblique, don't perform history back/forward. + int32_t intDelta = wheelEvent->GetPreferredIntDelta(); + if (!intDelta) { + break; + } + DoScrollHistory(intDelta); + break; + } + case WheelPrefs::ACTION_ZOOM: { + // If this event doesn't cause eLegacyMouseLineOrPageScroll event or + // the direction is oblique, don't perform zoom in/out. + int32_t intDelta = wheelEvent->GetPreferredIntDelta(); + if (!intDelta) { + break; + } + DoScrollZoom(mCurrentTarget, intDelta); + break; + } + case WheelPrefs::ACTION_NONE: + default: + bool allDeltaOverflown = false; + if (StaticPrefs::dom_event_wheel_event_groups_enabled() && + (wheelEvent->mDeltaX != 0.0 || wheelEvent->mDeltaY != 0.0)) { + if (frameToScroll) { + WheelTransaction::WillHandleDefaultAction( + wheelEvent, frameToScroll, mCurrentTarget); + } else { + WheelTransaction::EndTransaction(); + } + } + if (wheelEvent->mFlags.mHandledByAPZ) { + if (wheelEvent->mCanTriggerSwipe) { + // For events that can trigger swipes, APZ needs to know whether + // scrolling is possible in the requested direction. It does this + // by looking at the scroll overflow values on mCanTriggerSwipe + // events after they have been processed. + allDeltaOverflown = !ComputeScrollTarget( + mCurrentTarget, wheelEvent, COMPUTE_DEFAULT_ACTION_TARGET); + } + } else { + // The event was processed neither by APZ nor by us, so all of the + // delta values must be overflown delta values. + allDeltaOverflown = true; + } + + if (!allDeltaOverflown) { + break; + } + wheelEvent->mOverflowDeltaX = wheelEvent->mDeltaX; + wheelEvent->mOverflowDeltaY = wheelEvent->mDeltaY; + WheelPrefs::GetInstance()->CancelApplyingUserPrefsFromOverflowDelta( + wheelEvent); + wheelEvent->mViewPortIsOverscrolled = true; + break; + } + *aStatus = nsEventStatus_eConsumeNoDefault; + } break; + + case eGestureNotify: { + if (nsEventStatus_eConsumeNoDefault != *aStatus) { + DecideGestureEvent(aEvent->AsGestureNotifyEvent(), mCurrentTarget); + } + } break; + + case eDragEnter: + case eDragOver: { + NS_ASSERTION(aEvent->mClass == eDragEventClass, "Expected a drag event"); + + // Check if the drag is occurring inside a scrollable area. If so, scroll + // the area when the mouse is near the edges. + if (mCurrentTarget && aEvent->mMessage == eDragOver) { + nsIFrame* checkFrame = mCurrentTarget; + while (checkFrame) { + nsIScrollableFrame* scrollFrame = do_QueryFrame(checkFrame); + // Break out so only the innermost scrollframe is scrolled. + if (scrollFrame && scrollFrame->DragScroll(aEvent)) { + break; + } + checkFrame = checkFrame->GetParent(); + } + } + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) break; + + // Reset the flag. + dragSession->SetOnlyChromeDrop(false); + if (mPresContext) { + EnsureDocument(mPresContext); + } + bool isChromeDoc = nsContentUtils::IsChromeDoc(mDocument); + + // the initial dataTransfer is the one from the dragstart event that + // was set on the dragSession when the drag began. + RefPtr<DataTransfer> dataTransfer; + RefPtr<DataTransfer> initialDataTransfer = dragSession->GetDataTransfer(); + + WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); + + // collect any changes to moz cursor settings stored in the event's + // data transfer. + UpdateDragDataTransfer(dragEvent); + + // cancelling a dragenter or dragover event means that a drop should be + // allowed, so update the dropEffect and the canDrop state to indicate + // that a drag is allowed. If the event isn't cancelled, a drop won't be + // allowed. Essentially, to allow a drop somewhere, specify the effects + // using the effectAllowed and dropEffect properties in a dragenter or + // dragover event and cancel the event. To not allow a drop somewhere, + // don't cancel the event or set the effectAllowed or dropEffect to + // "none". This way, if the event is just ignored, no drop will be + // allowed. + uint32_t dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; + uint32_t action = nsIDragService::DRAGDROP_ACTION_NONE; + if (nsEventStatus_eConsumeNoDefault == *aStatus) { + // If the event has initialized its mDataTransfer, use it. + // Or the event has not been initialized its mDataTransfer, but + // it's set before dispatch because of synthesized, but without + // testing session (e.g., emulating drag from another app), use it + // coming from outside. + // XXX Perhaps, for the latter case, we need new API because we don't + // have a chance to initialize allowed effects of the session. + if (dragEvent->mDataTransfer) { + // get the dataTransfer and the dropEffect that was set on it + dataTransfer = dragEvent->mDataTransfer; + dropEffect = dataTransfer->DropEffectInt(); + } else { + // if dragEvent->mDataTransfer is null, it means that no attempt was + // made to access the dataTransfer during the event, yet the event + // was cancelled. Instead, use the initial data transfer available + // from the drag session. The drop effect would not have been + // initialized (which is done in DragEvent::GetDataTransfer), + // so set it from the drag action. We'll still want to filter it + // based on the effectAllowed below. + dataTransfer = initialDataTransfer; + + dragSession->GetDragAction(&action); + + // filter the drop effect based on the action. Use UNINITIALIZED as + // any effect is allowed. + dropEffect = nsContentUtils::FilterDropEffect( + action, nsIDragService::DRAGDROP_ACTION_UNINITIALIZED); + } + + // At this point, if the dataTransfer is null, it means that the + // drag was originally started by directly calling the drag service. + // Just assume that all effects are allowed. + uint32_t effectAllowed = nsIDragService::DRAGDROP_ACTION_UNINITIALIZED; + if (dataTransfer) { + effectAllowed = dataTransfer->EffectAllowedInt(); + } + + // set the drag action based on the drop effect and effect allowed. + // The drop effect field on the drag transfer object specifies the + // desired current drop effect. However, it cannot be used if the + // effectAllowed state doesn't include that type of action. If the + // dropEffect is "none", then the action will be 'none' so a drop will + // not be allowed. + if (effectAllowed == nsIDragService::DRAGDROP_ACTION_UNINITIALIZED || + dropEffect & effectAllowed) + action = dropEffect; + + if (action == nsIDragService::DRAGDROP_ACTION_NONE) + dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; + + // inform the drag session that a drop is allowed on this node. + dragSession->SetDragAction(action); + dragSession->SetCanDrop(action != nsIDragService::DRAGDROP_ACTION_NONE); + + // For now, do this only for dragover. + // XXXsmaug dragenter needs some more work. + if (aEvent->mMessage == eDragOver && !isChromeDoc) { + // Someone has called preventDefault(), check whether is was on + // content or chrome. + dragSession->SetOnlyChromeDrop( + !dragEvent->mDefaultPreventedOnContent); + } + } else if (aEvent->mMessage == eDragOver && !isChromeDoc) { + // No one called preventDefault(), so handle drop only in chrome. + dragSession->SetOnlyChromeDrop(true); + } + if (ContentChild* child = ContentChild::GetSingleton()) { + child->SendUpdateDropEffect(action, dropEffect); + } + if (aEvent->HasBeenPostedToRemoteProcess()) { + dragSession->SetCanDrop(true); + } else if (initialDataTransfer) { + // Now set the drop effect in the initial dataTransfer. This ensures + // that we can get the desired drop effect in the drop event. For events + // dispatched to content, the content process will take care of setting + // this. + initialDataTransfer->SetDropEffectInt(dropEffect); + } + } break; + + case eDrop: { + if (aEvent->mFlags.mIsSynthesizedForTests) { + if (nsCOMPtr<nsIDragSession> dragSession = + nsContentUtils::GetDragSession()) { + MOZ_ASSERT(dragSession->IsSynthesizedForTests()); + RefPtr<WindowContext> sourceWC; + DebugOnly<nsresult> rvIgnored = + dragSession->GetSourceWindowContext(getter_AddRefs(sourceWC)); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIDragSession::GetSourceDocument() failed, but ignored"); + // If the drag source hasn't been initialized, i.e., dragstart was + // consumed by the test, the test needs to dispatch "dragend" event + // instead of the drag session. Therefore, it does not make sense + // to set drag end point in such case (you hit assersion if you do + // it). + if (sourceWC) { + CSSIntPoint dropPointInScreen = + Event::GetScreenCoords(aPresContext, aEvent, aEvent->mRefPoint) + .extract(); + dragSession->SetDragEndPointForTests(dropPointInScreen.x, + dropPointInScreen.y); + } + } + } + sLastDragOverFrame = nullptr; + ClearGlobalActiveContent(this); + break; + } + case eDragExit: + // make sure to fire the enter and exit_synth events after the + // eDragExit event, otherwise we'll clean up too early + GenerateDragDropEnterExit(presContext, aEvent->AsDragEvent()); + if (ContentChild* child = ContentChild::GetSingleton()) { + // SendUpdateDropEffect to prevent nsIDragService from waiting for + // response of forwarded dragexit event. + child->SendUpdateDropEffect(nsIDragService::DRAGDROP_ACTION_NONE, + nsIDragService::DRAGDROP_ACTION_NONE); + } + break; + + case eKeyUp: + // If space key is released, we need to inactivate the element which was + // activated by preceding space key down. + // XXX Currently, we don't store the reason of activation. Therefore, + // this may cancel what is activated by a mousedown, but it must not + // cause actual problem in web apps in the wild since it must be + // rare case that users release space key during a mouse click/drag. + if (aEvent->AsKeyboardEvent()->ShouldWorkAsSpaceKey()) { + ClearGlobalActiveContent(this); + } + break; + + case eKeyPress: { + WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); + PostHandleKeyboardEvent(keyEvent, mCurrentTarget, *aStatus); + } break; + + case eMouseEnterIntoWidget: + if (mCurrentTarget) { + nsCOMPtr<nsIContent> targetContent; + mCurrentTarget->GetContentForEvent(aEvent, + getter_AddRefs(targetContent)); + SetContentState(targetContent, ElementState::HOVER); + } + break; + + case eMouseExitFromWidget: + PointerEventHandler::UpdateActivePointerState(aEvent->AsMouseEvent()); + break; + +#ifdef XP_MACOSX + case eMouseActivate: + if (mCurrentTarget) { + nsCOMPtr<nsIContent> targetContent; + mCurrentTarget->GetContentForEvent(aEvent, + getter_AddRefs(targetContent)); + if (!NodeAllowsClickThrough(targetContent)) { + *aStatus = nsEventStatus_eConsumeNoDefault; + } + } + break; +#endif + + default: + break; + } + + // Reset target frame to null to avoid mistargeting after reentrant event + mCurrentTarget = nullptr; + mCurrentTargetContent = nullptr; + + return ret; +} + +BrowserParent* EventStateManager::GetCrossProcessTarget() { + return IMEStateManager::GetActiveBrowserParent(); +} + +bool EventStateManager::IsTargetCrossProcess(WidgetGUIEvent* aEvent) { + // Check to see if there is a focused, editable content in chrome, + // in that case, do not forward IME events to content + Element* focusedElement = GetFocusedElement(); + if (focusedElement && focusedElement->IsEditable()) { + return false; + } + return IMEStateManager::GetActiveBrowserParent() != nullptr; +} + +void EventStateManager::NotifyDestroyPresContext(nsPresContext* aPresContext) { + RefPtr<nsPresContext> presContext = aPresContext; + if (presContext) { + IMEStateManager::OnDestroyPresContext(*presContext); + } + + // Bug 70855: Presentation is going away, possibly for a reframe. + // Reset the hover state so that if we're recreating the presentation, + // we won't have the old hover state still set in the new presentation, + // as if the new presentation is resized, a new element may be hovered. + ResetHoverState(); + + mPointersEnterLeaveHelper.Clear(); + PointerEventHandler::NotifyDestroyPresContext(presContext); +} + +void EventStateManager::ResetHoverState() { + if (mHoverContent) { + SetContentState(nullptr, ElementState::HOVER); + } +} + +void EventStateManager::SetPresContext(nsPresContext* aPresContext) { + mPresContext = aPresContext; +} + +void EventStateManager::ClearFrameRefs(nsIFrame* aFrame) { + if (aFrame && aFrame == mCurrentTarget) { + mCurrentTargetContent = aFrame->GetContent(); + } +} + +struct CursorImage { + gfx::IntPoint mHotspot; + nsCOMPtr<imgIContainer> mContainer; + ImageResolution mResolution; + bool mEarlierCursorLoading = false; +}; + +// Given the event that we're processing, and the computed cursor and hotspot, +// determine whether the custom CSS cursor should be blocked (that is, not +// honored). +// +// We will not honor it all of the following are true: +// +// * layout.cursor.block.enabled is true. +// * the size of the custom cursor is bigger than layout.cursor.block.max-size. +// * the bounds of the cursor would end up outside of the viewport of the +// top-level content document. +// +// This is done in order to prevent hijacking the cursor, see bug 1445844 and +// co. +static bool ShouldBlockCustomCursor(nsPresContext* aPresContext, + WidgetEvent* aEvent, + const CursorImage& aCursor) { + if (!StaticPrefs::layout_cursor_block_enabled()) { + return false; + } + + int32_t width = 0; + int32_t height = 0; + aCursor.mContainer->GetWidth(&width); + aCursor.mContainer->GetHeight(&height); + aCursor.mResolution.ApplyTo(width, height); + + int32_t maxSize = StaticPrefs::layout_cursor_block_max_size(); + + if (width <= maxSize && height <= maxSize) { + return false; + } + + auto input = DOMIntersectionObserver::ComputeInput(*aPresContext->Document(), + nullptr, nullptr); + + if (!input.mRootFrame) { + return false; + } + + nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, RelativeTo{input.mRootFrame}); + + // The cursor size won't be affected by our full zoom in the parent process, + // so undo that before checking the rect. + float zoom = aPresContext->GetFullZoom(); + + // Also adjust for accessibility cursor scaling factor. + zoom /= LookAndFeel::GetFloat(LookAndFeel::FloatID::CursorScale, 1.0f); + + nsSize size(CSSPixel::ToAppUnits(width / zoom), + CSSPixel::ToAppUnits(height / zoom)); + nsPoint hotspot(CSSPixel::ToAppUnits(aCursor.mHotspot.x / zoom), + CSSPixel::ToAppUnits(aCursor.mHotspot.y / zoom)); + + const nsRect cursorRect(point - hotspot, size); + auto output = DOMIntersectionObserver::Intersect(input, cursorRect); + return !output.mIntersectionRect || + !(*output.mIntersectionRect == cursorRect); +} + +static gfx::IntPoint ComputeHotspot(imgIContainer* aContainer, + const Maybe<gfx::Point>& aHotspot) { + MOZ_ASSERT(aContainer); + + // css3-ui says to use the CSS-specified hotspot if present, + // otherwise use the intrinsic hotspot, otherwise use the top left + // corner. + if (aHotspot) { + int32_t imgWidth, imgHeight; + aContainer->GetWidth(&imgWidth); + aContainer->GetHeight(&imgHeight); + auto hotspot = gfx::IntPoint::Round(*aHotspot); + return {std::max(std::min(hotspot.x.value, imgWidth - 1), 0), + std::max(std::min(hotspot.y.value, imgHeight - 1), 0)}; + } + + gfx::IntPoint hotspot; + aContainer->GetHotspotX(&hotspot.x.value); + aContainer->GetHotspotY(&hotspot.y.value); + return hotspot; +} + +static CursorImage ComputeCustomCursor(nsPresContext* aPresContext, + WidgetEvent* aEvent, + const nsIFrame& aFrame, + const nsIFrame::Cursor& aCursor) { + if (aCursor.mAllowCustomCursor == nsIFrame::AllowCustomCursorImage::No) { + return {}; + } + const ComputedStyle& style = + aCursor.mStyle ? *aCursor.mStyle : *aFrame.Style(); + + // If we are falling back because any cursor before us is loading, let the + // consumer know. + bool loading = false; + for (const auto& image : style.StyleUI()->Cursor().images.AsSpan()) { + MOZ_ASSERT(image.image.IsImageRequestType(), + "Cursor image should only parse url() types"); + uint32_t status; + imgRequestProxy* req = image.image.GetImageRequest(); + if (!req || NS_FAILED(req->GetImageStatus(&status))) { + continue; + } + if (!(status & imgIRequest::STATUS_LOAD_COMPLETE)) { + loading = true; + continue; + } + if (status & imgIRequest::STATUS_ERROR) { + continue; + } + nsCOMPtr<imgIContainer> container; + req->GetImage(getter_AddRefs(container)); + if (!container) { + continue; + } + StyleImageOrientation orientation = + aFrame.StyleVisibility()->UsedImageOrientation(req); + container = nsLayoutUtils::OrientImage(container, orientation); + Maybe<gfx::Point> specifiedHotspot = + image.has_hotspot ? Some(gfx::Point{image.hotspot_x, image.hotspot_y}) + : Nothing(); + gfx::IntPoint hotspot = ComputeHotspot(container, specifiedHotspot); + CursorImage result{hotspot, std::move(container), + image.image.GetResolution(), loading}; + if (ShouldBlockCustomCursor(aPresContext, aEvent, result)) { + continue; + } + // This is the one we want! + return result; + } + return {{}, nullptr, {}, loading}; +} + +void EventStateManager::UpdateCursor(nsPresContext* aPresContext, + WidgetEvent* aEvent, + nsIFrame* aTargetFrame, + nsEventStatus* aStatus) { + if (aTargetFrame && IsRemoteTarget(aTargetFrame->GetContent())) { + return; + } + + auto cursor = StyleCursorKind::Default; + nsCOMPtr<imgIContainer> container; + ImageResolution resolution; + Maybe<gfx::IntPoint> hotspot; + + // If cursor is locked just use the locked one + if (mLockCursor != kInvalidCursorKind) { + cursor = mLockCursor; + } + // If not locked, look for correct cursor + else if (aTargetFrame) { + nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, RelativeTo{aTargetFrame}); + Maybe<nsIFrame::Cursor> framecursor = aTargetFrame->GetCursor(pt); + // Avoid setting cursor when the mouse is over a windowless plugin. + if (!framecursor) { + if (XRE_IsContentProcess()) { + mLastFrameConsumedSetCursor = true; + } + return; + } + // Make sure cursors get reset after the mouse leaves a + // windowless plugin frame. + if (mLastFrameConsumedSetCursor) { + ClearCachedWidgetCursor(aTargetFrame); + mLastFrameConsumedSetCursor = false; + } + + const CursorImage customCursor = + ComputeCustomCursor(aPresContext, aEvent, *aTargetFrame, *framecursor); + + // If the current cursor is from the same frame, and it is now + // loading some new image for the cursor, we should wait for a + // while rather than taking its fallback cursor directly. + if (customCursor.mEarlierCursorLoading && + gLastCursorSourceFrame == aTargetFrame && + TimeStamp::NowLoRes() - gLastCursorUpdateTime < + TimeDuration::FromMilliseconds(kCursorLoadingTimeout)) { + return; + } + cursor = framecursor->mCursor; + container = std::move(customCursor.mContainer); + resolution = customCursor.mResolution; + hotspot = Some(customCursor.mHotspot); + } + + if (StaticPrefs::ui_use_activity_cursor()) { + // Check whether or not to show the busy cursor + nsCOMPtr<nsIDocShell> docShell(aPresContext->GetDocShell()); + if (!docShell) return; + auto busyFlags = docShell->GetBusyFlags(); + + // Show busy cursor everywhere before page loads + // and just replace the arrow cursor after page starts loading + if (busyFlags & nsIDocShell::BUSY_FLAGS_BUSY && + (cursor == StyleCursorKind::Auto || + cursor == StyleCursorKind::Default)) { + cursor = StyleCursorKind::Progress; + container = nullptr; + } + } + + if (aTargetFrame) { + if (cursor == StyleCursorKind::Pointer && IsSelectingLink(aTargetFrame)) { + cursor = aTargetFrame->GetWritingMode().IsVertical() + ? StyleCursorKind::VerticalText + : StyleCursorKind::Text; + } + SetCursor(cursor, container, resolution, hotspot, + aTargetFrame->GetNearestWidget(), false); + gLastCursorSourceFrame = aTargetFrame; + gLastCursorUpdateTime = TimeStamp::NowLoRes(); + } + + if (mLockCursor != kInvalidCursorKind || StyleCursorKind::Auto != cursor) { + *aStatus = nsEventStatus_eConsumeDoDefault; + } +} + +void EventStateManager::ClearCachedWidgetCursor(nsIFrame* aTargetFrame) { + if (!aTargetFrame) { + return; + } + nsIWidget* aWidget = aTargetFrame->GetNearestWidget(); + if (!aWidget) { + return; + } + aWidget->ClearCachedCursor(); +} + +nsresult EventStateManager::SetCursor(StyleCursorKind aCursor, + imgIContainer* aContainer, + const ImageResolution& aResolution, + const Maybe<gfx::IntPoint>& aHotspot, + nsIWidget* aWidget, bool aLockCursor) { + EnsureDocument(mPresContext); + NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE); + sMouseOverDocument = mDocument.get(); + + NS_ENSURE_TRUE(aWidget, NS_ERROR_FAILURE); + if (aLockCursor) { + if (StyleCursorKind::Auto != aCursor) { + mLockCursor = aCursor; + } else { + // If cursor style is set to auto we unlock the cursor again. + mLockCursor = kInvalidCursorKind; + } + } + nsCursor c; + switch (aCursor) { + case StyleCursorKind::Auto: + case StyleCursorKind::Default: + c = eCursor_standard; + break; + case StyleCursorKind::Pointer: + c = eCursor_hyperlink; + break; + case StyleCursorKind::Crosshair: + c = eCursor_crosshair; + break; + case StyleCursorKind::Move: + c = eCursor_move; + break; + case StyleCursorKind::Text: + c = eCursor_select; + break; + case StyleCursorKind::Wait: + c = eCursor_wait; + break; + case StyleCursorKind::Help: + c = eCursor_help; + break; + case StyleCursorKind::NResize: + c = eCursor_n_resize; + break; + case StyleCursorKind::SResize: + c = eCursor_s_resize; + break; + case StyleCursorKind::WResize: + c = eCursor_w_resize; + break; + case StyleCursorKind::EResize: + c = eCursor_e_resize; + break; + case StyleCursorKind::NwResize: + c = eCursor_nw_resize; + break; + case StyleCursorKind::SeResize: + c = eCursor_se_resize; + break; + case StyleCursorKind::NeResize: + c = eCursor_ne_resize; + break; + case StyleCursorKind::SwResize: + c = eCursor_sw_resize; + break; + case StyleCursorKind::Copy: // CSS3 + c = eCursor_copy; + break; + case StyleCursorKind::Alias: + c = eCursor_alias; + break; + case StyleCursorKind::ContextMenu: + c = eCursor_context_menu; + break; + case StyleCursorKind::Cell: + c = eCursor_cell; + break; + case StyleCursorKind::Grab: + c = eCursor_grab; + break; + case StyleCursorKind::Grabbing: + c = eCursor_grabbing; + break; + case StyleCursorKind::Progress: + c = eCursor_spinning; + break; + case StyleCursorKind::ZoomIn: + c = eCursor_zoom_in; + break; + case StyleCursorKind::ZoomOut: + c = eCursor_zoom_out; + break; + case StyleCursorKind::NotAllowed: + c = eCursor_not_allowed; + break; + case StyleCursorKind::ColResize: + c = eCursor_col_resize; + break; + case StyleCursorKind::RowResize: + c = eCursor_row_resize; + break; + case StyleCursorKind::NoDrop: + c = eCursor_no_drop; + break; + case StyleCursorKind::VerticalText: + c = eCursor_vertical_text; + break; + case StyleCursorKind::AllScroll: + c = eCursor_all_scroll; + break; + case StyleCursorKind::NeswResize: + c = eCursor_nesw_resize; + break; + case StyleCursorKind::NwseResize: + c = eCursor_nwse_resize; + break; + case StyleCursorKind::NsResize: + c = eCursor_ns_resize; + break; + case StyleCursorKind::EwResize: + c = eCursor_ew_resize; + break; + case StyleCursorKind::None: + c = eCursor_none; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown cursor kind"); + c = eCursor_standard; + break; + } + + uint32_t x = aHotspot ? aHotspot->x.value : 0; + uint32_t y = aHotspot ? aHotspot->y.value : 0; + aWidget->SetCursor(nsIWidget::Cursor{c, aContainer, x, y, aResolution}); + return NS_OK; +} + +class MOZ_STACK_CLASS ESMEventCB : public EventDispatchingCallback { + public: + explicit ESMEventCB(nsIContent* aTarget) : mTarget(aTarget) {} + + MOZ_CAN_RUN_SCRIPT + void HandleEvent(EventChainPostVisitor& aVisitor) override { + if (aVisitor.mPresContext) { + nsIFrame* frame = aVisitor.mPresContext->GetPrimaryFrameFor(mTarget); + if (frame) { + frame->HandleEvent(aVisitor.mPresContext, aVisitor.mEvent->AsGUIEvent(), + &aVisitor.mEventStatus); + } + } + } + + nsCOMPtr<nsIContent> mTarget; +}; + +static UniquePtr<WidgetMouseEvent> CreateMouseOrPointerWidgetEvent( + WidgetMouseEvent* aMouseEvent, EventMessage aMessage, + EventTarget* aRelatedTarget) { + WidgetPointerEvent* sourcePointer = aMouseEvent->AsPointerEvent(); + UniquePtr<WidgetMouseEvent> newEvent; + if (sourcePointer) { + AUTO_PROFILER_LABEL("CreateMouseOrPointerWidgetEvent", OTHER); + + WidgetPointerEvent* newPointerEvent = new WidgetPointerEvent( + aMouseEvent->IsTrusted(), aMessage, aMouseEvent->mWidget); + newPointerEvent->mIsPrimary = sourcePointer->mIsPrimary; + newPointerEvent->mWidth = sourcePointer->mWidth; + newPointerEvent->mHeight = sourcePointer->mHeight; + newPointerEvent->mInputSource = sourcePointer->mInputSource; + + newEvent = WrapUnique(newPointerEvent); + } else { + newEvent = MakeUnique<WidgetMouseEvent>(aMouseEvent->IsTrusted(), aMessage, + aMouseEvent->mWidget, + WidgetMouseEvent::eReal); + } + newEvent->mRelatedTarget = aRelatedTarget; + newEvent->mRefPoint = aMouseEvent->mRefPoint; + newEvent->mModifiers = aMouseEvent->mModifiers; + newEvent->mButton = aMouseEvent->mButton; + newEvent->mButtons = aMouseEvent->mButtons; + newEvent->mPressure = aMouseEvent->mPressure; + newEvent->mInputSource = aMouseEvent->mInputSource; + newEvent->pointerId = aMouseEvent->pointerId; + + return newEvent; +} + +nsIFrame* EventStateManager::DispatchMouseOrPointerEvent( + WidgetMouseEvent* aMouseEvent, EventMessage aMessage, + nsIContent* aTargetContent, nsIContent* aRelatedContent) { + // http://dvcs.w3.org/hg/webevents/raw-file/default/mouse-lock.html#methods + // "[When the mouse is locked on an element...e]vents that require the concept + // of a mouse cursor must not be dispatched (for example: mouseover, + // mouseout). + if (PointerLockManager::IsLocked() && + (aMessage == eMouseLeave || aMessage == eMouseEnter || + aMessage == eMouseOver || aMessage == eMouseOut)) { + mCurrentTargetContent = nullptr; + nsCOMPtr<Element> pointerLockedElement = + PointerLockManager::GetLockedElement(); + if (!pointerLockedElement) { + NS_WARNING("Should have pointer locked element, but didn't."); + return nullptr; + } + return mPresContext->GetPrimaryFrameFor(pointerLockedElement); + } + + mCurrentTargetContent = nullptr; + + if (!aTargetContent) { + return nullptr; + } + + nsCOMPtr<nsIContent> targetContent = aTargetContent; + nsCOMPtr<nsIContent> relatedContent = aRelatedContent; + + UniquePtr<WidgetMouseEvent> dispatchEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, aMessage, relatedContent); + + AutoWeakFrame previousTarget = mCurrentTarget; + mCurrentTargetContent = targetContent; + + nsIFrame* targetFrame = nullptr; + + nsEventStatus status = nsEventStatus_eIgnore; + ESMEventCB callback(targetContent); + RefPtr<nsPresContext> presContext = mPresContext; + EventDispatcher::Dispatch(targetContent, presContext, dispatchEvent.get(), + nullptr, &status, &callback); + + if (mPresContext) { + // Although the primary frame was checked in event callback, it may not be + // the same object after event dispatch and handling, so refetch it. + targetFrame = mPresContext->GetPrimaryFrameFor(targetContent); + + // If we are entering/leaving remote content, dispatch a mouse enter/exit + // event to the remote frame. + if (IsTopLevelRemoteTarget(targetContent)) { + if (aMessage == eMouseOut) { + // For remote content, send a puppet widget mouse exit event. + UniquePtr<WidgetMouseEvent> remoteEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, eMouseExitFromWidget, + relatedContent); + remoteEvent->mExitFrom = Some(WidgetMouseEvent::ePuppet); + + // mCurrentTarget is set to the new target, so we must reset it to the + // old target and then dispatch a cross-process event. (mCurrentTarget + // will be set back below.) HandleCrossProcessEvent will query for the + // proper target via GetEventTarget which will return mCurrentTarget. + mCurrentTarget = targetFrame; + HandleCrossProcessEvent(remoteEvent.get(), &status); + } else if (aMessage == eMouseOver) { + UniquePtr<WidgetMouseEvent> remoteEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, eMouseEnterIntoWidget, + relatedContent); + HandleCrossProcessEvent(remoteEvent.get(), &status); + } + } + } + + mCurrentTargetContent = nullptr; + mCurrentTarget = previousTarget; + + return targetFrame; +} + +static nsIContent* FindCommonAncestor(nsIContent* aNode1, nsIContent* aNode2) { + if (!aNode1 || !aNode2) { + return nullptr; + } + return nsContentUtils::GetCommonFlattenedTreeAncestor(aNode1, aNode2); +} + +class EnterLeaveDispatcher { + public: + EnterLeaveDispatcher(EventStateManager* aESM, nsIContent* aTarget, + nsIContent* aRelatedTarget, + WidgetMouseEvent* aMouseEvent, + EventMessage aEventMessage) + : mESM(aESM), mMouseEvent(aMouseEvent), mEventMessage(aEventMessage) { + nsPIDOMWindowInner* win = + aTarget ? aTarget->OwnerDoc()->GetInnerWindow() : nullptr; + if (aMouseEvent->AsPointerEvent() + ? win && win->HasPointerEnterLeaveEventListeners() + : win && win->HasMouseEnterLeaveEventListeners()) { + mRelatedTarget = + aRelatedTarget ? aRelatedTarget->FindFirstNonChromeOnlyAccessContent() + : nullptr; + nsINode* commonParent = FindCommonAncestor(aTarget, aRelatedTarget); + nsIContent* current = aTarget; + // Note, it is ok if commonParent is null! + while (current && current != commonParent) { + if (!current->ChromeOnlyAccess()) { + mTargets.AppendObject(current); + } + // mouseenter/leave is fired only on elements. + current = current->GetFlattenedTreeParent(); + } + } + } + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Dispatch() { + if (mEventMessage == eMouseEnter || mEventMessage == ePointerEnter) { + for (int32_t i = mTargets.Count() - 1; i >= 0; --i) { + mESM->DispatchMouseOrPointerEvent(mMouseEvent, mEventMessage, + MOZ_KnownLive(mTargets[i]), + mRelatedTarget); + } + } else { + for (int32_t i = 0; i < mTargets.Count(); ++i) { + mESM->DispatchMouseOrPointerEvent(mMouseEvent, mEventMessage, + MOZ_KnownLive(mTargets[i]), + mRelatedTarget); + } + } + } + + // Nothing overwrites anything after constructor. Please remove MOZ_KnownLive + // and MOZ_KNOWN_LIVE if anything marked as such becomes mutable. + const RefPtr<EventStateManager> mESM; + nsCOMArray<nsIContent> mTargets; + MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mRelatedTarget; + WidgetMouseEvent* mMouseEvent; + EventMessage mEventMessage; +}; + +void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent, + nsIContent* aMovingInto) { + RefPtr<OverOutElementsWrapper> wrapper = GetWrapperByEventID(aMouseEvent); + + if (!wrapper || !wrapper->mLastOverElement) { + return; + } + // Before firing mouseout, check for recursion + if (wrapper->mLastOverElement == wrapper->mFirstOutEventElement) { + return; + } + + if (RefPtr<nsFrameLoaderOwner> flo = + do_QueryObject(wrapper->mLastOverElement)) { + if (BrowsingContext* bc = flo->GetExtantBrowsingContext()) { + if (nsIDocShell* docshell = bc->GetDocShell()) { + if (RefPtr<nsPresContext> presContext = docshell->GetPresContext()) { + EventStateManager* kidESM = presContext->EventStateManager(); + // Not moving into any element in this subdocument + kidESM->NotifyMouseOut(aMouseEvent, nullptr); + } + } + } + } + // That could have caused DOM events which could wreak havoc. Reverify + // things and be careful. + if (!wrapper->mLastOverElement) { + return; + } + + // Store the first mouseOut event we fire and don't refire mouseOut + // to that element while the first mouseOut is still ongoing. + wrapper->mFirstOutEventElement = wrapper->mLastOverElement; + + // Don't touch hover state if aMovingInto is non-null. Caller will update + // hover state itself, and we have optimizations for hover switching between + // two nearby elements both deep in the DOM tree that would be defeated by + // switching the hover state to null here. + bool isPointer = aMouseEvent->mClass == ePointerEventClass; + if (!aMovingInto && !isPointer) { + // Unset :hover + SetContentState(nullptr, ElementState::HOVER); + } + + EnterLeaveDispatcher leaveDispatcher(this, wrapper->mLastOverElement, + aMovingInto, aMouseEvent, + isPointer ? ePointerLeave : eMouseLeave); + + // Fire mouseout + nsCOMPtr<nsIContent> lastOverElement = wrapper->mLastOverElement; + DispatchMouseOrPointerEvent(aMouseEvent, isPointer ? ePointerOut : eMouseOut, + lastOverElement, aMovingInto); + leaveDispatcher.Dispatch(); + + wrapper->mLastOverFrame = nullptr; + wrapper->mLastOverElement = nullptr; + + // Turn recursion protection back off + wrapper->mFirstOutEventElement = nullptr; +} + +void EventStateManager::RecomputeMouseEnterStateForRemoteFrame( + Element& aElement) { + if (!mMouseEnterLeaveHelper || + mMouseEnterLeaveHelper->mLastOverElement != &aElement) { + return; + } + + if (BrowserParent* remote = BrowserParent::GetFrom(&aElement)) { + remote->MouseEnterIntoWidget(); + } +} + +void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent, + nsIContent* aContent) { + NS_ASSERTION(aContent, "Mouse must be over something"); + + RefPtr<OverOutElementsWrapper> wrapper = GetWrapperByEventID(aMouseEvent); + + if (!wrapper || wrapper->mLastOverElement == aContent) return; + + // Before firing mouseover, check for recursion + if (aContent == wrapper->mFirstOverEventElement) return; + + // Check to see if we're a subdocument and if so update the parent + // document's ESM state to indicate that the mouse is over the + // content associated with our subdocument. + EnsureDocument(mPresContext); + if (Document* parentDoc = mDocument->GetInProcessParentDocument()) { + if (nsCOMPtr<nsIContent> docContent = mDocument->GetEmbedderElement()) { + if (PresShell* parentPresShell = parentDoc->GetPresShell()) { + RefPtr<EventStateManager> parentESM = + parentPresShell->GetPresContext()->EventStateManager(); + parentESM->NotifyMouseOver(aMouseEvent, docContent); + } + } + } + // Firing the DOM event in the parent document could cause all kinds + // of havoc. Reverify and take care. + if (wrapper->mLastOverElement == aContent) return; + + // Remember mLastOverElement as the related content for the + // DispatchMouseOrPointerEvent() call below, since NotifyMouseOut() resets it, + // bug 298477. + nsCOMPtr<nsIContent> lastOverElement = wrapper->mLastOverElement; + + bool isPointer = aMouseEvent->mClass == ePointerEventClass; + + EnterLeaveDispatcher enterDispatcher(this, aContent, lastOverElement, + aMouseEvent, + isPointer ? ePointerEnter : eMouseEnter); + + if (!isPointer) { + SetContentState(aContent, ElementState::HOVER); + } + + NotifyMouseOut(aMouseEvent, aContent); + + // Store the first mouseOver event we fire and don't refire mouseOver + // to that element while the first mouseOver is still ongoing. + wrapper->mFirstOverEventElement = aContent; + + // Fire mouseover + wrapper->mLastOverFrame = DispatchMouseOrPointerEvent( + aMouseEvent, isPointer ? ePointerOver : eMouseOver, aContent, + lastOverElement); + enterDispatcher.Dispatch(); + wrapper->mLastOverElement = aContent; + + // Turn recursion protection back off + wrapper->mFirstOverEventElement = nullptr; +} + +// Returns the center point of the window's client area. This is +// in widget coordinates, i.e. relative to the widget's top-left +// corner, not in screen coordinates, the same units that UIEvent:: +// refpoint is in. It may not be the exact center of the window if +// the platform requires rounding the coordinate. +static LayoutDeviceIntPoint GetWindowClientRectCenter(nsIWidget* aWidget) { + NS_ENSURE_TRUE(aWidget, LayoutDeviceIntPoint(0, 0)); + + LayoutDeviceIntRect rect = aWidget->GetClientBounds(); + LayoutDeviceIntPoint point(rect.x + rect.width / 2, rect.y + rect.height / 2); + int32_t round = aWidget->RoundsWidgetCoordinatesTo(); + point.x = point.x / round * round; + point.y = point.y / round * round; + return point - aWidget->WidgetToScreenOffset(); +} + +void EventStateManager::GeneratePointerEnterExit(EventMessage aMessage, + WidgetMouseEvent* aEvent) { + WidgetPointerEvent pointerEvent(*aEvent); + pointerEvent.mMessage = aMessage; + GenerateMouseEnterExit(&pointerEvent); +} + +/* static */ +void EventStateManager::UpdateLastRefPointOfMouseEvent( + WidgetMouseEvent* aMouseEvent) { + if (aMouseEvent->mMessage != eMouseMove && + aMouseEvent->mMessage != ePointerMove) { + return; + } + + // Mouse movement is reported on the MouseEvent.movement{X,Y} fields. + // Movement is calculated in UIEvent::GetMovementPoint() as: + // previous_mousemove_mRefPoint - current_mousemove_mRefPoint. + if (PointerLockManager::IsLocked() && aMouseEvent->mWidget) { + // The pointer is locked. If the pointer is not located at the center of + // the window, dispatch a synthetic mousemove to return the pointer there. + // Doing this between "real" pointer moves gives the impression that the + // (locked) pointer can continue moving and won't stop at the screen + // boundary. We cancel the synthetic event so that we don't end up + // dispatching the centering move event to content. + aMouseEvent->mLastRefPoint = + GetWindowClientRectCenter(aMouseEvent->mWidget); + + } else if (sLastRefPoint == kInvalidRefPoint) { + // We don't have a valid previous mousemove mRefPoint. This is either + // the first move we've encountered, or the mouse has just re-entered + // the application window. We should report (0,0) movement for this + // case, so make the current and previous mRefPoints the same. + aMouseEvent->mLastRefPoint = aMouseEvent->mRefPoint; + } else { + aMouseEvent->mLastRefPoint = sLastRefPoint; + } +} + +/* static */ +void EventStateManager::ResetPointerToWindowCenterWhilePointerLocked( + WidgetMouseEvent* aMouseEvent) { + MOZ_ASSERT(PointerLockManager::IsLocked()); + if ((aMouseEvent->mMessage != eMouseMove && + aMouseEvent->mMessage != ePointerMove) || + !aMouseEvent->mWidget) { + return; + } + + // We generate pointermove from mousemove event, so only synthesize native + // mouse move and update sSynthCenteringPoint by mousemove event. + bool updateSynthCenteringPoint = aMouseEvent->mMessage == eMouseMove; + + // The pointer is locked. If the pointer is not located at the center of + // the window, dispatch a synthetic mousemove to return the pointer there. + // Doing this between "real" pointer moves gives the impression that the + // (locked) pointer can continue moving and won't stop at the screen + // boundary. We cancel the synthetic event so that we don't end up + // dispatching the centering move event to content. + LayoutDeviceIntPoint center = GetWindowClientRectCenter(aMouseEvent->mWidget); + + if (aMouseEvent->mRefPoint != center && updateSynthCenteringPoint) { + // Mouse move doesn't finish at the center of the window. Dispatch a + // synthetic native mouse event to move the pointer back to the center + // of the window, to faciliate more movement. But first, record that + // we've dispatched a synthetic mouse movement, so we can cancel it + // in the other branch here. + sSynthCenteringPoint = center; + // XXX Once we fix XXX comments in SetPointerLock about this API, we could + // restrict that this API works only in the automation mode or in the + // pointer locked situation. + aMouseEvent->mWidget->SynthesizeNativeMouseMove( + center + aMouseEvent->mWidget->WidgetToScreenOffset(), nullptr); + } else if (aMouseEvent->mRefPoint == sSynthCenteringPoint) { + // This is the "synthetic native" event we dispatched to re-center the + // pointer. Cancel it so we don't expose the centering move to content. + aMouseEvent->StopPropagation(); + // Clear sSynthCenteringPoint so we don't cancel other events + // targeted at the center. + if (updateSynthCenteringPoint) { + sSynthCenteringPoint = kInvalidRefPoint; + } + } +} + +/* static */ +void EventStateManager::UpdateLastPointerPosition( + WidgetMouseEvent* aMouseEvent) { + if (aMouseEvent->mMessage != eMouseMove) { + return; + } + sLastRefPoint = aMouseEvent->mRefPoint; +} + +void EventStateManager::GenerateMouseEnterExit(WidgetMouseEvent* aMouseEvent) { + EnsureDocument(mPresContext); + if (!mDocument) return; + + // Hold onto old target content through the event and reset after. + nsCOMPtr<nsIContent> targetBeforeEvent = mCurrentTargetContent; + + switch (aMouseEvent->mMessage) { + case eMouseMove: + case ePointerMove: + case ePointerDown: + case ePointerGotCapture: { + // Get the target content target (mousemove target == mouseover target) + nsCOMPtr<nsIContent> targetElement = GetEventTargetContent(aMouseEvent); + if (!targetElement) { + // We're always over the document root, even if we're only + // over dead space in a page (whose frame is not associated with + // any content) or in print preview dead space + targetElement = mDocument->GetRootElement(); + } + if (targetElement) { + NotifyMouseOver(aMouseEvent, targetElement); + } + } break; + case ePointerUp: { + // Get the target content target (mousemove target == mouseover target) + nsCOMPtr<nsIContent> targetElement = GetEventTargetContent(aMouseEvent); + if (!targetElement) { + // We're always over the document root, even if we're only + // over dead space in a page (whose frame is not associated with + // any content) or in print preview dead space + targetElement = mDocument->GetRootElement(); + } + if (targetElement) { + RefPtr<OverOutElementsWrapper> helper = + GetWrapperByEventID(aMouseEvent); + if (helper) { + helper->mLastOverElement = targetElement; + } + NotifyMouseOut(aMouseEvent, nullptr); + } + } break; + case ePointerLeave: + case ePointerCancel: + case eMouseExitFromWidget: { + // This is actually the window mouse exit or pointer leave event. We're + // not moving into any new element. + + RefPtr<OverOutElementsWrapper> helper = GetWrapperByEventID(aMouseEvent); + if (helper && helper->mLastOverFrame && + nsContentUtils::GetTopLevelWidget(aMouseEvent->mWidget) != + nsContentUtils::GetTopLevelWidget( + helper->mLastOverFrame->GetNearestWidget())) { + // the Mouse/PointerOut event widget doesn't have same top widget with + // mLastOverFrame, it's a spurious event for mLastOverFrame + break; + } + + // Reset sLastRefPoint, so that we'll know not to report any + // movement the next time we re-enter the window. + sLastRefPoint = kInvalidRefPoint; + + NotifyMouseOut(aMouseEvent, nullptr); + } break; + default: + break; + } + + // reset mCurretTargetContent to what it was + mCurrentTargetContent = targetBeforeEvent; +} + +OverOutElementsWrapper* EventStateManager::GetWrapperByEventID( + WidgetMouseEvent* aEvent) { + WidgetPointerEvent* pointer = aEvent->AsPointerEvent(); + if (!pointer) { + MOZ_ASSERT(aEvent->AsMouseEvent() != nullptr); + if (!mMouseEnterLeaveHelper) { + mMouseEnterLeaveHelper = new OverOutElementsWrapper(); + } + return mMouseEnterLeaveHelper; + } + return mPointersEnterLeaveHelper.GetOrInsertNew(pointer->pointerId); +} + +/* static */ +void EventStateManager::SetPointerLock(nsIWidget* aWidget, + nsPresContext* aPresContext) { + // Reset mouse wheel transaction + WheelTransaction::EndTransaction(); + + // Deal with DnD events + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + + if (PointerLockManager::IsLocked()) { + MOZ_ASSERT(aWidget, "Locking pointer requires a widget"); + MOZ_ASSERT(aPresContext, "Locking pointer requires a presContext"); + + // Release all pointer capture when a pointer lock is successfully applied + // on an element. + PointerEventHandler::ReleaseAllPointerCapture(); + + // Store the last known ref point so we can reposition the pointer after + // unlock. + sPreLockScreenPoint = LayoutDeviceIntPoint::Round( + sLastScreenPoint * aPresContext->CSSToDevPixelScale()); + + // Fire a synthetic mouse move to ensure event state is updated. We first + // set the mouse to the center of the window, so that the mouse event + // doesn't report any movement. + // XXX Cannot we do synthesize the native mousemove in the parent process + // with calling LockNativePointer below? Then, we could make this API + // work only in the automation mode. + sLastRefPoint = GetWindowClientRectCenter(aWidget); + aWidget->SynthesizeNativeMouseMove( + sLastRefPoint + aWidget->WidgetToScreenOffset(), nullptr); + + // Suppress DnD + if (dragService) { + dragService->Suppress(); + } + + // Activate native pointer lock on platforms where it is required (Wayland) + aWidget->LockNativePointer(); + } else { + if (aWidget) { + // Deactivate native pointer lock on platforms where it is required + aWidget->UnlockNativePointer(); + } + + // Reset SynthCenteringPoint to invalid so that next time we start + // locking pointer, it has its initial value. + sSynthCenteringPoint = kInvalidRefPoint; + if (aWidget) { + // Unlocking, so return pointer to the original position by firing a + // synthetic mouse event. We first reset sLastRefPoint to its + // pre-pointerlock position, so that the synthetic mouse event reports + // no movement. + sLastRefPoint = sPreLockScreenPoint - aWidget->WidgetToScreenOffset(); + // XXX Cannot we do synthesize the native mousemove in the parent process + // with calling `UnlockNativePointer` above? Then, we could make this + // API work only in the automation mode. + aWidget->SynthesizeNativeMouseMove(sPreLockScreenPoint, nullptr); + } + + // Unsuppress DnD + if (dragService) { + dragService->Unsuppress(); + } + } +} + +void EventStateManager::GenerateDragDropEnterExit(nsPresContext* aPresContext, + WidgetDragEvent* aDragEvent) { + // Hold onto old target content through the event and reset after. + nsCOMPtr<nsIContent> targetBeforeEvent = mCurrentTargetContent; + + switch (aDragEvent->mMessage) { + case eDragOver: { + // when dragging from one frame to another, events are fired in the + // order: dragexit, dragenter, dragleave + if (sLastDragOverFrame != mCurrentTarget) { + // We'll need the content, too, to check if it changed separately from + // the frames. + nsCOMPtr<nsIContent> lastContent; + nsCOMPtr<nsIContent> targetContent; + mCurrentTarget->GetContentForEvent(aDragEvent, + getter_AddRefs(targetContent)); + if (targetContent && targetContent->IsText()) { + targetContent = targetContent->GetFlattenedTreeParent(); + } + + if (sLastDragOverFrame) { + // The frame has changed but the content may not have. Check before + // dispatching to content + sLastDragOverFrame->GetContentForEvent(aDragEvent, + getter_AddRefs(lastContent)); + if (lastContent && lastContent->IsText()) { + lastContent = lastContent->GetFlattenedTreeParent(); + } + + RefPtr<nsPresContext> presContext = sLastDragOverFrame->PresContext(); + FireDragEnterOrExit(presContext, aDragEvent, eDragExit, targetContent, + lastContent, sLastDragOverFrame); + nsIContent* target = sLastDragOverFrame + ? sLastDragOverFrame.GetFrame()->GetContent() + : nullptr; + // XXXedgar, look like we need to consider fission OOP iframe, too. + if (IsTopLevelRemoteTarget(target)) { + // Dragging something and moving from web content to chrome only + // fires dragexit and dragleave to xul:browser. We have to forward + // dragexit to sLastDragOverFrame when its content is a remote + // target. We don't forward dragleave since it's generated from + // dragexit. + WidgetDragEvent remoteEvent(aDragEvent->IsTrusted(), eDragExit, + aDragEvent->mWidget); + remoteEvent.AssignDragEventData(*aDragEvent, true); + remoteEvent.mFlags.mIsSynthesizedForTests = + aDragEvent->mFlags.mIsSynthesizedForTests; + nsEventStatus remoteStatus = nsEventStatus_eIgnore; + HandleCrossProcessEvent(&remoteEvent, &remoteStatus); + } + } + + AutoWeakFrame currentTraget = mCurrentTarget; + FireDragEnterOrExit(aPresContext, aDragEvent, eDragEnter, lastContent, + targetContent, currentTraget); + + if (sLastDragOverFrame) { + RefPtr<nsPresContext> presContext = sLastDragOverFrame->PresContext(); + FireDragEnterOrExit(presContext, aDragEvent, eDragLeave, + targetContent, lastContent, sLastDragOverFrame); + } + + sLastDragOverFrame = mCurrentTarget; + } + } break; + + case eDragExit: { + // This is actually the window mouse exit event. + if (sLastDragOverFrame) { + nsCOMPtr<nsIContent> lastContent; + sLastDragOverFrame->GetContentForEvent(aDragEvent, + getter_AddRefs(lastContent)); + + RefPtr<nsPresContext> lastDragOverFramePresContext = + sLastDragOverFrame->PresContext(); + FireDragEnterOrExit(lastDragOverFramePresContext, aDragEvent, eDragExit, + nullptr, lastContent, sLastDragOverFrame); + FireDragEnterOrExit(lastDragOverFramePresContext, aDragEvent, + eDragLeave, nullptr, lastContent, + sLastDragOverFrame); + + sLastDragOverFrame = nullptr; + } + } break; + + default: + break; + } + + // reset mCurretTargetContent to what it was + mCurrentTargetContent = targetBeforeEvent; + + // Now flush all pending notifications, for better responsiveness. + FlushLayout(aPresContext); +} + +void EventStateManager::FireDragEnterOrExit(nsPresContext* aPresContext, + WidgetDragEvent* aDragEvent, + EventMessage aMessage, + nsIContent* aRelatedTarget, + nsIContent* aTargetContent, + AutoWeakFrame& aTargetFrame) { + MOZ_ASSERT(aMessage == eDragLeave || aMessage == eDragExit || + aMessage == eDragEnter); + nsEventStatus status = nsEventStatus_eIgnore; + WidgetDragEvent event(aDragEvent->IsTrusted(), aMessage, aDragEvent->mWidget); + event.AssignDragEventData(*aDragEvent, false); + event.mFlags.mIsSynthesizedForTests = + aDragEvent->mFlags.mIsSynthesizedForTests; + event.mRelatedTarget = aRelatedTarget; + if (aMessage == eDragExit && !StaticPrefs::dom_event_dragexit_enabled()) { + event.mFlags.mOnlyChromeDispatch = true; + } + + mCurrentTargetContent = aTargetContent; + + if (aTargetContent != aRelatedTarget) { + // XXX This event should still go somewhere!! + if (aTargetContent) { + EventDispatcher::Dispatch(aTargetContent, aPresContext, &event, nullptr, + &status); + } + + // adjust the drag hover if the dragenter event was cancelled or this is a + // drag exit + if (status == nsEventStatus_eConsumeNoDefault || aMessage == eDragExit) { + SetContentState((aMessage == eDragEnter) ? aTargetContent : nullptr, + ElementState::DRAGOVER); + } + + // collect any changes to moz cursor settings stored in the event's + // data transfer. + UpdateDragDataTransfer(&event); + } + + // Finally dispatch the event to the frame + if (aTargetFrame) { + aTargetFrame->HandleEvent(aPresContext, &event, &status); + } +} + +void EventStateManager::UpdateDragDataTransfer(WidgetDragEvent* dragEvent) { + NS_ASSERTION(dragEvent, "drag event is null in UpdateDragDataTransfer!"); + if (!dragEvent->mDataTransfer) { + return; + } + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + + if (dragSession) { + // the initial dataTransfer is the one from the dragstart event that + // was set on the dragSession when the drag began. + RefPtr<DataTransfer> initialDataTransfer = dragSession->GetDataTransfer(); + if (initialDataTransfer) { + // retrieve the current moz cursor setting and save it. + nsAutoString mozCursor; + dragEvent->mDataTransfer->GetMozCursor(mozCursor); + initialDataTransfer->SetMozCursor(mozCursor); + } + } +} + +nsresult EventStateManager::SetClickCount(WidgetMouseEvent* aEvent, + nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget) { + nsCOMPtr<nsIContent> mouseContent = aOverrideClickTarget; + if (!mouseContent && mCurrentTarget) { + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(mouseContent)); + } + if (mouseContent && mouseContent->IsText()) { + nsINode* parent = mouseContent->GetFlattenedTreeParentNode(); + if (parent && parent->IsContent()) { + mouseContent = parent->AsContent(); + } + } + + switch (aEvent->mButton) { + case MouseButton::ePrimary: + if (aEvent->mMessage == eMouseDown) { + mLastLeftMouseDownContent = + !aEvent->mClickEventPrevented ? mouseContent : nullptr; + } else if (aEvent->mMessage == eMouseUp) { + aEvent->mClickTarget = + !aEvent->mClickEventPrevented + ? nsContentUtils::GetCommonAncestorUnderInteractiveContent( + mouseContent, mLastLeftMouseDownContent) + : nullptr; + if (aEvent->mClickTarget) { + aEvent->mClickCount = mLClickCount; + mLClickCount = 0; + } else { + aEvent->mClickCount = 0; + } + mLastLeftMouseDownContent = nullptr; + } + break; + + case MouseButton::eMiddle: + if (aEvent->mMessage == eMouseDown) { + mLastMiddleMouseDownContent = + !aEvent->mClickEventPrevented ? mouseContent : nullptr; + } else if (aEvent->mMessage == eMouseUp) { + aEvent->mClickTarget = + !aEvent->mClickEventPrevented + ? nsContentUtils::GetCommonAncestorUnderInteractiveContent( + mouseContent, mLastMiddleMouseDownContent) + : nullptr; + if (aEvent->mClickTarget) { + aEvent->mClickCount = mMClickCount; + mMClickCount = 0; + } else { + aEvent->mClickCount = 0; + } + mLastMiddleMouseDownContent = nullptr; + } + break; + + case MouseButton::eSecondary: + if (aEvent->mMessage == eMouseDown) { + mLastRightMouseDownContent = + !aEvent->mClickEventPrevented ? mouseContent : nullptr; + } else if (aEvent->mMessage == eMouseUp) { + aEvent->mClickTarget = + !aEvent->mClickEventPrevented + ? nsContentUtils::GetCommonAncestorUnderInteractiveContent( + mouseContent, mLastRightMouseDownContent) + : nullptr; + if (aEvent->mClickTarget) { + aEvent->mClickCount = mRClickCount; + mRClickCount = 0; + } else { + aEvent->mClickCount = 0; + } + mLastRightMouseDownContent = nullptr; + } + break; + } + + return NS_OK; +} + +// static +bool EventStateManager::EventCausesClickEvents( + const WidgetMouseEvent& aMouseEvent) { + if (NS_WARN_IF(aMouseEvent.mMessage != eMouseUp)) { + return false; + } + // If the mouseup event is synthesized event, we don't need to dispatch + // click events. + if (!aMouseEvent.IsReal()) { + return false; + } + // If mouse is still over same element, clickcount will be > 1. + // If it has moved it will be zero, so no click. + if (!aMouseEvent.mClickCount || !aMouseEvent.mClickTarget) { + return false; + } + // If click event was explicitly prevented, we shouldn't dispatch it. + if (aMouseEvent.mClickEventPrevented) { + return false; + } + // Check that the window isn't disabled before firing a click + // (see bug 366544). + return !(aMouseEvent.mWidget && !aMouseEvent.mWidget->IsEnabled()); +} + +nsresult EventStateManager::InitAndDispatchClickEvent( + WidgetMouseEvent* aMouseUpEvent, nsEventStatus* aStatus, + EventMessage aMessage, PresShell* aPresShell, nsIContent* aMouseUpContent, + AutoWeakFrame aCurrentTarget, bool aNoContentDispatch, + nsIContent* aOverrideClickTarget) { + MOZ_ASSERT(aMouseUpEvent); + MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); + MOZ_ASSERT(aMouseUpContent || aCurrentTarget || aOverrideClickTarget); + + WidgetMouseEvent event(aMouseUpEvent->IsTrusted(), aMessage, + aMouseUpEvent->mWidget, WidgetMouseEvent::eReal); + + event.mRefPoint = aMouseUpEvent->mRefPoint; + event.mClickCount = aMouseUpEvent->mClickCount; + event.mModifiers = aMouseUpEvent->mModifiers; + event.mButtons = aMouseUpEvent->mButtons; + event.mTimeStamp = aMouseUpEvent->mTimeStamp; + event.mFlags.mOnlyChromeDispatch = + aNoContentDispatch && !aMouseUpEvent->mUseLegacyNonPrimaryDispatch; + event.mFlags.mNoContentDispatch = aNoContentDispatch; + event.mButton = aMouseUpEvent->mButton; + event.pointerId = aMouseUpEvent->pointerId; + event.mInputSource = aMouseUpEvent->mInputSource; + nsIContent* target = aMouseUpContent; + nsIFrame* targetFrame = aCurrentTarget; + if (aOverrideClickTarget) { + target = aOverrideClickTarget; + targetFrame = aOverrideClickTarget->GetPrimaryFrame(); + } + + if (!target->IsInComposedDoc()) { + return NS_OK; + } + + // Use local event status for each click event dispatching since it'll be + // cleared by EventStateManager::PreHandleEvent(). Therefore, dispatching + // an event means that previous event status will be ignored. + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = aPresShell->HandleEventWithTarget( + &event, targetFrame, MOZ_KnownLive(target), &status); + + // Copy mMultipleActionsPrevented flag from a click event to the mouseup + // event only when it's set to true. It may be set to true if an editor has + // already handled it. This is important to avoid two or more default + // actions handled here. + aMouseUpEvent->mFlags.mMultipleActionsPrevented |= + event.mFlags.mMultipleActionsPrevented; + // If current status is nsEventStatus_eConsumeNoDefault, we don't need to + // overwrite it. + if (*aStatus == nsEventStatus_eConsumeNoDefault) { + return rv; + } + // If new status is nsEventStatus_eConsumeNoDefault or + // nsEventStatus_eConsumeDoDefault, use it. + if (status == nsEventStatus_eConsumeNoDefault || + status == nsEventStatus_eConsumeDoDefault) { + *aStatus = status; + return rv; + } + // Otherwise, keep the original status. + return rv; +} + +nsresult EventStateManager::PostHandleMouseUp( + WidgetMouseEvent* aMouseUpEvent, nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget) { + MOZ_ASSERT(aMouseUpEvent); + MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); + MOZ_ASSERT(aStatus); + + RefPtr<PresShell> presShell = mPresContext->GetPresShell(); + if (!presShell) { + return NS_OK; + } + + nsCOMPtr<nsIContent> clickTarget = + nsIContent::FromEventTargetOrNull(aMouseUpEvent->mClickTarget); + NS_ENSURE_STATE(clickTarget); + + // Fire click events if the event target is still available. + // Note that do not include the eMouseUp event's status since we ignore it + // for compatibility with the other browsers. + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, &status, + clickTarget, aOverrideClickTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Do not do anything if preceding click events are consumed. + // Note that Chromium dispatches "paste" event and actually pates clipboard + // text into focused editor even if the preceding click events are consumed. + // However, this is different from our traditional behavior and does not + // conform to DOM events. If we need to keep compatibility with Chromium, + // we should change it later. + if (status == nsEventStatus_eConsumeNoDefault) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return NS_OK; + } + + // Handle middle click paste if it's enabled and the mouse button is middle. + if (aMouseUpEvent->mButton != MouseButton::eMiddle || + !WidgetMouseEvent::IsMiddleClickPasteEnabled()) { + return NS_OK; + } + DebugOnly<nsresult> rvIgnored = + HandleMiddleClickPaste(presShell, aMouseUpEvent, &status, nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to paste for a middle click"); + + // If new status is nsEventStatus_eConsumeNoDefault or + // nsEventStatus_eConsumeDoDefault, use it. + if (*aStatus != nsEventStatus_eConsumeNoDefault && + (status == nsEventStatus_eConsumeNoDefault || + status == nsEventStatus_eConsumeDoDefault)) { + *aStatus = status; + } + + // Don't return error even if middle mouse paste fails since we haven't + // handled it here. + return NS_OK; +} + +nsresult EventStateManager::DispatchClickEvents( + PresShell* aPresShell, WidgetMouseEvent* aMouseUpEvent, + nsEventStatus* aStatus, nsIContent* aClickTarget, + nsIContent* aOverrideClickTarget) { + MOZ_ASSERT(aPresShell); + MOZ_ASSERT(aMouseUpEvent); + MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); + MOZ_ASSERT(aStatus); + MOZ_ASSERT(aClickTarget || aOverrideClickTarget); + + bool notDispatchToContents = + (aMouseUpEvent->mButton == MouseButton::eMiddle || + aMouseUpEvent->mButton == MouseButton::eSecondary); + + bool fireAuxClick = notDispatchToContents; + + AutoWeakFrame currentTarget = aClickTarget->GetPrimaryFrame(); + nsresult rv = InitAndDispatchClickEvent( + aMouseUpEvent, aStatus, eMouseClick, aPresShell, aClickTarget, + currentTarget, notDispatchToContents, aOverrideClickTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Fire auxclick event if necessary. + if (fireAuxClick && *aStatus != nsEventStatus_eConsumeNoDefault && + aClickTarget && aClickTarget->IsInComposedDoc()) { + rv = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseAuxClick, + aPresShell, aClickTarget, currentTarget, + false, aOverrideClickTarget); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch eMouseAuxClick"); + } + + // Fire double click event if click count is 2. + if (aMouseUpEvent->mClickCount == 2 && !fireAuxClick && aClickTarget && + aClickTarget->IsInComposedDoc()) { + rv = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseDoubleClick, + aPresShell, aClickTarget, currentTarget, + notDispatchToContents, aOverrideClickTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return rv; +} + +nsresult EventStateManager::HandleMiddleClickPaste( + PresShell* aPresShell, WidgetMouseEvent* aMouseEvent, + nsEventStatus* aStatus, EditorBase* aEditorBase) { + MOZ_ASSERT(aPresShell); + MOZ_ASSERT(aMouseEvent); + MOZ_ASSERT((aMouseEvent->mMessage == eMouseAuxClick && + aMouseEvent->mButton == MouseButton::eMiddle) || + EventCausesClickEvents(*aMouseEvent)); + MOZ_ASSERT(aStatus); + MOZ_ASSERT(*aStatus != nsEventStatus_eConsumeNoDefault); + + // Even if we're called twice or more for a mouse operation, we should + // handle only once. Although mMultipleActionsPrevented may be set to + // true by different event handler in the future, we can use it for now. + if (aMouseEvent->mFlags.mMultipleActionsPrevented) { + return NS_OK; + } + aMouseEvent->mFlags.mMultipleActionsPrevented = true; + + RefPtr<Selection> selection; + if (aEditorBase) { + selection = aEditorBase->GetSelection(); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + } else { + Document* document = aPresShell->GetDocument(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_FAILURE; + } + selection = nsCopySupport::GetSelectionForCopy(document); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + } + + // Don't modify selection here because we've already set caret to the point + // at "mousedown" event. + + int32_t clipboardType = nsIClipboard::kGlobalClipboard; + nsCOMPtr<nsIClipboard> clipboardService = + do_GetService("@mozilla.org/widget/clipboard;1"); + if (clipboardService && clipboardService->IsClipboardTypeSupported( + nsIClipboard::kSelectionClipboard)) { + clipboardType = nsIClipboard::kSelectionClipboard; + } + + // Fire ePaste event by ourselves since we need to dispatch "paste" event + // even if the middle click event was consumed for compatibility with + // Chromium. + if (!nsCopySupport::FireClipboardEvent(ePaste, clipboardType, aPresShell, + selection)) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return NS_OK; + } + + // Although we've fired "paste" event, there is no editor to accept the + // clipboard content. + if (!aEditorBase) { + return NS_OK; + } + + // Check if the editor is still the good target to paste. + if (aEditorBase->Destroyed() || aEditorBase->IsReadonly()) { + // XXX Should we consume the event when the editor is readonly and/or + // disabled? + return NS_OK; + } + + // The selection may have been modified during reflow. Therefore, we + // should adjust event target to pass IsAcceptableInputEvent(). + const nsRange* range = selection->GetRangeAt(0); + if (!range) { + return NS_OK; + } + WidgetMouseEvent mouseEvent(*aMouseEvent); + mouseEvent.mOriginalTarget = range->GetStartContainer(); + if (NS_WARN_IF(!mouseEvent.mOriginalTarget) || + !aEditorBase->IsAcceptableInputEvent(&mouseEvent)) { + return NS_OK; + } + + // If Control key is pressed, we should paste clipboard content as + // quotation. Otherwise, paste it as is. + if (aMouseEvent->IsControl()) { + DebugOnly<nsresult> rv = aEditorBase->PasteAsQuotationAsAction( + clipboardType, EditorBase::DispatchPasteEvent::No); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to paste as quotation"); + } else { + DebugOnly<nsresult> rv = aEditorBase->PasteAsAction( + clipboardType, EditorBase::DispatchPasteEvent::No); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to paste"); + } + *aStatus = nsEventStatus_eConsumeNoDefault; + + return NS_OK; +} + +void EventStateManager::ConsumeInteractionData( + Record<nsString, dom::InteractionData>& aInteractions) { + OnTypingInteractionEnded(); + + aInteractions.Entries().Clear(); + auto newEntry = aInteractions.Entries().AppendElement(); + newEntry->mKey = u"Typing"_ns; + newEntry->mValue = gTypingInteraction; + gTypingInteraction = {}; +} + +nsIFrame* EventStateManager::GetEventTarget() { + PresShell* presShell; + if (mCurrentTarget || !mPresContext || + !(presShell = mPresContext->GetPresShell())) { + return mCurrentTarget; + } + + if (mCurrentTargetContent) { + mCurrentTarget = mPresContext->GetPrimaryFrameFor(mCurrentTargetContent); + if (mCurrentTarget) { + return mCurrentTarget; + } + } + + nsIFrame* frame = presShell->GetCurrentEventFrame(); + return (mCurrentTarget = frame); +} + +already_AddRefed<nsIContent> EventStateManager::GetEventTargetContent( + WidgetEvent* aEvent) { + if (aEvent && (aEvent->mMessage == eFocus || aEvent->mMessage == eBlur)) { + nsCOMPtr<nsIContent> content = GetFocusedElement(); + return content.forget(); + } + + if (mCurrentTargetContent) { + nsCOMPtr<nsIContent> content = mCurrentTargetContent; + return content.forget(); + } + + nsCOMPtr<nsIContent> content; + if (PresShell* presShell = mPresContext->GetPresShell()) { + content = presShell->GetEventTargetContent(aEvent); + } + + // Some events here may set mCurrentTarget but not set the corresponding + // event target in the PresShell. + if (!content && mCurrentTarget) { + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(content)); + } + + return content.forget(); +} + +static Element* GetLabelTarget(nsIContent* aPossibleLabel) { + mozilla::dom::HTMLLabelElement* label = + mozilla::dom::HTMLLabelElement::FromNode(aPossibleLabel); + if (!label) return nullptr; + + return label->GetLabeledElement(); +} + +/* static */ +inline void EventStateManager::DoStateChange(Element* aElement, + ElementState aState, + bool aAddState) { + if (aAddState) { + aElement->AddStates(aState); + } else { + aElement->RemoveStates(aState); + } +} + +/* static */ +inline void EventStateManager::DoStateChange(nsIContent* aContent, + ElementState aState, + bool aStateAdded) { + if (aContent->IsElement()) { + DoStateChange(aContent->AsElement(), aState, aStateAdded); + } +} + +/* static */ +void EventStateManager::UpdateAncestorState(nsIContent* aStartNode, + nsIContent* aStopBefore, + ElementState aState, + bool aAddState) { + for (; aStartNode && aStartNode != aStopBefore; + aStartNode = aStartNode->GetFlattenedTreeParent()) { + // We might be starting with a non-element (e.g. a text node) and + // if someone is doing something weird might be ending with a + // non-element too (e.g. a document fragment) + if (!aStartNode->IsElement()) { + continue; + } + Element* element = aStartNode->AsElement(); + DoStateChange(element, aState, aAddState); + Element* labelTarget = GetLabelTarget(element); + if (labelTarget) { + DoStateChange(labelTarget, aState, aAddState); + } + } + + if (aAddState) { + // We might be in a situation where a node was in hover both + // because it was hovered and because the label for it was + // hovered, and while we stopped hovering the node the label is + // still hovered. Or we might have had two nested labels for the + // same node, and while one is no longer hovered the other still + // is. In that situation, the label that's still hovered will be + // aStopBefore or some ancestor of it, and the call we just made + // to UpdateAncestorState with aAddState = false would have + // removed the hover state from the node. But the node should + // still be in hover state. To handle this situation we need to + // keep walking up the tree and any time we find a label mark its + // corresponding node as still in our state. + for (; aStartNode; aStartNode = aStartNode->GetFlattenedTreeParent()) { + if (!aStartNode->IsElement()) { + continue; + } + + Element* labelTarget = GetLabelTarget(aStartNode->AsElement()); + if (labelTarget && !labelTarget->State().HasState(aState)) { + DoStateChange(labelTarget, aState, true); + } + } + } +} + +// static +bool CanContentHaveActiveState(nsIContent& aContent) { + // Editable content can never become active since their default actions + // are disabled. Watch out for editable content in native anonymous + // subtrees though, as they belong to text controls. + return !aContent.IsEditable() || aContent.IsInNativeAnonymousSubtree(); +} + +bool EventStateManager::SetContentState(nsIContent* aContent, + ElementState aState) { + MOZ_ASSERT(ManagesState(aState), "Unexpected state"); + + nsCOMPtr<nsIContent> notifyContent1; + nsCOMPtr<nsIContent> notifyContent2; + bool updateAncestors; + + if (aState == ElementState::HOVER || aState == ElementState::ACTIVE) { + // Hover and active are hierarchical + updateAncestors = true; + + // check to see that this state is allowed by style. Check dragover too? + // XXX Is this even what we want? + if (mCurrentTarget && + mCurrentTarget->StyleUI()->UserInput() == StyleUserInput::None) { + return false; + } + + if (aState == ElementState::ACTIVE) { + if (aContent && !CanContentHaveActiveState(*aContent)) { + aContent = nullptr; + } + if (aContent != mActiveContent) { + notifyContent1 = aContent; + notifyContent2 = mActiveContent; + mActiveContent = aContent; + } + } else { + NS_ASSERTION(aState == ElementState::HOVER, "How did that happen?"); + nsIContent* newHover; + + if (mPresContext->IsDynamic()) { + newHover = aContent; + } else { + NS_ASSERTION(!aContent || aContent->GetComposedDoc() == + mPresContext->PresShell()->GetDocument(), + "Unexpected document"); + nsIFrame* frame = aContent ? aContent->GetPrimaryFrame() : nullptr; + if (frame && nsLayoutUtils::IsViewportScrollbarFrame(frame)) { + // The scrollbars of viewport should not ignore the hover state. + // Because they are *not* the content of the web page. + newHover = aContent; + } else { + // All contents of the web page should ignore the hover state. + newHover = nullptr; + } + } + + if (newHover != mHoverContent) { + notifyContent1 = newHover; + notifyContent2 = mHoverContent; + mHoverContent = newHover; + } + } + } else { + updateAncestors = false; + if (aState == ElementState::DRAGOVER) { + if (aContent != sDragOverContent) { + notifyContent1 = aContent; + notifyContent2 = sDragOverContent; + sDragOverContent = aContent; + } + } else if (aState == ElementState::URLTARGET) { + if (aContent != mURLTargetContent) { + notifyContent1 = aContent; + notifyContent2 = mURLTargetContent; + mURLTargetContent = aContent; + } + } + } + + // We need to keep track of which of notifyContent1 and notifyContent2 is + // getting the state set and which is getting it unset. If both are + // non-null, then notifyContent1 is having the state set and notifyContent2 + // is having it unset. But if one of them is null, we need to keep track of + // the right thing for notifyContent1 explicitly. + bool content1StateSet = true; + if (!notifyContent1) { + // This is ok because FindCommonAncestor wouldn't find anything + // anyway if notifyContent1 is null. + notifyContent1 = notifyContent2; + notifyContent2 = nullptr; + content1StateSet = false; + } + + if (notifyContent1 && mPresContext) { + EnsureDocument(mPresContext); + if (mDocument) { + nsAutoScriptBlocker scriptBlocker; + + if (updateAncestors) { + nsCOMPtr<nsIContent> commonAncestor = + FindCommonAncestor(notifyContent1, notifyContent2); + if (notifyContent2) { + // It's very important to first notify the state removal and + // then the state addition, because due to labels it's + // possible that we're removing state from some element but + // then adding it again (say because mHoverContent changed + // from a control to its label). + UpdateAncestorState(notifyContent2, commonAncestor, aState, false); + } + UpdateAncestorState(notifyContent1, commonAncestor, aState, + content1StateSet); + } else { + if (notifyContent2) { + DoStateChange(notifyContent2, aState, false); + } + DoStateChange(notifyContent1, aState, content1StateSet); + } + } + } + + return true; +} + +void EventStateManager::ResetLastOverForContent( + const uint32_t& aIdx, const RefPtr<OverOutElementsWrapper>& aElemWrapper, + nsIContent* aContent) { + if (aElemWrapper && aElemWrapper->mLastOverElement && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + aElemWrapper->mLastOverElement, aContent)) { + aElemWrapper->mLastOverElement = nullptr; + } +} + +void EventStateManager::RemoveNodeFromChainIfNeeded(ElementState aState, + nsIContent* aContentRemoved, + bool aNotify) { + MOZ_ASSERT(aState == ElementState::HOVER || aState == ElementState::ACTIVE); + if (!aContentRemoved->IsElement() || + !aContentRemoved->AsElement()->State().HasState(aState)) { + return; + } + + nsCOMPtr<nsIContent>& leaf = + aState == ElementState::HOVER ? mHoverContent : mActiveContent; + + MOZ_ASSERT(leaf); + // These two NS_ASSERTIONS below can fail for Shadow DOM sometimes, and it's + // not clear how to best handle it, see + // https://github.com/whatwg/html/issues/4795 and bug 1551621. + NS_ASSERTION( + nsContentUtils::ContentIsFlattenedTreeDescendantOf(leaf, aContentRemoved), + "Flat tree and active / hover chain got out of sync"); + + nsIContent* newLeaf = aContentRemoved->GetFlattenedTreeParent(); + MOZ_ASSERT(!newLeaf || newLeaf->IsElement()); + NS_ASSERTION(!newLeaf || newLeaf->AsElement()->State().HasState(aState), + "State got out of sync because of shadow DOM"); + if (aNotify) { + SetContentState(newLeaf, aState); + } else { + // We don't update the removed content's state here, since removing NAC + // happens from layout and we don't really want to notify at that point or + // what not. + // + // Also, NAC is not observable and NAC being removed will go away soon. + leaf = newLeaf; + } + MOZ_ASSERT(leaf == newLeaf || (aState == ElementState::ACTIVE && !leaf && + !CanContentHaveActiveState(*newLeaf))); +} + +void EventStateManager::NativeAnonymousContentRemoved(nsIContent* aContent) { + MOZ_ASSERT(aContent->IsRootOfNativeAnonymousSubtree()); + RemoveNodeFromChainIfNeeded(ElementState::HOVER, aContent, false); + RemoveNodeFromChainIfNeeded(ElementState::ACTIVE, aContent, false); + + if (mLastLeftMouseDownContent && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mLastLeftMouseDownContent, aContent)) { + mLastLeftMouseDownContent = aContent->GetFlattenedTreeParent(); + } + + if (mLastMiddleMouseDownContent && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mLastMiddleMouseDownContent, aContent)) { + mLastMiddleMouseDownContent = aContent->GetFlattenedTreeParent(); + } + + if (mLastRightMouseDownContent && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mLastRightMouseDownContent, aContent)) { + mLastRightMouseDownContent = aContent->GetFlattenedTreeParent(); + } +} + +void EventStateManager::ContentRemoved(Document* aDocument, + nsIContent* aContent) { + /* + * Anchor and area elements when focused or hovered might make the UI to show + * the current link. We want to make sure that the UI gets informed when they + * are actually removed from the DOM. + */ + if (aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area) && + (aContent->AsElement()->State().HasAtLeastOneOfStates( + ElementState::FOCUS | ElementState::HOVER))) { + Element* element = aContent->AsElement(); + element->LeaveLink(element->GetPresContext(Element::eForComposedDoc)); + } + + if (aContent->IsElement()) { + if (RefPtr<nsPresContext> presContext = mPresContext) { + IMEStateManager::OnRemoveContent(*presContext, + MOZ_KnownLive(*aContent->AsElement())); + } + WheelTransaction::OnRemoveElement(aContent); + } + + // inform the focus manager that the content is being removed. If this + // content is focused, the focus will be removed without firing events. + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + fm->ContentRemoved(aDocument, aContent); + } + + RemoveNodeFromChainIfNeeded(ElementState::HOVER, aContent, true); + RemoveNodeFromChainIfNeeded(ElementState::ACTIVE, aContent, true); + + if (sDragOverContent && + sDragOverContent->OwnerDoc() == aContent->OwnerDoc() && + nsContentUtils::ContentIsFlattenedTreeDescendantOf(sDragOverContent, + aContent)) { + sDragOverContent = nullptr; + } + + PointerEventHandler::ReleaseIfCaptureByDescendant(aContent); + + // See bug 292146 for why we want to null this out + ResetLastOverForContent(0, mMouseEnterLeaveHelper, aContent); + for (const auto& entry : mPointersEnterLeaveHelper) { + ResetLastOverForContent(entry.GetKey(), entry.GetData(), aContent); + } +} + +void EventStateManager::TextControlRootWillBeRemoved( + TextControlElement& aTextControlElement) { + if (!mGestureDownInTextControl || !mGestureDownFrameOwner || + !mGestureDownFrameOwner->IsInNativeAnonymousSubtree()) { + return; + } + // If we track gesture to start drag in aTextControlElement, we should keep + // tracking it with aTextContrlElement itself for now because this may be + // caused by reframing aTextControlElement which may not be intended by the + // user. + if (&aTextControlElement == + mGestureDownFrameOwner + ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) { + mGestureDownFrameOwner = &aTextControlElement; + } +} + +void EventStateManager::TextControlRootAdded( + Element& aAnonymousDivElement, TextControlElement& aTextControlElement) { + if (!mGestureDownInTextControl || + mGestureDownFrameOwner != &aTextControlElement) { + return; + } + // If we track gesture to start drag in aTextControlElement, but the frame + // owner is the text control element itself, the anonymous nodes in it are + // recreated by a reframe. If so, we should keep tracking it with the + // recreated native anonymous node. + mGestureDownFrameOwner = + aAnonymousDivElement.GetFirstChild() + ? aAnonymousDivElement.GetFirstChild() + : static_cast<nsIContent*>(&aAnonymousDivElement); +} + +bool EventStateManager::EventStatusOK(WidgetGUIEvent* aEvent) { + return !(aEvent->mMessage == eMouseDown && + aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary && + !sNormalLMouseEventInProcess); +} + +//------------------------------------------- +// Access Key Registration +//------------------------------------------- +void EventStateManager::RegisterAccessKey(Element* aElement, uint32_t aKey) { + if (aElement && !mAccessKeys.Contains(aElement)) { + mAccessKeys.AppendObject(aElement); + } +} + +void EventStateManager::UnregisterAccessKey(Element* aElement, uint32_t aKey) { + if (aElement) { + mAccessKeys.RemoveObject(aElement); + } +} + +uint32_t EventStateManager::GetRegisteredAccessKey(Element* aElement) { + MOZ_ASSERT(aElement); + + if (!mAccessKeys.Contains(aElement)) { + return 0; + } + + nsAutoString accessKey; + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, accessKey); + return accessKey.First(); +} + +void EventStateManager::EnsureDocument(nsPresContext* aPresContext) { + if (!mDocument) mDocument = aPresContext->Document(); +} + +void EventStateManager::FlushLayout(nsPresContext* aPresContext) { + MOZ_ASSERT(aPresContext, "nullptr ptr"); + if (RefPtr<PresShell> presShell = aPresContext->GetPresShell()) { + presShell->FlushPendingNotifications(FlushType::InterruptibleLayout); + } +} + +Element* EventStateManager::GetFocusedElement() { + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + EnsureDocument(mPresContext); + if (!fm || !mDocument) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + return nsFocusManager::GetFocusedDescendant( + mDocument->GetWindow(), nsFocusManager::eOnlyCurrentWindow, + getter_AddRefs(focusedWindow)); +} + +//------------------------------------------------------- +// Return true if the docshell is visible + +bool EventStateManager::IsShellVisible(nsIDocShell* aShell) { + NS_ASSERTION(aShell, "docshell is null"); + + nsCOMPtr<nsIBaseWindow> basewin = do_QueryInterface(aShell); + if (!basewin) return true; + + bool isVisible = true; + basewin->GetVisibility(&isVisible); + + // We should be doing some additional checks here so that + // we don't tab into hidden tabs of tabbrowser. -bryner + + return isVisible; +} + +nsresult EventStateManager::DoContentCommandEvent( + WidgetContentCommandEvent* aEvent) { + EnsureDocument(mPresContext); + NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE); + nsCOMPtr<nsPIDOMWindowOuter> window(mDocument->GetWindow()); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + nsCOMPtr<nsPIWindowRoot> root = window->GetTopWindowRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + const char* cmd; + switch (aEvent->mMessage) { + case eContentCommandCut: + cmd = "cmd_cut"; + break; + case eContentCommandCopy: + cmd = "cmd_copy"; + break; + case eContentCommandPaste: + cmd = "cmd_paste"; + break; + case eContentCommandDelete: + cmd = "cmd_delete"; + break; + case eContentCommandUndo: + cmd = "cmd_undo"; + break; + case eContentCommandRedo: + cmd = "cmd_redo"; + break; + case eContentCommandPasteTransferable: + cmd = "cmd_pasteTransferable"; + break; + case eContentCommandLookUpDictionary: + cmd = "cmd_lookUpDictionary"; + break; + default: + return NS_ERROR_NOT_IMPLEMENTED; + } + // If user tries to do something, user must try to do it in visible window. + // So, let's retrieve controller of visible window. + nsCOMPtr<nsIController> controller; + nsresult rv = + root->GetControllerForCommand(cmd, true, getter_AddRefs(controller)); + NS_ENSURE_SUCCESS(rv, rv); + if (!controller) { + // When GetControllerForCommand succeeded but there is no controller, the + // command isn't supported. + aEvent->mIsEnabled = false; + } else { + bool canDoIt; + rv = controller->IsCommandEnabled(cmd, &canDoIt); + NS_ENSURE_SUCCESS(rv, rv); + aEvent->mIsEnabled = canDoIt; + if (canDoIt && !aEvent->mOnlyEnabledCheck) { + switch (aEvent->mMessage) { + case eContentCommandPasteTransferable: { + BrowserParent* remote = BrowserParent::GetFocused(); + if (remote) { + nsCOMPtr<nsITransferable> transferable = aEvent->mTransferable; + IPCTransferableData ipcTransferableData; + nsContentUtils::TransferableToIPCTransferableData( + transferable, &ipcTransferableData, false, remote->Manager()); + bool isPrivateData = transferable->GetIsPrivateData(); + nsCOMPtr<nsIPrincipal> requestingPrincipal = + transferable->GetRequestingPrincipal(); + nsContentPolicyType contentPolicyType = + transferable->GetContentPolicyType(); + remote->SendPasteTransferable(std::move(ipcTransferableData), + isPrivateData, requestingPrincipal, + contentPolicyType); + rv = NS_OK; + } else { + nsCOMPtr<nsICommandController> commandController = + do_QueryInterface(controller); + NS_ENSURE_STATE(commandController); + + RefPtr<nsCommandParams> params = new nsCommandParams(); + rv = params->SetISupports("transferable", aEvent->mTransferable); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = commandController->DoCommandWithParams(cmd, params); + } + break; + } + + case eContentCommandLookUpDictionary: { + nsCOMPtr<nsICommandController> commandController = + do_QueryInterface(controller); + if (NS_WARN_IF(!commandController)) { + return NS_ERROR_FAILURE; + } + + RefPtr<nsCommandParams> params = new nsCommandParams(); + rv = params->SetInt("x", aEvent->mRefPoint.x); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = params->SetInt("y", aEvent->mRefPoint.y); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = commandController->DoCommandWithParams(cmd, params); + break; + } + + default: + rv = controller->DoCommand(cmd); + break; + } + NS_ENSURE_SUCCESS(rv, rv); + } + } + aEvent->mSucceeded = true; + return NS_OK; +} + +nsresult EventStateManager::DoContentCommandInsertTextEvent( + WidgetContentCommandEvent* aEvent) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aEvent->mMessage == eContentCommandInsertText); + MOZ_DIAGNOSTIC_ASSERT(aEvent->mString.isSome()); + MOZ_DIAGNOSTIC_ASSERT(!aEvent->mString.ref().IsEmpty()); + + aEvent->mIsEnabled = false; + aEvent->mSucceeded = false; + + NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); + + if (XRE_IsParentProcess()) { + // Handle it in focused content process if there is. + if (BrowserParent* remote = BrowserParent::GetFocused()) { + remote->SendInsertText(aEvent->mString.ref()); + aEvent->mIsEnabled = true; // XXX it can be a lie... + aEvent->mSucceeded = true; + return NS_OK; + } + } + + // If there is no active editor in this process, we should treat the command + // is disabled. + RefPtr<EditorBase> activeEditor = + nsContentUtils::GetActiveEditor(mPresContext); + if (!activeEditor) { + aEvent->mSucceeded = true; + return NS_OK; + } + + nsresult rv = activeEditor->InsertTextAsAction(aEvent->mString.ref()); + aEvent->mIsEnabled = rv != NS_SUCCESS_DOM_NO_OPERATION; + aEvent->mSucceeded = NS_SUCCEEDED(rv); + return NS_OK; +} + +nsresult EventStateManager::DoContentCommandScrollEvent( + WidgetContentCommandEvent* aEvent) { + NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); + PresShell* presShell = mPresContext->GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(aEvent->mScroll.mAmount != 0, NS_ERROR_INVALID_ARG); + + ScrollUnit scrollUnit; + switch (aEvent->mScroll.mUnit) { + case WidgetContentCommandEvent::eCmdScrollUnit_Line: + scrollUnit = ScrollUnit::LINES; + break; + case WidgetContentCommandEvent::eCmdScrollUnit_Page: + scrollUnit = ScrollUnit::PAGES; + break; + case WidgetContentCommandEvent::eCmdScrollUnit_Whole: + scrollUnit = ScrollUnit::WHOLE; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + aEvent->mSucceeded = true; + + nsIScrollableFrame* sf = + presShell->GetScrollableFrameToScroll(layers::EitherScrollDirection); + aEvent->mIsEnabled = + sf ? (aEvent->mScroll.mIsHorizontal ? WheelHandlingUtils::CanScrollOn( + sf, aEvent->mScroll.mAmount, 0) + : WheelHandlingUtils::CanScrollOn( + sf, 0, aEvent->mScroll.mAmount)) + : false; + + if (!aEvent->mIsEnabled || aEvent->mOnlyEnabledCheck) { + return NS_OK; + } + + nsIntPoint pt(0, 0); + if (aEvent->mScroll.mIsHorizontal) { + pt.x = aEvent->mScroll.mAmount; + } else { + pt.y = aEvent->mScroll.mAmount; + } + + // The caller may want synchronous scrolling. + sf->ScrollBy(pt, scrollUnit, ScrollMode::Instant); + return NS_OK; +} + +void EventStateManager::SetActiveManager(EventStateManager* aNewESM, + nsIContent* aContent) { + if (sActiveESM && aNewESM != sActiveESM) { + sActiveESM->SetContentState(nullptr, ElementState::ACTIVE); + } + sActiveESM = aNewESM; + if (sActiveESM && aContent) { + sActiveESM->SetContentState(aContent, ElementState::ACTIVE); + } +} + +void EventStateManager::ClearGlobalActiveContent(EventStateManager* aClearer) { + if (aClearer) { + aClearer->SetContentState(nullptr, ElementState::ACTIVE); + if (sDragOverContent) { + aClearer->SetContentState(nullptr, ElementState::DRAGOVER); + } + } + if (sActiveESM && aClearer != sActiveESM) { + sActiveESM->SetContentState(nullptr, ElementState::ACTIVE); + } + sActiveESM = nullptr; +} + +/******************************************************************/ +/* mozilla::EventStateManager::DeltaAccumulator */ +/******************************************************************/ + +void EventStateManager::DeltaAccumulator::InitLineOrPageDelta( + nsIFrame* aTargetFrame, EventStateManager* aESM, WidgetWheelEvent* aEvent) { + MOZ_ASSERT(aESM); + MOZ_ASSERT(aEvent); + + // Reset if the previous wheel event is too old. + if (!mLastTime.IsNull()) { + TimeDuration duration = TimeStamp::Now() - mLastTime; + if (duration.ToMilliseconds() > + StaticPrefs::mousewheel_transaction_timeout()) { + Reset(); + } + } + // If we have accumulated delta, we may need to reset it. + if (IsInTransaction()) { + // If wheel event type is changed, reset the values. + if (mHandlingDeltaMode != aEvent->mDeltaMode || + mIsNoLineOrPageDeltaDevice != aEvent->mIsNoLineOrPageDelta) { + Reset(); + } else { + // If the delta direction is changed, we should reset only the + // accumulated values. + if (mX && aEvent->mDeltaX && ((aEvent->mDeltaX > 0.0) != (mX > 0.0))) { + mX = mPendingScrollAmountX = 0.0; + } + if (mY && aEvent->mDeltaY && ((aEvent->mDeltaY > 0.0) != (mY > 0.0))) { + mY = mPendingScrollAmountY = 0.0; + } + } + } + + mHandlingDeltaMode = aEvent->mDeltaMode; + mIsNoLineOrPageDeltaDevice = aEvent->mIsNoLineOrPageDelta; + + { + nsIFrame* frame = aESM->ComputeScrollTarget(aTargetFrame, aEvent, + COMPUTE_DEFAULT_ACTION_TARGET); + nsPresContext* pc = + frame ? frame->PresContext() : aTargetFrame->PresContext(); + nsIScrollableFrame* scrollTarget = do_QueryFrame(frame); + aEvent->mScrollAmount = aESM->GetScrollAmount(pc, aEvent, scrollTarget); + } + + // If it's handling neither a device that does not provide line or page deltas + // nor delta values multiplied by prefs, we must not modify lineOrPageDelta + // values. + // TODO(emilio): Does this care about overridden scroll speed? + if (!mIsNoLineOrPageDeltaDevice && + !EventStateManager::WheelPrefs::GetInstance() + ->NeedToComputeLineOrPageDelta(aEvent)) { + // Set the delta values to mX and mY. They would be used when above block + // resets mX/mY/mPendingScrollAmountX/mPendingScrollAmountY if the direction + // is changed. + // NOTE: We shouldn't accumulate the delta values, it might could cause + // overflow even though it's not a realistic situation. + if (aEvent->mDeltaX) { + mX = aEvent->mDeltaX; + } + if (aEvent->mDeltaY) { + mY = aEvent->mDeltaY; + } + mLastTime = TimeStamp::Now(); + return; + } + + mX += aEvent->mDeltaX; + mY += aEvent->mDeltaY; + + if (mHandlingDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL) { + // Records pixel delta values and init mLineOrPageDeltaX and + // mLineOrPageDeltaY for wheel events which are caused by pixel only + // devices. Ignore mouse wheel transaction for computing this. The + // lineOrPageDelta values will be used by dispatching legacy + // eMouseScrollEventClass (DOMMouseScroll) but not be used for scrolling + // of default action. The transaction should be used only for the default + // action. + auto scrollAmountInCSSPixels = + CSSIntSize::FromAppUnitsRounded(aEvent->mScrollAmount); + + aEvent->mLineOrPageDeltaX = RoundDown(mX) / scrollAmountInCSSPixels.width; + aEvent->mLineOrPageDeltaY = RoundDown(mY) / scrollAmountInCSSPixels.height; + + mX -= aEvent->mLineOrPageDeltaX * scrollAmountInCSSPixels.width; + mY -= aEvent->mLineOrPageDeltaY * scrollAmountInCSSPixels.height; + } else { + aEvent->mLineOrPageDeltaX = RoundDown(mX); + aEvent->mLineOrPageDeltaY = RoundDown(mY); + mX -= aEvent->mLineOrPageDeltaX; + mY -= aEvent->mLineOrPageDeltaY; + } + + mLastTime = TimeStamp::Now(); +} + +void EventStateManager::DeltaAccumulator::Reset() { + mX = mY = 0.0; + mPendingScrollAmountX = mPendingScrollAmountY = 0.0; + mHandlingDeltaMode = UINT32_MAX; + mIsNoLineOrPageDeltaDevice = false; +} + +nsIntPoint +EventStateManager::DeltaAccumulator::ComputeScrollAmountForDefaultAction( + WidgetWheelEvent* aEvent, const nsIntSize& aScrollAmountInDevPixels) { + MOZ_ASSERT(aEvent); + + DeltaValues acceleratedDelta = WheelTransaction::AccelerateWheelDelta(aEvent); + + nsIntPoint result(0, 0); + if (aEvent->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL) { + mPendingScrollAmountX += acceleratedDelta.deltaX; + mPendingScrollAmountY += acceleratedDelta.deltaY; + } else { + mPendingScrollAmountX += + aScrollAmountInDevPixels.width * acceleratedDelta.deltaX; + mPendingScrollAmountY += + aScrollAmountInDevPixels.height * acceleratedDelta.deltaY; + } + result.x = RoundDown(mPendingScrollAmountX); + result.y = RoundDown(mPendingScrollAmountY); + mPendingScrollAmountX -= result.x; + mPendingScrollAmountY -= result.y; + + return result; +} + +/******************************************************************/ +/* mozilla::EventStateManager::WheelPrefs */ +/******************************************************************/ + +// static +EventStateManager::WheelPrefs* EventStateManager::WheelPrefs::GetInstance() { + if (!sInstance) { + sInstance = new WheelPrefs(); + } + return sInstance; +} + +// static +void EventStateManager::WheelPrefs::Shutdown() { + delete sInstance; + sInstance = nullptr; +} + +// static +void EventStateManager::WheelPrefs::OnPrefChanged(const char* aPrefName, + void* aClosure) { + // forget all prefs, it's not problem for performance. + sInstance->Reset(); + DeltaAccumulator::GetInstance()->Reset(); +} + +EventStateManager::WheelPrefs::WheelPrefs() { + Reset(); + Preferences::RegisterPrefixCallback(OnPrefChanged, "mousewheel."); +} + +EventStateManager::WheelPrefs::~WheelPrefs() { + Preferences::UnregisterPrefixCallback(OnPrefChanged, "mousewheel."); +} + +void EventStateManager::WheelPrefs::Reset() { memset(mInit, 0, sizeof(mInit)); } + +EventStateManager::WheelPrefs::Index EventStateManager::WheelPrefs::GetIndexFor( + const WidgetWheelEvent* aEvent) { + if (!aEvent) { + return INDEX_DEFAULT; + } + + Modifiers modifiers = + (aEvent->mModifiers & (MODIFIER_ALT | MODIFIER_CONTROL | MODIFIER_META | + MODIFIER_SHIFT | MODIFIER_OS)); + + switch (modifiers) { + case MODIFIER_ALT: + return INDEX_ALT; + case MODIFIER_CONTROL: + return INDEX_CONTROL; + case MODIFIER_META: + return INDEX_META; + case MODIFIER_SHIFT: + return INDEX_SHIFT; + case MODIFIER_OS: + return INDEX_OS; + default: + // If two or more modifier keys are pressed, we should use default + // settings. + return INDEX_DEFAULT; + } +} + +void EventStateManager::WheelPrefs::GetBasePrefName( + EventStateManager::WheelPrefs::Index aIndex, nsACString& aBasePrefName) { + aBasePrefName.AssignLiteral("mousewheel."); + switch (aIndex) { + case INDEX_ALT: + aBasePrefName.AppendLiteral("with_alt."); + break; + case INDEX_CONTROL: + aBasePrefName.AppendLiteral("with_control."); + break; + case INDEX_META: + aBasePrefName.AppendLiteral("with_meta."); + break; + case INDEX_SHIFT: + aBasePrefName.AppendLiteral("with_shift."); + break; + case INDEX_OS: + aBasePrefName.AppendLiteral("with_win."); + break; + case INDEX_DEFAULT: + default: + aBasePrefName.AppendLiteral("default."); + break; + } +} + +void EventStateManager::WheelPrefs::Init( + EventStateManager::WheelPrefs::Index aIndex) { + if (mInit[aIndex]) { + return; + } + mInit[aIndex] = true; + + nsAutoCString basePrefName; + GetBasePrefName(aIndex, basePrefName); + + nsAutoCString prefNameX(basePrefName); + prefNameX.AppendLiteral("delta_multiplier_x"); + mMultiplierX[aIndex] = + static_cast<double>(Preferences::GetInt(prefNameX.get(), 100)) / 100; + + nsAutoCString prefNameY(basePrefName); + prefNameY.AppendLiteral("delta_multiplier_y"); + mMultiplierY[aIndex] = + static_cast<double>(Preferences::GetInt(prefNameY.get(), 100)) / 100; + + nsAutoCString prefNameZ(basePrefName); + prefNameZ.AppendLiteral("delta_multiplier_z"); + mMultiplierZ[aIndex] = + static_cast<double>(Preferences::GetInt(prefNameZ.get(), 100)) / 100; + + nsAutoCString prefNameAction(basePrefName); + prefNameAction.AppendLiteral("action"); + int32_t action = Preferences::GetInt(prefNameAction.get(), ACTION_SCROLL); + if (action < int32_t(ACTION_NONE) || action > int32_t(ACTION_LAST)) { + NS_WARNING("Unsupported action pref value, replaced with 'Scroll'."); + action = ACTION_SCROLL; + } + mActions[aIndex] = static_cast<Action>(action); + + // Compute action values overridden by .override_x pref. + // At present, override is possible only for the x-direction + // because this pref is introduced mainly for tilt wheels. + // Note that ACTION_HORIZONTALIZED_SCROLL isn't a valid value for this pref + // because it affects only to deltaY. + prefNameAction.AppendLiteral(".override_x"); + int32_t actionOverrideX = Preferences::GetInt(prefNameAction.get(), -1); + if (actionOverrideX < -1 || actionOverrideX > int32_t(ACTION_LAST) || + actionOverrideX == ACTION_HORIZONTALIZED_SCROLL) { + NS_WARNING("Unsupported action override pref value, didn't override."); + actionOverrideX = -1; + } + mOverriddenActionsX[aIndex] = (actionOverrideX == -1) + ? static_cast<Action>(action) + : static_cast<Action>(actionOverrideX); +} + +void EventStateManager::WheelPrefs::GetMultiplierForDeltaXAndY( + const WidgetWheelEvent* aEvent, Index aIndex, double* aMultiplierForDeltaX, + double* aMultiplierForDeltaY) { + *aMultiplierForDeltaX = mMultiplierX[aIndex]; + *aMultiplierForDeltaY = mMultiplierY[aIndex]; + // If the event has been horizontalized(I.e. treated as a horizontal wheel + // scroll for a vertical wheel scroll), then we should swap mMultiplierX and + // mMultiplierY. By doing this, multipliers will still apply to the delta + // values they origianlly corresponded to. + if (aEvent->mDeltaValuesHorizontalizedForDefaultHandler && + ComputeActionFor(aEvent) == ACTION_HORIZONTALIZED_SCROLL) { + std::swap(*aMultiplierForDeltaX, *aMultiplierForDeltaY); + } +} + +void EventStateManager::WheelPrefs::ApplyUserPrefsToDelta( + WidgetWheelEvent* aEvent) { + if (aEvent->mCustomizedByUserPrefs) { + return; + } + + Index index = GetIndexFor(aEvent); + Init(index); + + double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0; + GetMultiplierForDeltaXAndY(aEvent, index, &multiplierForDeltaX, + &multiplierForDeltaY); + aEvent->mDeltaX *= multiplierForDeltaX; + aEvent->mDeltaY *= multiplierForDeltaY; + aEvent->mDeltaZ *= mMultiplierZ[index]; + + // If the multiplier is 1.0 or -1.0, i.e., it doesn't change the absolute + // value, we should use lineOrPageDelta values which were set by widget. + // Otherwise, we need to compute them from accumulated delta values. + if (!NeedToComputeLineOrPageDelta(aEvent)) { + aEvent->mLineOrPageDeltaX *= static_cast<int32_t>(multiplierForDeltaX); + aEvent->mLineOrPageDeltaY *= static_cast<int32_t>(multiplierForDeltaY); + } else { + aEvent->mLineOrPageDeltaX = 0; + aEvent->mLineOrPageDeltaY = 0; + } + + aEvent->mCustomizedByUserPrefs = + ((mMultiplierX[index] != 1.0) || (mMultiplierY[index] != 1.0) || + (mMultiplierZ[index] != 1.0)); +} + +void EventStateManager::WheelPrefs::CancelApplyingUserPrefsFromOverflowDelta( + WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + + // XXX If the multiplier pref value is negative, the scroll direction was + // changed and caused to scroll different direction. In such case, + // this method reverts the sign of overflowDelta. Does it make widget + // happy? Although, widget can know the pref applied delta values by + // referrencing the deltaX and deltaY of the event. + + double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0; + GetMultiplierForDeltaXAndY(aEvent, index, &multiplierForDeltaX, + &multiplierForDeltaY); + if (multiplierForDeltaX) { + aEvent->mOverflowDeltaX /= multiplierForDeltaX; + } + if (multiplierForDeltaY) { + aEvent->mOverflowDeltaY /= multiplierForDeltaY; + } +} + +EventStateManager::WheelPrefs::Action +EventStateManager::WheelPrefs::ComputeActionFor( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + + bool deltaXPreferred = (Abs(aEvent->mDeltaX) > Abs(aEvent->mDeltaY) && + Abs(aEvent->mDeltaX) > Abs(aEvent->mDeltaZ)); + Action* actions = deltaXPreferred ? mOverriddenActionsX : mActions; + if (actions[index] == ACTION_NONE || actions[index] == ACTION_SCROLL || + actions[index] == ACTION_HORIZONTALIZED_SCROLL) { + return actions[index]; + } + + // Momentum events shouldn't run special actions. + if (aEvent->mIsMomentum) { + // Use the default action. Note that user might kill the wheel scrolling. + Init(INDEX_DEFAULT); + if (actions[INDEX_DEFAULT] == ACTION_SCROLL || + actions[INDEX_DEFAULT] == ACTION_HORIZONTALIZED_SCROLL) { + return actions[INDEX_DEFAULT]; + } + return ACTION_NONE; + } + + return actions[index]; +} + +bool EventStateManager::WheelPrefs::NeedToComputeLineOrPageDelta( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + + return (mMultiplierX[index] != 1.0 && mMultiplierX[index] != -1.0) || + (mMultiplierY[index] != 1.0 && mMultiplierY[index] != -1.0); +} + +void EventStateManager::WheelPrefs::GetUserPrefsForEvent( + const WidgetWheelEvent* aEvent, double* aOutMultiplierX, + double* aOutMultiplierY) { + Index index = GetIndexFor(aEvent); + Init(index); + + double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0; + GetMultiplierForDeltaXAndY(aEvent, index, &multiplierForDeltaX, + &multiplierForDeltaY); + *aOutMultiplierX = multiplierForDeltaX; + *aOutMultiplierY = multiplierForDeltaY; +} + +// static +Maybe<layers::APZWheelAction> EventStateManager::APZWheelActionFor( + const WidgetWheelEvent* aEvent) { + if (aEvent->mMessage != eWheel) { + return Nothing(); + } + WheelPrefs::Action action = + WheelPrefs::GetInstance()->ComputeActionFor(aEvent); + switch (action) { + case WheelPrefs::ACTION_SCROLL: + case WheelPrefs::ACTION_HORIZONTALIZED_SCROLL: + return Some(layers::APZWheelAction::Scroll); + case WheelPrefs::ACTION_PINCH_ZOOM: + return Some(layers::APZWheelAction::PinchZoom); + default: + return Nothing(); + } +} + +// static +WheelDeltaAdjustmentStrategy EventStateManager::GetWheelDeltaAdjustmentStrategy( + const WidgetWheelEvent& aEvent) { + if (aEvent.mMessage != eWheel) { + return WheelDeltaAdjustmentStrategy::eNone; + } + switch (WheelPrefs::GetInstance()->ComputeActionFor(&aEvent)) { + case WheelPrefs::ACTION_SCROLL: + if (StaticPrefs::mousewheel_autodir_enabled() && 0 == aEvent.mDeltaZ) { + if (StaticPrefs::mousewheel_autodir_honourroot()) { + return WheelDeltaAdjustmentStrategy::eAutoDirWithRootHonour; + } + return WheelDeltaAdjustmentStrategy::eAutoDir; + } + return WheelDeltaAdjustmentStrategy::eNone; + case WheelPrefs::ACTION_HORIZONTALIZED_SCROLL: + return WheelDeltaAdjustmentStrategy::eHorizontalize; + default: + break; + } + return WheelDeltaAdjustmentStrategy::eNone; +} + +void EventStateManager::GetUserPrefsForWheelEvent( + const WidgetWheelEvent* aEvent, double* aOutMultiplierX, + double* aOutMultiplierY) { + WheelPrefs::GetInstance()->GetUserPrefsForEvent(aEvent, aOutMultiplierX, + aOutMultiplierY); +} + +bool EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedX( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + return Abs(mMultiplierX[index]) >= + MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL; +} + +bool EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedY( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + return Abs(mMultiplierY[index]) >= + MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL; +} + +} // namespace mozilla |