From 51b0e867e66e5e12266ff7847e6602017ec3e806 Mon Sep 17 00:00:00 2001 From: Neil Deakin Date: Sun, 13 Sep 2009 09:13:16 -0400 Subject: [PATCH] Bug 503943, add mouse capturing api to elements, remove capturing from views, r=roc,sr=smaug --- content/base/src/nsDocument.cpp | 12 ++ content/base/src/nsGenericElement.cpp | 23 +++ content/events/src/nsEventStateManager.cpp | 26 +-- content/events/src/nsEventStateManager.h | 17 +- .../html/content/src/nsHTMLFormElement.cpp | 2 +- dom/interfaces/core/nsIDOMNSDocument.idl | 8 +- dom/interfaces/core/nsIDOMNSElement.idl | 18 +- layout/base/nsIPresShell.h | 53 +++++ layout/base/nsPresShell.cpp | 181 +++++++++++++---- layout/forms/nsListControlFrame.cpp | 45 ++--- layout/generic/nsFrame.cpp | 143 +++++-------- layout/generic/nsFrame.h | 12 -- layout/generic/nsFrameSetFrame.cpp | 79 ++------ layout/generic/nsGfxScrollFrame.h | 8 - layout/generic/nsIFrame.h | 21 +- layout/generic/nsSelection.cpp | 75 +------ layout/generic/test/test_bug470212.html | 1 + layout/xul/base/src/nsResizerFrame.cpp | 6 +- layout/xul/base/src/nsSliderFrame.cpp | 36 +--- layout/xul/base/src/nsSliderFrame.h | 2 - layout/xul/base/src/nsSplitterFrame.cpp | 82 +------- layout/xul/base/src/nsSplitterFrame.h | 2 - layout/xul/base/src/nsTitleBarFrame.cpp | 48 +---- layout/xul/base/src/nsTitleBarFrame.h | 8 - layout/xul/base/src/nsXULPopupManager.cpp | 2 +- toolkit/content/tests/widgets/Makefile.in | 1 + .../tests/widgets/test_mousecapture.xul | 191 ++++++++++++++++++ toolkit/content/tests/widgets/test_scale.xul | 24 +++ view/public/nsIViewManager.h | 19 +- view/public/nsIViewObserver.h | 13 +- view/src/nsView.cpp | 21 +- view/src/nsViewManager.cpp | 58 +----- view/src/nsViewManager.h | 12 +- widget/src/xpwidgets/nsBaseDragService.cpp | 20 +- 34 files changed, 623 insertions(+), 646 deletions(-) create mode 100644 toolkit/content/tests/widgets/test_mousecapture.xul diff --git a/content/base/src/nsDocument.cpp b/content/base/src/nsDocument.cpp index a78d6d1185f9..8c1761839868 100644 --- a/content/base/src/nsDocument.cpp +++ b/content/base/src/nsDocument.cpp @@ -2773,6 +2773,18 @@ nsDocument::DestroyClassNameArray(void* aData) delete info; } +NS_IMETHODIMP +nsDocument::ReleaseCapture() +{ + // only release the capture if the caller can access it. This prevents a + // page from stopping a scrollbar grab for example. + nsCOMPtr node = do_QueryInterface(nsIPresShell::GetCapturingContent()); + if (node && nsContentUtils::CanCallerAccess(node)) { + nsIPresShell::SetCapturingContent(nsnull, 0); + } + return NS_OK; +} + nsresult nsDocument::SetBaseURI(nsIURI* aURI) { diff --git a/content/base/src/nsGenericElement.cpp b/content/base/src/nsGenericElement.cpp index 44d5faeb260f..59a1b03ab15f 100644 --- a/content/base/src/nsGenericElement.cpp +++ b/content/base/src/nsGenericElement.cpp @@ -1084,6 +1084,29 @@ nsNSElementTearoff::GetClassList(nsIDOMDOMTokenList** aResult) return NS_OK; } +NS_IMETHODIMP +nsNSElementTearoff::SetCapture(PRBool aRetargetToElement) +{ + // If there is already an active capture, ignore this request. This would + // occur if a splitter, frame resizer, etc had already captured and we don't + // want to override those. + nsCOMPtr node = do_QueryInterface(nsIPresShell::GetCapturingContent()); + if (node) + return NS_OK; + + nsIPresShell::SetCapturingContent(mContent, aRetargetToElement ? CAPTURE_RETARGETTOELEMENT : 0); + return NS_OK; +} + +NS_IMETHODIMP +nsNSElementTearoff::ReleaseCapture() +{ + if (nsIPresShell::GetCapturingContent() == mContent) { + nsIPresShell::SetCapturingContent(nsnull, 0); + } + return NS_OK; +} + //---------------------------------------------------------------------- diff --git a/content/events/src/nsEventStateManager.cpp b/content/events/src/nsEventStateManager.cpp index 12007cfcd4e5..9d1421b0c535 100644 --- a/content/events/src/nsEventStateManager.cpp +++ b/content/events/src/nsEventStateManager.cpp @@ -2741,19 +2741,9 @@ nsEventStateManager::PostHandleEvent(nsPresContext* aPresContext, { if (static_cast(aEvent)->button == nsMouseEvent::eLeftButton && !mNormalLMouseEventInProcess) { - //Our state is out of whack. We got a mouseup while still processing - //the mousedown. Kill View-level mouse capture or it'll stay stuck - if (aView) { - nsIViewManager* viewMan = aView->GetViewManager(); - if (viewMan) { - nsIView* grabbingView; - viewMan->GetMouseEventGrabber(grabbingView); - if (grabbingView == aView) { - PRBool result; - viewMan->GrabMouseEvents(nsnull, result); - } - } - } + // We got a mouseup event while a mousedown event was being processed. + // Make sure that the capturing content is cleared. + nsIPresShell::SetCapturingContent(nsnull, 0); break; } @@ -2858,17 +2848,9 @@ nsEventStateManager::PostHandleEvent(nsPresContext* aPresContext, ret = CheckForAndDispatchClick(presContext, (nsMouseEvent*)aEvent, aStatus); } + nsIPresShell *shell = presContext->GetPresShell(); if (shell) { - nsIViewManager* viewMan = shell->GetViewManager(); - if (viewMan) { - nsIView* grabbingView = nsnull; - viewMan->GetMouseEventGrabber(grabbingView); - if (grabbingView == aView) { - PRBool result; - viewMan->GrabMouseEvents(nsnull, result); - } - } nsCOMPtr frameSelection = shell->FrameSelection(); frameSelection->SetMouseDownState(PR_FALSE); } diff --git a/content/events/src/nsEventStateManager.h b/content/events/src/nsEventStateManager.h index c3f14b5d9a5c..e7853560bd3e 100644 --- a/content/events/src/nsEventStateManager.h +++ b/content/events/src/nsEventStateManager.h @@ -411,15 +411,22 @@ protected: static PRInt32 sUserInputEventDepth; }; - +/** + * This class is used while processing real user input. During this time, popups + * are allowed. For mousedown events, mouse capturing is also permitted. + */ class nsAutoHandlingUserInputStatePusher { public: - nsAutoHandlingUserInputStatePusher(PRBool aIsHandlingUserInput) - : mIsHandlingUserInput(aIsHandlingUserInput) + nsAutoHandlingUserInputStatePusher(PRBool aIsHandlingUserInput, PRBool aIsMouseDown) + : mIsHandlingUserInput(aIsHandlingUserInput), mIsMouseDown(aIsMouseDown) { if (aIsHandlingUserInput) { nsEventStateManager::StartHandlingUserInput(); + if (aIsMouseDown) { + nsIPresShell::SetCapturingContent(nsnull, 0); + nsIPresShell::AllowMouseCapture(PR_TRUE); + } } } @@ -427,11 +434,15 @@ public: { if (mIsHandlingUserInput) { nsEventStateManager::StopHandlingUserInput(); + if (mIsMouseDown) { + nsIPresShell::AllowMouseCapture(PR_FALSE); + } } } protected: PRBool mIsHandlingUserInput; + PRBool mIsMouseDown; private: // Hide so that this class can only be stack-allocated diff --git a/content/html/content/src/nsHTMLFormElement.cpp b/content/html/content/src/nsHTMLFormElement.cpp index c1157a1418a1..3ac7de643ca1 100644 --- a/content/html/content/src/nsHTMLFormElement.cpp +++ b/content/html/content/src/nsHTMLFormElement.cpp @@ -1139,7 +1139,7 @@ nsHTMLFormElement::SubmitSubmission(nsIFormSubmission* aFormSubmission) { nsAutoPopupStatePusher popupStatePusher(mSubmitPopupState); - nsAutoHandlingUserInputStatePusher userInpStatePusher(mSubmitInitiatedFromUserInput); + nsAutoHandlingUserInputStatePusher userInpStatePusher(mSubmitInitiatedFromUserInput, PR_FALSE); rv = aFormSubmission->SubmitTo(actionURI, target, this, linkHandler, getter_AddRefs(docShell), diff --git a/dom/interfaces/core/nsIDOMNSDocument.idl b/dom/interfaces/core/nsIDOMNSDocument.idl index 53af4e94ce04..e8631cf03ac0 100644 --- a/dom/interfaces/core/nsIDOMNSDocument.idl +++ b/dom/interfaces/core/nsIDOMNSDocument.idl @@ -43,7 +43,7 @@ interface nsIBoxObject; interface nsIDOMLocation; -[scriptable, uuid(09a439ad-4079-46d5-a050-4d7015d1a108)] +[scriptable, uuid(B7E9211B-F29F-4E2D-9762-6BCBC64E5C05)] interface nsIDOMNSDocument : nsISupports { readonly attribute DOMString characterSet; @@ -91,4 +91,10 @@ interface nsIDOMNSDocument : nsISupports */ nsIDOMElement elementFromPoint(in long x, in long y); + /** + * Release the current mouse capture if it is on an element within this + * document. + */ + void releaseCapture(); + }; diff --git a/dom/interfaces/core/nsIDOMNSElement.idl b/dom/interfaces/core/nsIDOMNSElement.idl index adb862854ffc..47688e1f871e 100644 --- a/dom/interfaces/core/nsIDOMNSElement.idl +++ b/dom/interfaces/core/nsIDOMNSElement.idl @@ -39,7 +39,7 @@ #include "domstubs.idl" -[scriptable, uuid(df86b1a8-02c3-47be-a76b-856620f925df)] +[scriptable, uuid(FA8D7AF8-C208-4564-A0CD-346C345711F0)] interface nsIDOMNSElement : nsISupports { /* @@ -152,4 +152,20 @@ interface nsIDOMNSElement : nsISupports * Returns a DOMTokenList object reflecting the class attribute. */ readonly attribute nsIDOMDOMTokenList classList; + + /** + * Set this during a mousedown event to grab and retarget all mouse events + * to this element until the mouse button is released or releaseCapture is + * called. If retargetToElement is true, then all events are targetted at + * this element. If false, events can also fire at descendants of this + * element. + * + */ + void setCapture([optional] in boolean retargetToElement); + + /** + * If this element has captured the mouse, release the capture. If another + * element has captured the mouse, this method has no effect. + */ + void releaseCapture(); }; diff --git a/layout/base/nsIPresShell.h b/layout/base/nsIPresShell.h index 7999482c72c5..d741c8768110 100644 --- a/layout/base/nsIPresShell.h +++ b/layout/base/nsIPresShell.h @@ -104,6 +104,23 @@ class nsDisplayListBuilder; typedef short SelectionType; typedef PRUint32 nsFrameState; +// Flags to pass to SetCapturingContent +// +// when assigning capture, ignore whether capture is allowed or not +#define CAPTURE_IGNOREALLOWED 1 +// true if events should be targeted at the capturing content or its children +#define CAPTURE_RETARGETTOELEMENT 2 + +typedef struct CapturingContentInfo { + // capture should only be allowed during a mousedown event + PRPackedBool mAllowed; + PRPackedBool mRetargetToElement; + nsIContent* mContent; + + CapturingContentInfo() : + mAllowed(PR_FALSE), mRetargetToElement(PR_FALSE), mContent(nsnull) { } +} CapturingContentInfo; + // eba51d41-68db-4dab-a57b-dc1a2704de87 #define NS_IPRESSHELL_IID \ { 0xeba51d41, 0x68db, 0x4dab, \ @@ -857,6 +874,42 @@ public: return mObservesMutationsForPrint; } + // mouse capturing + + static CapturingContentInfo gCaptureInfo; + + /** + * When capturing content is set, it traps all mouse events and retargets + * them at this content node. If capturing is not allowed + * (gCaptureInfo.mAllowed is false), then capturing is not set. However, if + * the CAPTURE_IGNOREALLOWED flag is set, the allowed state is ignored and + * capturing is set regardless. To disable capture, pass null for the value + * of aContent. + * + * If CAPTURE_RETARGETTOELEMENT is set, all mouse events are targeted at + * aContent only. Otherwise, mouse events are targeted at aContent or its + * descendants. That is, descendants of aContent receive mouse events as + * they normally would, but mouse events outside of aContent are retargeted + * to aContent. + */ + static void SetCapturingContent(nsIContent* aContent, PRUint8 aFlags); + + /** + * Return the active content currently capturing the mouse if any. + */ + static nsIContent* GetCapturingContent() + { + return gCaptureInfo.mContent; + } + + /** + * Allow or disallow mouse capturing. + */ + static void AllowMouseCapture(PRBool aAllowed) + { + gCaptureInfo.mAllowed = aAllowed; + } + 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 88255c7cb01b..ea83d3cbe1d9 100644 --- a/layout/base/nsPresShell.cpp +++ b/layout/base/nsPresShell.cpp @@ -208,6 +208,7 @@ static NS_DEFINE_CID(kCSSStyleSheetCID, NS_CSS_STYLESHEET_CID); static NS_DEFINE_IID(kRangeCID, NS_RANGE_CID); PRBool nsIPresShell::gIsAccessibilityActive = PR_FALSE; +CapturingContentInfo nsIPresShell::gCaptureInfo; // convert a color value to a string, in the CSS format #RRGGBB // * - initially created for bugs 31816, 20760, 22963 @@ -797,6 +798,7 @@ public: NS_IMETHOD_(void) InvalidateFrameForView(nsIView *view); NS_IMETHOD_(void) DispatchSynthMouseMove(nsGUIEvent *aEvent, PRBool aFlushOnHoverChange); + NS_IMETHOD_(void) ClearMouseCapture(nsIView* aView); // caret handling NS_IMETHOD GetCaret(nsCaret **aOutCaret); @@ -4377,6 +4379,41 @@ PresShell::DispatchSynthMouseMove(nsGUIEvent *aEvent, } } +NS_IMETHODIMP_(void) +PresShell::ClearMouseCapture(nsIView* aView) +{ + if (gCaptureInfo.mContent) { + if (aView) { + // if a view was specified, ensure that the captured content + // is within this view + nsIFrame* frame = GetPrimaryFrameFor(gCaptureInfo.mContent); + if (frame) { + nsIView* view = frame->GetClosestView(); + while (view) { + if (view == aView) { + NS_RELEASE(gCaptureInfo.mContent); + // the view containing the captured content likely disappeared so + // disable capture for now. + gCaptureInfo.mAllowed = PR_FALSE; + break; + } + + view = view->GetParent(); + } + // return if the view wasn't found + return; + } + } + + NS_RELEASE(gCaptureInfo.mContent); + } + + // disable mouse capture until the next mousedown as a dialog has opened + // or a drag has started. Otherwise, someone could start capture during + // the modal dialog or drag. + gCaptureInfo.mAllowed = PR_FALSE; +} + NS_IMETHODIMP PresShell::DoGetContents(const nsACString& aMimeType, PRUint32 aFlags, PRBool aSelectionOnly, nsAString& aOutValue) { @@ -5701,6 +5738,22 @@ PresShell::PaintDefaultBackground(nsIView* aView, return NS_OK; } +// static +void +nsIPresShell::SetCapturingContent(nsIContent* aContent, PRUint8 aFlags) +{ + NS_IF_RELEASE(gCaptureInfo.mContent); + + // only set capturing content if allowed or the CAPTURE_IGNOREALLOWED flag + // is used + if ((aFlags & CAPTURE_IGNOREALLOWED) || gCaptureInfo.mAllowed) { + if (aContent) { + NS_ADDREF(gCaptureInfo.mContent = aContent); + } + gCaptureInfo.mRetargetToElement = (aFlags & CAPTURE_RETARGETTOELEMENT) != 0; + } +} + nsIFrame* PresShell::GetCurrentEventFrame() { @@ -5857,40 +5910,49 @@ PresShell::HandleEvent(nsIView *aView, } #endif + nsCOMPtr retargetEventDoc; // key and IME events must be targeted at the presshell for the focused frame - if (!sDontRetargetEvents && NS_IsEventTargetedAtFocusedWindow(aEvent)) { - nsIFocusManager* fm = nsFocusManager::GetFocusManager(); - if (!fm) - return NS_ERROR_FAILURE; + if (!sDontRetargetEvents) { + if (NS_IsEventTargetedAtFocusedWindow(aEvent)) { + nsIFocusManager* fm = nsFocusManager::GetFocusManager(); + if (!fm) + return NS_ERROR_FAILURE; + + nsCOMPtr window; + fm->GetFocusedWindow(getter_AddRefs(window)); - nsCOMPtr window; - fm->GetFocusedWindow(getter_AddRefs(window)); + // if there is no focused frame, there isn't anything to fire a key event + // at so just return + nsCOMPtr piWindow = do_QueryInterface(window); + if (!piWindow) + return NS_OK; - // if there is no focused frame, there isn't anything to fire a key event - // at so just return - nsCOMPtr piWindow = do_QueryInterface(window); - if (!piWindow) - return NS_OK; + retargetEventDoc = do_QueryInterface(piWindow->GetExtantDocument()); + if (!retargetEventDoc) + return NS_OK; + } else if (NS_IS_MOUSE_EVENT(aEvent) && GetCapturingContent()) { + // if the mouse is being captured then retarget the mouse event at the + // document that is being captured. + retargetEventDoc = gCaptureInfo.mContent->GetCurrentDoc(); + } - nsCOMPtr doc(do_QueryInterface(piWindow->GetExtantDocument())); - if (!doc) - return NS_OK; + if (retargetEventDoc) { + nsIPresShell* presShell = retargetEventDoc->GetPrimaryShell(); + if (!presShell) + return NS_OK; - nsIPresShell *presShell = doc->GetPrimaryShell(); - if (!presShell) - return NS_OK; + if (presShell != this) { + nsCOMPtr viewObserver = do_QueryInterface(presShell); + if (!viewObserver) + return NS_ERROR_FAILURE; - if (presShell != this) { - nsCOMPtr viewObserver = do_QueryInterface(presShell); - if (!viewObserver) - return NS_ERROR_FAILURE; - - nsIView *view; - presShell->GetViewManager()->GetRootView(view); - sDontRetargetEvents = PR_TRUE; - nsresult rv = viewObserver->HandleEvent(view, aEvent, aEventStatus); - sDontRetargetEvents = PR_FALSE; - return rv; + nsIView *view; + presShell->GetViewManager()->GetRootView(view); + sDontRetargetEvents = PR_TRUE; + nsresult rv = viewObserver->HandleEvent(view, aEvent, aEventStatus); + sDontRetargetEvents = PR_FALSE; + return rv; + } } } @@ -5941,8 +6003,41 @@ PresShell::HandleEvent(nsIView *aView, return NS_OK; } + PRBool getDescendantPoint = PR_TRUE; nsIFrame* frame = static_cast(aView->GetClientData()); + if (NS_IS_MOUSE_EVENT(aEvent) && GetCapturingContent()) { + // if a node is capturing the mouse, get the frame for the capturing + // content and use that instead. However, if the content has no parent, + // such as the root frame, get the parent canvas frame instead. This + // ensures that positioned frames are included when hit-testing. + nsIContent* capturingContent = gCaptureInfo.mContent; + frame = GetPrimaryFrameFor(capturingContent); + if (frame) { + getDescendantPoint = !gCaptureInfo.mRetargetToElement; + if (!capturingContent->GetParent()) { + frame = frame->GetParent(); + } + else { + // special case for