michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 sw=2 et tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "WheelHandlingHelper.h" michael@0: michael@0: #include "mozilla/EventDispatcher.h" michael@0: #include "mozilla/EventStateManager.h" michael@0: #include "mozilla/MouseEvents.h" michael@0: #include "mozilla/Preferences.h" michael@0: #include "nsCOMPtr.h" michael@0: #include "nsContentUtils.h" michael@0: #include "nsIContent.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsIPresShell.h" michael@0: #include "nsIScrollableFrame.h" michael@0: #include "nsITimer.h" michael@0: #include "nsPresContext.h" michael@0: #include "prtime.h" michael@0: #include "Units.h" michael@0: michael@0: namespace mozilla { michael@0: michael@0: /******************************************************************/ michael@0: /* mozilla::DeltaValues */ michael@0: /******************************************************************/ michael@0: michael@0: DeltaValues::DeltaValues(WidgetWheelEvent* aEvent) michael@0: : deltaX(aEvent->deltaX) michael@0: , deltaY(aEvent->deltaY) michael@0: { michael@0: } michael@0: michael@0: /******************************************************************/ michael@0: /* mozilla::WheelHandlingUtils */ michael@0: /******************************************************************/ michael@0: michael@0: /* static */ bool michael@0: WheelHandlingUtils::CanScrollInRange(nscoord aMin, nscoord aValue, nscoord aMax, michael@0: double aDirection) michael@0: { michael@0: return aDirection > 0.0 ? aValue < static_cast(aMax) : michael@0: static_cast(aMin) < aValue; michael@0: } michael@0: michael@0: /* static */ bool michael@0: WheelHandlingUtils::CanScrollOn(nsIScrollableFrame* aScrollFrame, michael@0: double aDirectionX, double aDirectionY) michael@0: { michael@0: MOZ_ASSERT(aScrollFrame); michael@0: NS_ASSERTION(aDirectionX || aDirectionY, michael@0: "One of the delta values must be non-zero at least"); michael@0: michael@0: nsPoint scrollPt = aScrollFrame->GetScrollPosition(); michael@0: nsRect scrollRange = aScrollFrame->GetScrollRange(); michael@0: uint32_t directions = aScrollFrame->GetPerceivedScrollingDirections(); michael@0: michael@0: return (aDirectionX && (directions & nsIScrollableFrame::HORIZONTAL) && michael@0: CanScrollInRange(scrollRange.x, scrollPt.x, michael@0: scrollRange.XMost(), aDirectionX)) || michael@0: (aDirectionY && (directions & nsIScrollableFrame::VERTICAL) && michael@0: CanScrollInRange(scrollRange.y, scrollPt.y, michael@0: scrollRange.YMost(), aDirectionY)); michael@0: } michael@0: michael@0: /******************************************************************/ michael@0: /* mozilla::WheelTransaction */ michael@0: /******************************************************************/ michael@0: michael@0: nsWeakFrame WheelTransaction::sTargetFrame(nullptr); michael@0: uint32_t WheelTransaction::sTime = 0; michael@0: uint32_t WheelTransaction::sMouseMoved = 0; michael@0: nsITimer* WheelTransaction::sTimer = nullptr; michael@0: int32_t WheelTransaction::sScrollSeriesCounter = 0; michael@0: bool WheelTransaction::sOwnScrollbars = false; michael@0: michael@0: /* static */ bool michael@0: WheelTransaction::OutOfTime(uint32_t aBaseTime, uint32_t aThreshold) michael@0: { michael@0: uint32_t now = PR_IntervalToMilliseconds(PR_IntervalNow()); michael@0: return (now - aBaseTime > aThreshold); michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::OwnScrollbars(bool aOwn) michael@0: { michael@0: sOwnScrollbars = aOwn; michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::BeginTransaction(nsIFrame* aTargetFrame, michael@0: WidgetWheelEvent* aEvent) michael@0: { michael@0: NS_ASSERTION(!sTargetFrame, "previous transaction is not finished!"); michael@0: MOZ_ASSERT(aEvent->message == NS_WHEEL_WHEEL, michael@0: "Transaction must be started with a wheel event"); michael@0: ScrollbarsForWheel::OwnWheelTransaction(false); michael@0: sTargetFrame = aTargetFrame; michael@0: sScrollSeriesCounter = 0; michael@0: if (!UpdateTransaction(aEvent)) { michael@0: NS_ERROR("BeginTransaction is called even cannot scroll the frame"); michael@0: EndTransaction(); michael@0: } michael@0: } michael@0: michael@0: /* static */ bool michael@0: WheelTransaction::UpdateTransaction(WidgetWheelEvent* aEvent) michael@0: { michael@0: nsIScrollableFrame* sf = GetTargetFrame()->GetScrollTargetFrame(); michael@0: NS_ENSURE_TRUE(sf, false); michael@0: michael@0: if (!WheelHandlingUtils::CanScrollOn(sf, aEvent->deltaX, aEvent->deltaY)) { michael@0: OnFailToScrollTarget(); michael@0: // We should not modify the transaction state when the view will not be michael@0: // scrolled actually. michael@0: return false; michael@0: } michael@0: michael@0: SetTimeout(); michael@0: michael@0: if (sScrollSeriesCounter != 0 && OutOfTime(sTime, kScrollSeriesTimeout)) { michael@0: sScrollSeriesCounter = 0; michael@0: } michael@0: sScrollSeriesCounter++; michael@0: michael@0: // We should use current time instead of WidgetEvent.time. michael@0: // 1. Some events doesn't have the correct creation time. michael@0: // 2. If the computer runs slowly by other processes eating the CPU resource, michael@0: // the event creation time doesn't keep real time. michael@0: sTime = PR_IntervalToMilliseconds(PR_IntervalNow()); michael@0: sMouseMoved = 0; michael@0: return true; michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::MayEndTransaction() michael@0: { michael@0: if (!sOwnScrollbars && ScrollbarsForWheel::IsActive()) { michael@0: ScrollbarsForWheel::OwnWheelTransaction(true); michael@0: } else { michael@0: EndTransaction(); michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::EndTransaction() michael@0: { michael@0: if (sTimer) { michael@0: sTimer->Cancel(); michael@0: } michael@0: sTargetFrame = nullptr; michael@0: sScrollSeriesCounter = 0; michael@0: if (sOwnScrollbars) { michael@0: sOwnScrollbars = false; michael@0: ScrollbarsForWheel::OwnWheelTransaction(false); michael@0: ScrollbarsForWheel::Inactivate(); michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::OnEvent(WidgetEvent* aEvent) michael@0: { michael@0: if (!sTargetFrame) { michael@0: return; michael@0: } michael@0: michael@0: if (OutOfTime(sTime, GetTimeoutTime())) { michael@0: // Even if the scroll event which is handled after timeout, but onTimeout michael@0: // was not fired by timer, then the scroll event will scroll old frame, michael@0: // therefore, we should call OnTimeout here and ensure to finish the old michael@0: // transaction. michael@0: OnTimeout(nullptr, nullptr); michael@0: return; michael@0: } michael@0: michael@0: switch (aEvent->message) { michael@0: case NS_WHEEL_WHEEL: michael@0: if (sMouseMoved != 0 && michael@0: OutOfTime(sMouseMoved, GetIgnoreMoveDelayTime())) { michael@0: // Terminate the current mousewheel transaction if the mouse moved more michael@0: // than ignoremovedelay milliseconds ago michael@0: EndTransaction(); michael@0: } michael@0: return; michael@0: case NS_MOUSE_MOVE: michael@0: case NS_DRAGDROP_OVER: { michael@0: WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); michael@0: if (mouseEvent->IsReal()) { michael@0: // If the cursor is moving to be outside the frame, michael@0: // terminate the scrollwheel transaction. michael@0: nsIntPoint pt = GetScreenPoint(mouseEvent); michael@0: nsIntRect r = sTargetFrame->GetScreenRectExternal(); michael@0: if (!r.Contains(pt)) { michael@0: EndTransaction(); michael@0: return; michael@0: } michael@0: michael@0: // If the cursor is moving inside the frame, and it is less than michael@0: // ignoremovedelay milliseconds since the last scroll operation, ignore michael@0: // the mouse move; otherwise, record the current mouse move time to be michael@0: // checked later michael@0: if (!sMouseMoved && OutOfTime(sTime, GetIgnoreMoveDelayTime())) { michael@0: sMouseMoved = PR_IntervalToMilliseconds(PR_IntervalNow()); michael@0: } michael@0: } michael@0: return; michael@0: } michael@0: case NS_KEY_PRESS: michael@0: case NS_KEY_UP: michael@0: case NS_KEY_DOWN: michael@0: case NS_MOUSE_BUTTON_UP: michael@0: case NS_MOUSE_BUTTON_DOWN: michael@0: case NS_MOUSE_DOUBLECLICK: michael@0: case NS_MOUSE_CLICK: michael@0: case NS_CONTEXTMENU: michael@0: case NS_DRAGDROP_DROP: michael@0: EndTransaction(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::Shutdown() michael@0: { michael@0: NS_IF_RELEASE(sTimer); michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::OnFailToScrollTarget() michael@0: { michael@0: NS_PRECONDITION(sTargetFrame, "We don't have mouse scrolling transaction"); michael@0: michael@0: if (Preferences::GetBool("test.mousescroll", false)) { michael@0: // This event is used for automated tests, see bug 442774. michael@0: nsContentUtils::DispatchTrustedEvent( michael@0: sTargetFrame->GetContent()->OwnerDoc(), michael@0: sTargetFrame->GetContent(), michael@0: NS_LITERAL_STRING("MozMouseScrollFailed"), michael@0: true, true); michael@0: } michael@0: // The target frame might be destroyed in the event handler, at that time, michael@0: // we need to finish the current transaction michael@0: if (!sTargetFrame) { michael@0: EndTransaction(); michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure) michael@0: { michael@0: if (!sTargetFrame) { michael@0: // The transaction target was destroyed already michael@0: EndTransaction(); michael@0: return; michael@0: } michael@0: // Store the sTargetFrame, the variable becomes null in EndTransaction. michael@0: nsIFrame* frame = sTargetFrame; michael@0: // We need to finish current transaction before DOM event firing. Because michael@0: // the next DOM event might create strange situation for us. michael@0: MayEndTransaction(); michael@0: michael@0: if (Preferences::GetBool("test.mousescroll", false)) { michael@0: // This event is used for automated tests, see bug 442774. michael@0: nsContentUtils::DispatchTrustedEvent( michael@0: frame->GetContent()->OwnerDoc(), michael@0: frame->GetContent(), michael@0: NS_LITERAL_STRING("MozMouseScrollTransactionTimeout"), michael@0: true, true); michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: WheelTransaction::SetTimeout() michael@0: { michael@0: if (!sTimer) { michael@0: nsCOMPtr timer = do_CreateInstance(NS_TIMER_CONTRACTID); michael@0: if (!timer) { michael@0: return; michael@0: } michael@0: timer.swap(sTimer); michael@0: } michael@0: sTimer->Cancel(); michael@0: DebugOnly rv = michael@0: sTimer->InitWithFuncCallback(OnTimeout, nullptr, GetTimeoutTime(), michael@0: nsITimer::TYPE_ONE_SHOT); michael@0: NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "nsITimer::InitWithFuncCallback failed"); michael@0: } michael@0: michael@0: /* static */ nsIntPoint michael@0: WheelTransaction::GetScreenPoint(WidgetGUIEvent* aEvent) michael@0: { michael@0: NS_ASSERTION(aEvent, "aEvent is null"); michael@0: NS_ASSERTION(aEvent->widget, "aEvent-widget is null"); michael@0: return LayoutDeviceIntPoint::ToUntyped(aEvent->refPoint) + michael@0: aEvent->widget->WidgetToScreenOffset(); michael@0: } michael@0: michael@0: /* static */ uint32_t michael@0: WheelTransaction::GetTimeoutTime() michael@0: { michael@0: return Preferences::GetUint("mousewheel.transaction.timeout", 1500); michael@0: } michael@0: michael@0: /* static */ uint32_t michael@0: WheelTransaction::GetIgnoreMoveDelayTime() michael@0: { michael@0: return Preferences::GetUint("mousewheel.transaction.ignoremovedelay", 100); michael@0: } michael@0: michael@0: /* static */ DeltaValues michael@0: WheelTransaction::AccelerateWheelDelta(WidgetWheelEvent* aEvent, michael@0: bool aAllowScrollSpeedOverride) michael@0: { michael@0: DeltaValues result(aEvent); michael@0: michael@0: // Don't accelerate the delta values if the event isn't line scrolling. michael@0: if (aEvent->deltaMode != nsIDOMWheelEvent::DOM_DELTA_LINE) { michael@0: return result; michael@0: } michael@0: michael@0: if (aAllowScrollSpeedOverride) { michael@0: result = OverrideSystemScrollSpeed(aEvent); michael@0: } michael@0: michael@0: // Accelerate by the sScrollSeriesCounter michael@0: int32_t start = GetAccelerationStart(); michael@0: if (start >= 0 && sScrollSeriesCounter >= start) { michael@0: int32_t factor = GetAccelerationFactor(); michael@0: if (factor > 0) { michael@0: result.deltaX = ComputeAcceleratedWheelDelta(result.deltaX, factor); michael@0: result.deltaY = ComputeAcceleratedWheelDelta(result.deltaY, factor); michael@0: } michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: /* static */ double michael@0: WheelTransaction::ComputeAcceleratedWheelDelta(double aDelta, michael@0: int32_t aFactor) michael@0: { michael@0: if (aDelta == 0.0) { michael@0: return 0; michael@0: } michael@0: michael@0: return (aDelta * sScrollSeriesCounter * (double)aFactor / 10); michael@0: } michael@0: michael@0: /* static */ int32_t michael@0: WheelTransaction::GetAccelerationStart() michael@0: { michael@0: return Preferences::GetInt("mousewheel.acceleration.start", -1); michael@0: } michael@0: michael@0: /* static */ int32_t michael@0: WheelTransaction::GetAccelerationFactor() michael@0: { michael@0: return Preferences::GetInt("mousewheel.acceleration.factor", -1); michael@0: } michael@0: michael@0: /* static */ DeltaValues michael@0: WheelTransaction::OverrideSystemScrollSpeed(WidgetWheelEvent* aEvent) michael@0: { michael@0: MOZ_ASSERT(sTargetFrame, "We don't have mouse scrolling transaction"); michael@0: MOZ_ASSERT(aEvent->deltaMode == nsIDOMWheelEvent::DOM_DELTA_LINE); michael@0: michael@0: // If the event doesn't scroll to both X and Y, we don't need to do anything michael@0: // here. michael@0: if (!aEvent->deltaX && !aEvent->deltaY) { michael@0: return DeltaValues(aEvent); michael@0: } michael@0: michael@0: // We shouldn't override the scrolling speed on non root scroll frame. michael@0: if (sTargetFrame != michael@0: sTargetFrame->PresContext()->PresShell()->GetRootScrollFrame()) { michael@0: return DeltaValues(aEvent); michael@0: } michael@0: michael@0: // Compute the overridden speed to nsIWidget. The widget can check the michael@0: // conditions (e.g., checking the prefs, and also whether the user customized michael@0: // the system settings of the mouse wheel scrolling or not), and can limit michael@0: // the speed for preventing the unexpected high speed scrolling. michael@0: nsCOMPtr widget(sTargetFrame->GetNearestWidget()); michael@0: NS_ENSURE_TRUE(widget, DeltaValues(aEvent)); michael@0: DeltaValues overriddenDeltaValues(0.0, 0.0); michael@0: nsresult rv = michael@0: widget->OverrideSystemMouseScrollSpeed(aEvent->deltaX, aEvent->deltaY, michael@0: overriddenDeltaValues.deltaX, michael@0: overriddenDeltaValues.deltaY); michael@0: return NS_FAILED(rv) ? DeltaValues(aEvent) : overriddenDeltaValues; michael@0: } michael@0: michael@0: /******************************************************************/ michael@0: /* mozilla::ScrollbarsForWheel */ michael@0: /******************************************************************/ michael@0: michael@0: const DeltaValues ScrollbarsForWheel::directions[kNumberOfTargets] = { michael@0: DeltaValues(-1, 0), DeltaValues(+1, 0), DeltaValues(0, -1), DeltaValues(0, +1) michael@0: }; michael@0: michael@0: nsWeakFrame ScrollbarsForWheel::sActiveOwner = nullptr; michael@0: nsWeakFrame ScrollbarsForWheel::sActivatedScrollTargets[kNumberOfTargets] = { michael@0: nullptr, nullptr, nullptr, nullptr michael@0: }; michael@0: michael@0: bool ScrollbarsForWheel::sHadWheelStart = false; michael@0: bool ScrollbarsForWheel::sOwnWheelTransaction = false; michael@0: michael@0: /* static */ void michael@0: ScrollbarsForWheel::PrepareToScrollText(EventStateManager* aESM, michael@0: nsIFrame* aTargetFrame, michael@0: WidgetWheelEvent* aEvent) michael@0: { michael@0: if (aEvent->message == NS_WHEEL_START) { michael@0: WheelTransaction::OwnScrollbars(false); michael@0: if (!IsActive()) { michael@0: TemporarilyActivateAllPossibleScrollTargets(aESM, aTargetFrame, aEvent); michael@0: sHadWheelStart = true; michael@0: } michael@0: } else { michael@0: DeactivateAllTemporarilyActivatedScrollTargets(); michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: ScrollbarsForWheel::SetActiveScrollTarget(nsIScrollableFrame* aScrollTarget) michael@0: { michael@0: if (!sHadWheelStart) { michael@0: return; michael@0: } michael@0: nsIScrollbarOwner* scrollbarOwner = do_QueryFrame(aScrollTarget); michael@0: if (!scrollbarOwner) { michael@0: return; michael@0: } michael@0: sHadWheelStart = false; michael@0: sActiveOwner = do_QueryFrame(aScrollTarget); michael@0: scrollbarOwner->ScrollbarActivityStarted(); michael@0: } michael@0: michael@0: /* static */ void michael@0: ScrollbarsForWheel::MayInactivate() michael@0: { michael@0: if (!sOwnWheelTransaction && WheelTransaction::GetTargetFrame()) { michael@0: WheelTransaction::OwnScrollbars(true); michael@0: } else { michael@0: Inactivate(); michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: ScrollbarsForWheel::Inactivate() michael@0: { michael@0: nsIScrollbarOwner* scrollbarOwner = do_QueryFrame(sActiveOwner); michael@0: if (scrollbarOwner) { michael@0: scrollbarOwner->ScrollbarActivityStopped(); michael@0: } michael@0: sActiveOwner = nullptr; michael@0: DeactivateAllTemporarilyActivatedScrollTargets(); michael@0: if (sOwnWheelTransaction) { michael@0: sOwnWheelTransaction = false; michael@0: WheelTransaction::OwnScrollbars(false); michael@0: WheelTransaction::EndTransaction(); michael@0: } michael@0: } michael@0: michael@0: /* static */ bool michael@0: ScrollbarsForWheel::IsActive() michael@0: { michael@0: if (sActiveOwner) { michael@0: return true; michael@0: } michael@0: for (size_t i = 0; i < kNumberOfTargets; ++i) { michael@0: if (sActivatedScrollTargets[i]) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: /* static */ void michael@0: ScrollbarsForWheel::OwnWheelTransaction(bool aOwn) michael@0: { michael@0: sOwnWheelTransaction = aOwn; michael@0: } michael@0: michael@0: /* static */ void michael@0: ScrollbarsForWheel::TemporarilyActivateAllPossibleScrollTargets( michael@0: EventStateManager* aESM, michael@0: nsIFrame* aTargetFrame, michael@0: WidgetWheelEvent* aEvent) michael@0: { michael@0: for (size_t i = 0; i < kNumberOfTargets; i++) { michael@0: const DeltaValues *dir = &directions[i]; michael@0: nsWeakFrame* scrollTarget = &sActivatedScrollTargets[i]; michael@0: MOZ_ASSERT(!*scrollTarget, "scroll target still temporarily activated!"); michael@0: nsIScrollableFrame* target = michael@0: aESM->ComputeScrollTarget(aTargetFrame, dir->deltaX, dir->deltaY, aEvent, michael@0: EventStateManager::COMPUTE_DEFAULT_ACTION_TARGET); michael@0: nsIScrollbarOwner* scrollbarOwner = do_QueryFrame(target); michael@0: if (scrollbarOwner) { michael@0: nsIFrame* targetFrame = do_QueryFrame(target); michael@0: *scrollTarget = targetFrame; michael@0: scrollbarOwner->ScrollbarActivityStarted(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /* static */ void michael@0: ScrollbarsForWheel::DeactivateAllTemporarilyActivatedScrollTargets() michael@0: { michael@0: for (size_t i = 0; i < kNumberOfTargets; i++) { michael@0: nsWeakFrame* scrollTarget = &sActivatedScrollTargets[i]; michael@0: if (*scrollTarget) { michael@0: nsIScrollbarOwner* scrollbarOwner = do_QueryFrame(*scrollTarget); michael@0: if (scrollbarOwner) { michael@0: scrollbarOwner->ScrollbarActivityStopped(); michael@0: } michael@0: *scrollTarget = nullptr; michael@0: } michael@0: } michael@0: } michael@0: michael@0: } // namespace mozilla