From 4fa4a8c50df3b9f1699bed57e98cedc1bd196b1e Mon Sep 17 00:00:00 2001 From: Masayuki Nakano Date: Tue, 10 Feb 2009 20:17:37 +0900 Subject: [PATCH] Bug 442774 Wheel/touchpad scrolling gets stuck in frame, stop scrolling the web page as a whole r=Olli.Pettay, sr=roc --- content/events/src/nsEventStateManager.cpp | 135 +- dom/public/idl/base/nsIDOMWindowUtils.idl | 14 +- dom/src/base/nsDOMWindowUtils.cpp | 19 + layout/base/nsIPresShell.h | 15 +- layout/base/nsPresShell.cpp | 17 +- .../mochitest/tests/SimpleTest/EventUtils.js | 17 +- widget/public/nsGUIEvent.h | 5 + widget/tests/Makefile.in | 2 + widget/tests/test_wheeltransaction.xul | 30 + widget/tests/window_wheeltransaction.xul | 1469 +++++++++++++++++ 10 files changed, 1697 insertions(+), 26 deletions(-) create mode 100644 widget/tests/test_wheeltransaction.xul create mode 100644 widget/tests/window_wheeltransaction.xul diff --git a/content/events/src/nsEventStateManager.cpp b/content/events/src/nsEventStateManager.cpp index fa83d6bfe0c..d714defa20e 100644 --- a/content/events/src/nsEventStateManager.cpp +++ b/content/events/src/nsEventStateManager.cpp @@ -375,47 +375,88 @@ class nsMouseWheelTransaction { public: static nsIFrame* GetTargetFrame() { return sTargetFrame; } static void BeginTransaction(nsIFrame* aTargetFrame, - nsGUIEvent* aEvent); - static void UpdateTransaction(); + PRInt32 aNumLines, + PRBool aScrollHorizontal); + static PRBool UpdateTransaction(PRInt32 aNumLines, + PRBool aScrollHorizontal); static void EndTransaction(); static void OnEvent(nsEvent* aEvent); + static void Shutdown(); protected: static nsIntPoint GetScreenPoint(nsGUIEvent* aEvent); + static void OnFailToScrollTarget(); + static void OnTimeout(nsITimer *aTimer, void *aClosure); + static void SetTimeout(); static PRUint32 GetTimeoutTime(); static PRUint32 GetIgnoreMoveDelayTime(); static nsWeakFrame sTargetFrame; static PRUint32 sTime; // in milliseconds static PRUint32 sMouseMoved; // in milliseconds + static nsITimer* sTimer; }; nsWeakFrame nsMouseWheelTransaction::sTargetFrame(nsnull); PRUint32 nsMouseWheelTransaction::sTime = 0; PRUint32 nsMouseWheelTransaction::sMouseMoved = 0; +nsITimer* nsMouseWheelTransaction::sTimer = nsnull; -void -nsMouseWheelTransaction::BeginTransaction(nsIFrame* aTargetFrame, - nsGUIEvent* aEvent) +static PRBool +CanScrollOn(nsIScrollableView* aScrollView, PRInt32 aNumLines, + PRBool aScrollHorizontal) { - NS_ASSERTION(!sTargetFrame, "previous transaction is not finished!"); - sTargetFrame = aTargetFrame; - UpdateTransaction(); + NS_PRECONDITION(aScrollView, "aScrollView is null"); + NS_PRECONDITION(aNumLines, "aNumLines must be non-zero"); + PRBool canScroll; + nsresult rv = + aScrollView->CanScroll(aScrollHorizontal, aNumLines > 0, canScroll); + return NS_SUCCEEDED(rv) && canScroll; } void -nsMouseWheelTransaction::UpdateTransaction() +nsMouseWheelTransaction::BeginTransaction(nsIFrame* aTargetFrame, + PRInt32 aNumLines, + PRBool aScrollHorizontal) { + NS_ASSERTION(!sTargetFrame, "previous transaction is not finished!"); + sTargetFrame = aTargetFrame; + if (!UpdateTransaction(aNumLines, aScrollHorizontal)) { + NS_ERROR("BeginTransaction is called even cannot scroll the frame"); + EndTransaction(); + } +} + +PRBool +nsMouseWheelTransaction::UpdateTransaction(PRInt32 aNumLines, + PRBool aScrollHorizontal) +{ + nsIScrollableViewProvider* svp = do_QueryFrame(GetTargetFrame()); + NS_ENSURE_TRUE(svp, PR_FALSE); + nsIScrollableView *scrollView = svp->GetScrollableView(); + NS_ENSURE_TRUE(scrollView, PR_FALSE); + + if (!CanScrollOn(scrollView, aNumLines, aScrollHorizontal)) { + OnFailToScrollTarget(); + // We should not modify the transaction state when the view will not be + // scrolled actually. + return PR_FALSE; + } + + SetTimeout(); // We should use current time instead of nsEvent.time. // 1. Some events doesn't have the correct creation time. // 2. If the computer runs slowly by other processes eating the CPU resource, // the event creation time doesn't keep real time. sTime = PR_IntervalToMilliseconds(PR_IntervalNow()); sMouseMoved = 0; + return PR_TRUE; } void nsMouseWheelTransaction::EndTransaction() { + if (sTimer) + sTimer->Cancel(); sTargetFrame = nsnull; } @@ -433,8 +474,11 @@ nsMouseWheelTransaction::OnEvent(nsEvent* aEvent) return; if (OutOfTime(sTime, GetTimeoutTime())) { - // Time out the current transaction. - EndTransaction(); + // Even if the scroll event which is handled after timeout, but onTimeout + // was not fired by timer, then the scroll event will scroll old frame, + // therefore, we should call OnTimeout here and ensure to finish the old + // transaction. + OnTimeout(nsnull, nsnull); return; } @@ -483,6 +527,61 @@ nsMouseWheelTransaction::OnEvent(nsEvent* aEvent) } } +void +nsMouseWheelTransaction::Shutdown() +{ + NS_IF_RELEASE(sTimer); +} + +void +nsMouseWheelTransaction::OnFailToScrollTarget() +{ + NS_PRECONDITION(sTargetFrame, "We don't have mouse scrolling transaction"); + // This event is used for automated tests, see bug 442774. + nsContentUtils::DispatchTrustedEvent( + sTargetFrame->GetContent()->GetOwnerDoc(), + sTargetFrame->GetContent(), + NS_LITERAL_STRING("MozMouseScrollFailed"), + PR_TRUE, PR_TRUE); +} + +void +nsMouseWheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure) +{ + if (!sTargetFrame) { + // The transaction target was destroyed already + EndTransaction(); + return; + } + // Store the sTargetFrame, the variable becomes null in EndTransaction. + nsIFrame* frame = sTargetFrame; + // We need to finish current transaction before DOM event firing. Because + // the next DOM event might create strange situation for us. + EndTransaction(); + // This event is used for automated tests, see bug 442774. + nsContentUtils::DispatchTrustedEvent( + frame->GetContent()->GetOwnerDoc(), + frame->GetContent(), + NS_LITERAL_STRING("MozMouseScrollTransactionTimeout"), + PR_TRUE, PR_TRUE); +} + +void +nsMouseWheelTransaction::SetTimeout() +{ + if (!sTimer) { + nsCOMPtr timer = do_CreateInstance(NS_TIMER_CONTRACTID); + if (!timer) + return; + timer.swap(sTimer); + } + sTimer->Cancel(); + nsresult rv = + sTimer->InitWithFuncCallback(OnTimeout, nsnull, GetTimeoutTime(), + nsITimer::TYPE_ONE_SHOT); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "nsITimer::InitWithFuncCallback failed"); +} + nsIntPoint nsMouseWheelTransaction::GetScreenPoint(nsGUIEvent* aEvent) { @@ -621,6 +720,7 @@ nsEventStateManager::~nsEventStateManager() --sESMInstanceCount; if(sESMInstanceCount == 0) { + nsMouseWheelTransaction::Shutdown(); NS_IF_RELEASE(gLastFocusedContent); NS_IF_RELEASE(gLastFocusedDocument); if (gUserInteractionTimerCallback) { @@ -2632,9 +2732,8 @@ nsEventStateManager::DoScrollText(nsPresContext* aPresContext, nsIFrame* lastScrollFrame = nsMouseWheelTransaction::GetTargetFrame(); if (lastScrollFrame) { nsIScrollableViewProvider* svp = do_QueryFrame(lastScrollFrame); - if (svp) { - scrollView = svp->GetScrollableView(); - nsMouseWheelTransaction::UpdateTransaction(); + if (svp && (scrollView = svp->GetScrollableView())) { + nsMouseWheelTransaction::UpdateTransaction(aNumLines, aScrollHorizontal); } else { nsMouseWheelTransaction::EndTransaction(); lastScrollFrame = nsnull; @@ -2666,12 +2765,10 @@ nsEventStateManager::DoScrollText(nsPresContext* aPresContext, scrollView->GetLineHeight(&lineHeight); if (lineHeight != 0) { - PRBool canScroll; - nsresult rv = scrollView->CanScroll(aScrollHorizontal, - (aNumLines > 0), canScroll); - if (NS_SUCCEEDED(rv) && canScroll) { + if (CanScrollOn(scrollView, aNumLines, aScrollHorizontal)) { passToParent = PR_FALSE; - nsMouseWheelTransaction::BeginTransaction(scrollFrame, aEvent); + nsMouseWheelTransaction::BeginTransaction(scrollFrame, + aNumLines, aScrollHorizontal); } // Comboboxes need special care. diff --git a/dom/public/idl/base/nsIDOMWindowUtils.idl b/dom/public/idl/base/nsIDOMWindowUtils.idl index 37503ace395..209bab7ee9e 100644 --- a/dom/public/idl/base/nsIDOMWindowUtils.idl +++ b/dom/public/idl/base/nsIDOMWindowUtils.idl @@ -48,7 +48,7 @@ interface nsIDOMElement; interface nsIDOMHTMLCanvasElement; -[scriptable, uuid(190be8e6-35af-4e3e-9a9f-719f5b1a44a0)] +[scriptable, uuid(8C6263C9-F3EF-419d-80EF-D5D716635FAA)] interface nsIDOMWindowUtils : nsISupports { /** @@ -297,4 +297,16 @@ interface nsIDOMWindowUtils : nsISupports { * fired. */ readonly attribute boolean isMozAfterPaintPending; + + /** + * Disable or enable non synthetic test mouse events on *all* windows. + * + * Cannot be accessed from unprivileged context (not content-accessible). + * Will throw a DOM security error if called without UniversalXPConnect + * privileges. + * + * @param aDisable If true, disable all non synthetic test mouse events + * on all windows. Otherwise, enable them. + */ + void disableNonTestMouseEvents(in boolean aDisable); }; diff --git a/dom/src/base/nsDOMWindowUtils.cpp b/dom/src/base/nsDOMWindowUtils.cpp index 8a7f9aabf69..0c9969cdb99 100644 --- a/dom/src/base/nsDOMWindowUtils.cpp +++ b/dom/src/base/nsDOMWindowUtils.cpp @@ -249,6 +249,7 @@ nsDOMWindowUtils::SendMouseEvent(const nsAString& aType, event.clickCount = aClickCount; event.time = PR_IntervalNow(); + event.flags |= NS_EVENT_FLAG_SYNTETIC_TEST_EVENT; float appPerDev = float(widget->GetDeviceContext()->AppUnitsPerDevPixel()); event.refPoint.x = @@ -681,3 +682,21 @@ nsDOMWindowUtils::GetIsMozAfterPaintPending(PRBool *aResult) *aResult = presContext->IsDOMPaintEventPending(); return NS_OK; } + +NS_IMETHODIMP +nsDOMWindowUtils::DisableNonTestMouseEvents(PRBool aDisable) +{ + PRBool hasCap = PR_FALSE; + if (NS_FAILED(nsContentUtils::GetSecurityManager()-> + IsCapabilityEnabled("UniversalXPConnect", &hasCap)) || + !hasCap) + return NS_ERROR_DOM_SECURITY_ERR; + + NS_ENSURE_TRUE(mWindow, NS_ERROR_FAILURE); + nsIDocShell *docShell = mWindow->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + nsCOMPtr presShell; + docShell->GetPresShell(getter_AddRefs(presShell)); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + return presShell->DisableNonTestMouseEvents(aDisable); +} diff --git a/layout/base/nsIPresShell.h b/layout/base/nsIPresShell.h index e58e0dc2fe5..7009de84846 100644 --- a/layout/base/nsIPresShell.h +++ b/layout/base/nsIPresShell.h @@ -101,10 +101,10 @@ class gfxContext; typedef short SelectionType; typedef PRUint32 nsFrameState; -// b86c23c5-602d-4ca6-a968-379b244fed9e +// 780d34b0-00c3-4bbd-b57d-c600aaf53613 #define NS_IPRESSHELL_IID \ -{ 0xb86c23c5, 0x602d, 0x4ca6, \ - { 0xa9, 0x68, 0x37, 0x9b, 0x24, 0x4f, 0xed, 0x9e } } + { 0x780d34b0, 0xc3, 0x4bbd, \ + { 0xb5, 0x7d, 0xc6, 0x0, 0xaa, 0xf5, 0x36, 0x13 } } // Constants for ScrollContentIntoView() function #define NS_PRESSHELL_SCROLL_TOP 0 @@ -772,6 +772,15 @@ public: nsIFrame* GetDrawEventTargetFrame() { return mDrawEventTargetFrame; } #endif + /** + * Stop or restart non synthetic test mouse event handling on *all* + * presShells. + * + * @param aDisable If true, disable all non synthetic test mouse events on all + * presShells. Otherwise, enable them. + */ + NS_IMETHOD DisableNonTestMouseEvents(PRBool aDisable) = 0; + protected: // IMPORTANT: The ownership implicit in the following member variables // has been explicitly checked. If you add any members to this class, diff --git a/layout/base/nsPresShell.cpp b/layout/base/nsPresShell.cpp index 119f6eedda9..10b819d1115 100644 --- a/layout/base/nsPresShell.cpp +++ b/layout/base/nsPresShell.cpp @@ -1009,6 +1009,8 @@ public: static PRLogModuleInfo* gLog; #endif + NS_IMETHOD DisableNonTestMouseEvents(PRBool aDisable); + protected: virtual ~PresShell(); @@ -1175,6 +1177,8 @@ protected: ReflowCountMgr * mReflowCountMgr; #endif + static PRBool sDisableNonTestMouseEvents; + private: PRBool InZombieDocument(nsIContent *aContent); @@ -1250,6 +1254,8 @@ public: nsRefPtr mPresShell; }; +PRBool PresShell::sDisableNonTestMouseEvents = PR_FALSE; + #ifdef PR_LOGGING PRLogModuleInfo* PresShell::gLog; #endif @@ -5530,6 +5536,13 @@ nsresult PresShell::RetargetEventToParent(nsGUIEvent* aEvent, aEventStatus); } +NS_IMETHODIMP +PresShell::DisableNonTestMouseEvents(PRBool aDisable) +{ + sDisableNonTestMouseEvents = aDisable; + return NS_OK; +} + NS_IMETHODIMP PresShell::HandleEvent(nsIView *aView, nsGUIEvent* aEvent, @@ -5537,7 +5550,9 @@ PresShell::HandleEvent(nsIView *aView, { NS_ASSERTION(aView, "null view"); - if (mIsDestroying || !nsContentUtils::IsSafeToRunScript()) { + if (mIsDestroying || !nsContentUtils::IsSafeToRunScript() || + (sDisableNonTestMouseEvents && NS_IS_MOUSE_EVENT(aEvent) && + !(aEvent->flags & NS_EVENT_FLAG_SYNTETIC_TEST_EVENT))) { return NS_OK; } diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js index deee4cd2989..61c1651e043 100644 --- a/testing/mochitest/tests/SimpleTest/EventUtils.js +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -264,8 +264,10 @@ function synthesizeMouseScroll(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) var button = aEvent.button || 0; var modifiers = _parseModifiers(aEvent); - var left = aTarget.boxObject.x; - var top = aTarget.boxObject.y; + var rect = aTarget.getBoundingClientRect(); + + var left = rect.left; + var top = rect.top; var type = aEvent.type || "DOMMouseScroll"; var axis = aEvent.axis || "vertical"; @@ -516,3 +518,14 @@ function synthesizeDrop(element, dragData, effectAllowed) return dataTransfer.dropEffect; } + +function disableNonTestMouseEvents(aDisable) +{ + netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); + + var utils = + window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + if (utils) + utils.disableNonTestMouseEvents(aDisable); +} diff --git a/widget/public/nsGUIEvent.h b/widget/public/nsGUIEvent.h index 244a19bf63f..8c05963b7f1 100644 --- a/widget/public/nsGUIEvent.h +++ b/widget/public/nsGUIEvent.h @@ -124,6 +124,11 @@ class nsHashKey; // Event has been dispatched at least once #define NS_EVENT_DISPATCHED 0x0400 #define NS_EVENT_FLAG_DISPATCHING 0x0800 +// When an event is synthesized for testing, this flag will be set. +// Note that this is currently used only with mouse events. Because this flag +// is not needed on other events now. Therfore, if you need this flag on other +// events, you can do it. +#define NS_EVENT_FLAG_SYNTETIC_TEST_EVENT 0x1000 #define NS_PRIV_EVENT_UNTRUSTED_PERMITTED 0x8000 diff --git a/widget/tests/Makefile.in b/widget/tests/Makefile.in index e6503525e4d..49b58417bbc 100644 --- a/widget/tests/Makefile.in +++ b/widget/tests/Makefile.in @@ -61,6 +61,8 @@ _TEST_FILES = test_bug343416.xul \ test_bug444800.xul \ test_bug462106.xul \ test_keycodes.xul \ + test_wheeltransaction.xul \ + window_wheeltransaction.xul \ $(NULL) ifeq ($(MOZ_WIDGET_TOOLKIT),cocoa) diff --git a/widget/tests/test_wheeltransaction.xul b/widget/tests/test_wheeltransaction.xul new file mode 100644 index 00000000000..1f8d45dd7a5 --- /dev/null +++ b/widget/tests/test_wheeltransaction.xul @@ -0,0 +1,30 @@ + + + + + + + diff --git a/widget/tests/window_wheeltransaction.xul b/widget/tests/window_wheeltransaction.xul new file mode 100644 index 00000000000..c42310d9a63 --- /dev/null +++ b/widget/tests/window_wheeltransaction.xul @@ -0,0 +1,1469 @@ + + + + + + +