Bug 1284788 - Allow change target of pointer lock when the pointer has been locked in the document. r=smaug draft
authorXidorn Quan <me@upsuper.org>
Wed, 20 Jul 2016 14:42:08 +1000
changeset 392233 ce3826834065242a4e99b82d52ac65ddc205ae61
parent 392232 e2eaa5594d271edd196f4d4b6c721691aad22de2
child 526280 8b61a73a7a6782003ca7cc6e8a3a754cce865539
push id23969
push userxquan@mozilla.com
push dateMon, 25 Jul 2016 05:15:55 +0000
reviewerssmaug
bugs1284788
milestone50.0a1
Bug 1284788 - Allow change target of pointer lock when the pointer has been locked in the document. r=smaug MozReview-Commit-ID: HiPkCPrQQr0
dom/base/nsDocument.cpp
dom/events/EventStateManager.cpp
dom/tests/mochitest/pointerlock/file_changeLockElement.html
dom/tests/mochitest/pointerlock/mochitest.ini
dom/tests/mochitest/pointerlock/test_pointerlock-api.html
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -12364,16 +12364,49 @@ GetPointerLockError(Element* aElement, E
     if (!top->GetExtantDoc()->HasFocus(rv)) {
       return "PointerLockDeniedNotFocused";
     }
   }
 
   return nullptr;
 }
 
+static void
+ChangePointerLockedElement(Element* aElement, nsIDocument* aDocument,
+                           Element* aPointerLockedElement)
+{
+  // aDocument here is not really necessary, as it is the uncomposed
+  // document of both aElement and aPointerLockedElement as far as one
+  // is not nullptr, and they wouldn't both be nullptr in any case.
+  // But since the caller of this function should have known what the
+  // document is, we just don't try to figure out what it should be.
+  MOZ_ASSERT(aDocument);
+  MOZ_ASSERT(aElement != aPointerLockedElement);
+  if (aPointerLockedElement) {
+    MOZ_ASSERT(aPointerLockedElement->GetUncomposedDoc() == aDocument);
+    aPointerLockedElement->ClearPointerLock();
+  }
+  if (aElement) {
+    MOZ_ASSERT(aElement->GetUncomposedDoc() == aDocument);
+    aElement->SetPointerLock();
+    EventStateManager::sPointerLockedElement = do_GetWeakReference(aElement);
+    EventStateManager::sPointerLockedDoc = do_GetWeakReference(aDocument);
+    NS_ASSERTION(EventStateManager::sPointerLockedElement &&
+                 EventStateManager::sPointerLockedDoc,
+                 "aElement and this should support weak references!");
+  } else {
+    EventStateManager::sPointerLockedElement = nullptr;
+    EventStateManager::sPointerLockedDoc = nullptr;
+  }
+  // Retarget all events to aElement via capture or
+  // stop retargeting if aElement is nullptr.
+  nsIPresShell::SetCapturingContent(aElement, CAPTURE_POINTERLOCK);
+  DispatchPointerLockChange(aDocument);
+}
+
 NS_IMETHODIMP
 PointerLockRequest::Run()
 {
   nsCOMPtr<Element> e = do_QueryReferent(mElement);
   nsCOMPtr<nsIDocument> doc = do_QueryReferent(mDocument);
   nsDocument* d = static_cast<nsDocument*>(doc.get());
   const char* error = nullptr;
   if (!e || !d || !e->GetUncomposedDoc()) {
@@ -12385,42 +12418,40 @@ PointerLockRequest::Run()
     nsCOMPtr<Element> pointerLockedElement =
       do_QueryReferent(EventStateManager::sPointerLockedElement);
     if (e == pointerLockedElement) {
       DispatchPointerLockChange(d);
       return NS_OK;
     }
     // Note, we must bypass focus change, so pass true as the last parameter!
     error = GetPointerLockError(e, pointerLockedElement, true);
+    // Another element in the same document is requesting pointer lock,
+    // just grant it without user input check.
+    if (!error && pointerLockedElement) {
+      ChangePointerLockedElement(e, d, pointerLockedElement);
+      return NS_OK;
+    }
   }
   // If it is neither user input initiated, nor requested in fullscreen,
   // it should be rejected.
   if (!error && !mUserInputOrChromeCaller && !doc->GetFullscreenElement()) {
     error = "PointerLockDeniedNotInputDriven";
   }
   if (!error && !d->SetPointerLock(e, NS_STYLE_CURSOR_NONE)) {
     error = "PointerLockDeniedFailedToLock";
   }
   if (error) {
     DispatchPointerLockError(d, error);
     return NS_OK;
   }
 
-  e->SetPointerLock();
-  EventStateManager::sPointerLockedElement = do_GetWeakReference(e);
-  EventStateManager::sPointerLockedDoc = do_GetWeakReference(doc);
-  NS_ASSERTION(EventStateManager::sPointerLockedElement &&
-               EventStateManager::sPointerLockedDoc,
-               "aElement and this should support weak references!");
-
+  ChangePointerLockedElement(e, d, nullptr);
   nsContentUtils::DispatchEventOnlyToChrome(
     doc, ToSupports(e), NS_LITERAL_STRING("MozDOMPointerLock:Entered"),
     /* Bubbles */ true, /* Cancelable */ false, /* DefaultAction */ nullptr);
-
-  DispatchPointerLockChange(d);
   return NS_OK;
 }
 
 void
 nsDocument::RequestPointerLock(Element* aElement)
 {
   NS_ASSERTION(aElement,
     "Must pass non-null element to nsDocument::RequestPointerLock");
@@ -12502,29 +12533,22 @@ nsDocument::UnlockPointer(nsIDocument* a
   }
   nsDocument* doc = static_cast<nsDocument*>(pointerLockedDoc.get());
   if (!doc->SetPointerLock(nullptr, NS_STYLE_CURSOR_AUTO)) {
     return;
   }
 
   nsCOMPtr<Element> pointerLockedElement =
     do_QueryReferent(EventStateManager::sPointerLockedElement);
-  if (pointerLockedElement) {
-    pointerLockedElement->ClearPointerLock();
-  }
-
-  EventStateManager::sPointerLockedElement = nullptr;
-  EventStateManager::sPointerLockedDoc = nullptr;
+  ChangePointerLockedElement(nullptr, doc, pointerLockedElement);
 
   nsContentUtils::DispatchEventOnlyToChrome(
     doc, ToSupports(pointerLockedElement),
     NS_LITERAL_STRING("MozDOMPointerLock:Exited"),
     /* Bubbles */ true, /* Cancelable */ false, /* DefaultAction */ nullptr);
-
-  DispatchPointerLockChange(pointerLockedDoc);
 }
 
 void
 nsIDocument::UnlockPointer(nsIDocument* aDoc)
 {
   nsDocument::UnlockPointer(aDoc);
 }
 
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -4341,19 +4341,16 @@ EventStateManager::SetPointerLock(nsIWid
 
     // 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.
     sLastRefPoint = GetWindowClientRectCenter(aWidget);
     aWidget->SynthesizeNativeMouseMove(
       sLastRefPoint + aWidget->WidgetToScreenOffset(), nullptr);
 
-    // Retarget all events to this element via capture.
-    nsIPresShell::SetCapturingContent(aElement, CAPTURE_POINTERLOCK);
-
     // Suppress DnD
     if (dragService) {
       dragService->Suppress();
     }
   } else {
     // 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
@@ -4362,19 +4359,16 @@ EventStateManager::SetPointerLock(nsIWid
     // Reset SynthCenteringPoint to invalid so that next time we start
     // locking pointer, it has its initial value.
     sSynthCenteringPoint = kInvalidRefPoint;
     if (aWidget) {
       aWidget->SynthesizeNativeMouseMove(
         mPreLockPoint + aWidget->WidgetToScreenOffset(), nullptr);
     }
 
-    // Don't retarget events to this element any more.
-    nsIPresShell::SetCapturingContent(nullptr, CAPTURE_POINTERLOCK);
-
     // Unsuppress DnD
     if (dragService) {
       dragService->Unsuppress();
     }
   }
 }
 
 void
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/pointerlock/file_changeLockElement.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Bug 1284788</title>
+  <script src="/tests/SimpleTest/EventUtils.js"></script>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="pointerlock_utils.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+  <style>
+    #block1, #block2, #block3 {
+      background: blue;
+      width: 50px; height: 50px;
+      margin: 10px;
+    }
+  </style>
+</head>
+<body>
+  <div id="block1"></div>
+  <div id="block2"></div>
+  <div id="block3"></div>
+  <div id="test">
+    <script>
+      SimpleTest.waitForExplicitFinish();
+      SimpleTest.requestFlakyTimeout("For changing pointer lock element not in a valid user event handler");
+
+      var block1 = document.getElementById("block1");
+      var block2 = document.getElementById("block2");
+      var block3 = document.getElementById("block3");
+
+      class ClickTester {
+        constructor(target) {
+          this._target = target;
+          this._callback = null;
+          document.addEventListener("click", this);
+        }
+
+        synthesize(callback) {
+          ok(!this._callback, "No callback should have been hooked");
+          this._callback = callback;
+          synthesizeMouseAtCenter(this._target, {}, window);
+        }
+
+        handleEvent(e) {
+          ok(!!this._callback, "Should have hooked a callback");
+          var callback = this._callback;
+          this._callback = null;
+          callback(e);
+        }
+      };
+
+      var tester = new ClickTester(block3);
+      // It would be called in handler of load event in pointerlock_utils.js
+      function start() {
+        tester.synthesize(firstClick);
+      }
+
+      function firstClick(e) {
+        is(e.target, block3, "Click is triggered inside block3");
+        document.addEventListener("mozpointerlockchange", lockedPointerOnBlock1);
+        block1.mozRequestPointerLock();
+      }
+
+      function lockedPointerOnBlock1() {
+        document.removeEventListener("mozpointerlockchange", lockedPointerOnBlock1);
+        is(document.mozPointerLockElement, block1, "Pointer should be locked on #block1");
+        SimpleTest.executeSoon(() => {
+          tester.synthesize(secondClick);
+        });
+      }
+
+      function secondClick(e) {
+        is(e.target, block1, "Event should be redirected to block1");
+        // Use 2s to ensure that we never consider this as an extension of user input.
+        setTimeout(() => {
+          document.addEventListener("mozpointerlockchange", lockedPointerOnBlock2);
+          block2.mozRequestPointerLock();
+        }, 2000);
+      }
+
+      function lockedPointerOnBlock2() {
+        document.removeEventListener("mozpointerlockchange", lockedPointerOnBlock2);
+        is(document.mozPointerLockElement, block2, "Pointer should be locked on #block2");
+        SimpleTest.executeSoon(() => {
+          tester.synthesize(thirdClick);
+        });
+      }
+
+      function thirdClick(e) {
+        is(e.target, block2, "Event should be redirected to block2");
+        // Use 2s to ensure that we never consider this as an extension of user input.
+        setTimeout(() => {
+          document.addEventListener("mozpointerlockchange", lockedPointerOnBlock1Again);
+          block1.mozRequestPointerLock();
+        }, 2000);
+      }
+
+      function lockedPointerOnBlock1Again() {
+        document.removeEventListener("mozpointerlockchange", lockedPointerOnBlock1Again);
+        is(document.mozPointerLockElement, block1, "Pointer should be locked on #block1 again");
+        SimpleTest.executeSoon(() => {
+          tester.synthesize(fourthClick);
+        });
+      }
+
+      function fourthClick(e) {
+        is(e.target, block1, "Event should be redirected to block1 again");
+        document.addEventListener("mozpointerlockchange", () => SimpleTest.finish());
+        document.mozExitPointerLock();
+      }
+
+    </script>
+  </div>
+</body>
+</html>
--- a/dom/tests/mochitest/pointerlock/mochitest.ini
+++ b/dom/tests/mochitest/pointerlock/mochitest.ini
@@ -13,13 +13,14 @@ support-files =
   file_movementXY.html
   file_infiniteMovement.html
   file_retargetMouseEvents.html
   file_targetOutOfFocus.html
   file_screenClientXYConst.html
   file_suppressSomeMouseEvents.html
   file_locksvgelement.html
   file_allowPointerLockSandboxFlag.html
+  file_changeLockElement.html
   iframe_differentDOM.html
 
 [test_pointerlock-api.html]
 tags = fullscreen
 skip-if = buildapp == 'b2g' || toolkit == 'android' || os == 'win' # B2G - window.open focus issues using fullscreen. Win: Bug 931445
--- a/dom/tests/mochitest/pointerlock/test_pointerlock-api.html
+++ b/dom/tests/mochitest/pointerlock/test_pointerlock-api.html
@@ -51,17 +51,18 @@ https://bugzilla.mozilla.org/show_bug.cg
           "file_pointerlock-api.html",
           "file_pointerlockerror.html",
           "file_pointerLockPref.html",
           "file_removedFromDOM.html",
           "file_retargetMouseEvents.html",
           "file_suppressSomeMouseEvents.html",
           "file_targetOutOfFocus.html",
           "file_withoutDOM.html",
-          "file_allowPointerLockSandboxFlag.html"
+          "file_allowPointerLockSandboxFlag.html",
+          "file_changeLockElement.html",
         ];
 
         var gDisableList = [
         ];
 
         var gTestWindow = null;
         var gTestIndex = 0;