Bug 527748 - Fire mouse enter and exit events when window focus changes. r=josh
authorMarkus Stange <mstange@themasta.com>
Fri, 11 Dec 2009 22:56:59 +0100
changeset 35621 086e9c075a15d3be38868bdaacdd7f54e0b93d52
parent 35620 12d62a61c4463620ac69088d5d4fb41e73370072
child 35622 6f43d0e35fc8ef8bc3f64982c0dda07febac1247
push idunknown
push userunknown
push dateunknown
reviewersjosh
bugs527748
milestone1.9.3a1pre
Bug 527748 - Fire mouse enter and exit events when window focus changes. r=josh
widget/src/cocoa/nsChildView.h
widget/src/cocoa/nsChildView.mm
widget/src/cocoa/nsCocoaUtils.h
widget/src/cocoa/nsCocoaUtils.mm
widget/src/cocoa/nsCocoaWindow.mm
widget/tests/native_mouse_mac_window.xul
--- a/widget/src/cocoa/nsChildView.h
+++ b/widget/src/cocoa/nsChildView.h
@@ -279,16 +279,17 @@ private:
 
 class ChildViewMouseTracker {
 
 public:
 
   static void MouseMoved(NSEvent* aEvent);
   static void OnDestroyView(ChildView* aView);
   static BOOL WindowAcceptsEvent(NSWindow* aWindow, NSEvent* aEvent);
+  static void ReEvaluateMouseEnterState(NSEvent* aEvent = nil);
 
   static ChildView* sLastMouseEventView;
 
 private:
 
   static NSWindow* WindowForEvent(NSEvent* aEvent);
   static ChildView* ViewForEvent(NSEvent* aEvent);
 };
--- a/widget/src/cocoa/nsChildView.mm
+++ b/widget/src/cocoa/nsChildView.mm
@@ -6499,36 +6499,38 @@ nsTSMManager::CancelIME()
 void
 ChildViewMouseTracker::OnDestroyView(ChildView* aView)
 {
   if (sLastMouseEventView == aView)
     sLastMouseEventView = nil;
 }
 
 void
-ChildViewMouseTracker::MouseMoved(NSEvent* aEvent)
+ChildViewMouseTracker::ReEvaluateMouseEnterState(NSEvent* aEvent)
 {
   ChildView* oldView = sLastMouseEventView;
-  ChildView* newView = ViewForEvent(aEvent);
-  sLastMouseEventView = newView;
-  if (newView != oldView) {
+  sLastMouseEventView = ViewForEvent(aEvent);
+  if (sLastMouseEventView != oldView) {
     // Send enter and / or exit events.
-    nsMouseEvent::exitType type = [newView window] == [oldView window] ?
+    nsMouseEvent::exitType type = [sLastMouseEventView window] == [oldView window] ?
                                     nsMouseEvent::eChild : nsMouseEvent::eTopLevel;
     [oldView sendMouseEnterOrExitEvent:aEvent enter:NO type:type];
     // After the cursor exits the window set it to a visible regular arrow cursor.
     if (type == nsMouseEvent::eTopLevel) {
       [[nsCursorManager sharedInstance] setCursor:eCursor_standard];
     }
-    // Sending the exit event to the old view might have destroyed our new view;
-    // if that has happened, sLastMouseEventView has been set to nil.
-    newView = sLastMouseEventView;
-    [newView sendMouseEnterOrExitEvent:aEvent enter:YES type:type];
-  }
-  [newView handleMouseMoved:aEvent];
+    [sLastMouseEventView sendMouseEnterOrExitEvent:aEvent enter:YES type:type];
+  }
+}
+
+void
+ChildViewMouseTracker::MouseMoved(NSEvent* aEvent)
+{
+  ReEvaluateMouseEnterState(aEvent);
+  [sLastMouseEventView handleMouseMoved:aEvent];
 }
 
 ChildView*
 ChildViewMouseTracker::ViewForEvent(NSEvent* aEvent)
 {
   NSWindow* window = WindowForEvent(aEvent);
   if (!window || !WindowAcceptsEvent(window, aEvent))
     return nil;
--- a/widget/src/cocoa/nsCocoaUtils.h
+++ b/widget/src/cocoa/nsCocoaUtils.h
@@ -133,16 +133,17 @@ class nsCocoaUtils
   // contain width/height info, with no difference in their use.
   static NSRect GeckoRectToCocoaRect(const nsIntRect &geckoRect);
   
   // See explanation for geckoRectToCocoaRect, guess what this does...
   static nsIntRect CocoaRectToGeckoRect(const NSRect &cocoaRect);
   
   // Gives the location for the event in screen coordinates. Do not call this
   // unless the window the event was originally targeted at is still alive!
+  // anEvent may be nil -- in that case the current mouse location is returned.
   static NSPoint ScreenLocationForEvent(NSEvent* anEvent);
   
   // Determines if an event happened over a window, whether or not the event
   // is for the window. Does not take window z-order into account.
   static BOOL IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow);
 
   // Events are set up so that their coordinates refer to the window to which they
   // were originally sent. If we reroute the event somewhere else, we'll have
--- a/widget/src/cocoa/nsCocoaUtils.mm
+++ b/widget/src/cocoa/nsCocoaUtils.mm
@@ -95,17 +95,17 @@ nsIntRect nsCocoaUtils::CocoaRectToGecko
   return rect;
 }
 
 NSPoint nsCocoaUtils::ScreenLocationForEvent(NSEvent* anEvent)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN;
 
   // Don't trust mouse locations of mouse move events, see bug 443178.
-  if ([anEvent type] == NSMouseMoved)
+  if (!anEvent || [anEvent type] == NSMouseMoved)
     return [NSEvent mouseLocation];
 
   return [[anEvent window] convertBaseToScreen:[anEvent locationInWindow]];
 
   NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NSMakePoint(0.0, 0.0));
 }
 
 BOOL nsCocoaUtils::IsEventOverWindow(NSEvent* anEvent, NSWindow* aWindow)
--- a/widget/src/cocoa/nsCocoaWindow.mm
+++ b/widget/src/cocoa/nsCocoaWindow.mm
@@ -1558,31 +1558,33 @@ nsCocoaWindow::UnifiedShading(void* aInf
   mGeckoWindow->ReportSizeEvent();
 }
 
 - (void)windowDidBecomeMain:(NSNotification *)aNotification
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   RollUpPopups();
+  ChildViewMouseTracker::ReEvaluateMouseEnterState();
 
   // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
   // app modally. If one of those is up then we want it to retain its menu bar.
   if ([NSApp _isRunningAppModal])
     return;
   NSWindow* window = [aNotification object];
   if (window)
     [WindowDelegate paintMenubarForWindow:window];
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 - (void)windowDidResignMain:(NSNotification *)aNotification
 {
   RollUpPopups();
+  ChildViewMouseTracker::ReEvaluateMouseEnterState();
 
   // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
   // app modally. If one of those is up then we want it to retain its menu bar.
   if ([NSApp _isRunningAppModal])
     return;
   nsRefPtr<nsMenuBarX> hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
   if (hiddenWindowMenuBar) {
     // printf("painting hidden window menu bar due to window losing main status\n");
@@ -1590,29 +1592,31 @@ nsCocoaWindow::UnifiedShading(void* aInf
   }
 }
 
 - (void)windowDidBecomeKey:(NSNotification *)aNotification
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   RollUpPopups();
+  ChildViewMouseTracker::ReEvaluateMouseEnterState();
 
   NSWindow* window = [aNotification object];
   if ([window isSheet])
     [WindowDelegate paintMenubarForWindow:window];
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 - (void)windowDidResignKey:(NSNotification *)aNotification
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   RollUpPopups();
+  ChildViewMouseTracker::ReEvaluateMouseEnterState();
 
   // If a sheet just resigned key then we should paint the menu bar
   // for whatever window is now main.
   NSWindow* window = [aNotification object];
   if ([window isSheet])
     [WindowDelegate paintMenubarForWindow:[NSApp mainWindow]];
 
   NS_OBJC_END_TRY_ABORT_BLOCK;
--- a/widget/tests/native_mouse_mac_window.xul
+++ b/widget/tests/native_mouse_mac_window.xul
@@ -105,26 +105,31 @@
           NSOtherMouseDragged  = 27,
           NSEventTypeGesture   = 29,
           NSEventTypeMagnify   = 30,
           NSEventTypeSwipe     = 31,
           NSEventTypeRotate    = 18,
           NSEventTypeBeginGesture = 19,
           NSEventTypeEndGesture   = 20;
 
+    const gDebug = false;
+
+    function printDebug(msg) { if (gDebug) dump(msg); }
+
     var gExpectedEvents = [];
     var gRightWindow = null, gPopup = null;
 
     function testMouse(x, y, msg, elem, win, exp, callback) {
       clearExpectedEvents();
       exp.forEach(function (expEv) {
         expEv.screenX = x;
         expEv.screenY = y;
         gExpectedEvents.push(expEv);
       });
+      printDebug("sending event: " + x + ", " + y + " (" + msg + ")\n");
       netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
       var utils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
                                      getInterface(Components.interfaces.nsIDOMWindowUtils);
       utils.sendNativeMouseEvent(x, y, msg, 0, elem);
       SimpleTest.executeSoon(function () {
         clearExpectedEvents();
         callback();
       });
@@ -134,16 +139,17 @@
       elem.addEventListener(name, function(e) {
         elem.removeEventListener(name, arguments.callee, false);
         callback(e);
       }, false);
     }
 
     function focusAndThen(win, callback) {
       eventListenOnce(win, "focus", callback);
+      printDebug("focusing a window\n");
       win.focus();
     }
 
     function eventToString(e) {
       return JSON.stringify({
         type: e.type, target: e.target.nodeName, screenX: e.screenX, screenY: e.screenY
       });
     }
@@ -154,16 +160,17 @@
         var errFun = expectedEvent.todoShouldHaveFired ? todo : ok;
         errFun(false, "didn't receive expected event: " + eventToString(expectedEvent));
       }
     }
 
     var gEventNum = 0;
 
     function eventMonitor(e) {
+      printDebug("got event: " + eventToString(e) + "\n");
       var expectedEvent = gExpectedEvents.shift();
       while (expectedEvent && expectedEvent.todoShouldHaveFired) {
         todo(false, "Should have got event: " + eventToString(expectedEvent));
         expectedEvent = gExpectedEvents.shift();
       }
       if (!expectedEvent) {
         ok(false, "received event I didn't expect: " + eventToString(e));
         return true;
@@ -233,32 +240,31 @@
           { type: "mousemove", target: leftElem },
         ]],
         // Move over the right window, which is inactive.
         // Inactive windows shouldn't respond to mousemove events,
         // so we should only get a mouseout event, no mouseover event.
         [400, 150, NSMouseMoved, null, right, [
           { type: "mouseout", target: leftElem },
         ]],
-        // Clicking an inactive window shouldn't have any effect, other
-        // than focusing it.
+        // Clicking an inactive window should make it active and fire a mouseover
+        // event.
         [400, 150, NSLeftMouseDown, null, right, [
+          { type: "mouseover", target: rightElem },
         ]],
         [400, 150, NSLeftMouseUp, null, right, [
         ]],
         // Now it's focused, so we should get a mousedown event when clicking.
         [400, 150, NSLeftMouseDown, null, right, [
           { type: "mousedown", target: rightElem },
-          { type: "mouseover", target: rightElem, todoShouldHaveFired: true },
         ]],
         // Let's drag to the right without letting the button go. It would be better
         // if the mouseover event had fired as soon as the mouse entered the window,
         // and not only when dragging, but that's ok.
         [410, 150, NSLeftMouseDragged, null, right, [
-          { type: "mouseover", target: rightElem, todoShouldNotHaveFired: true },
           { type: "mousemove", target: rightElem },
         ]],
         // Let go of the mouse.
         [410, 150, NSLeftMouseUp, null, right, [
           { type: "mouseup", target: rightElem },
           { type: "click", target: rightElem },
         ]],
         // Now we're being sneaky. The left window is inactive, but *right*-clicks to it
@@ -274,22 +280,25 @@
         [150, 170, NSRightMouseUp, null, left, [
           { type: "mouseover", target: leftElem, todoShouldHaveFired: true },
           { type: "mouseup", target: leftElem },
           { type: "click", target: leftElem },
           { type: "mouseout", target: leftElem, todoShouldHaveFired: true },
         ]],
         // Right clicking hasn't focused it, so the window is still inactive.
         // Let's focus it; this time without the mouse, for variaton's sake.
+        // Still, mouseout and mouseover events should fire.
         function raiseLeftWindow(callback) {
-          focusAndThen(left, callback);
+          clearExpectedEvents();
+          gExpectedEvents.push({ screenX: 150, screenY: 170, type: "mouseout", target: rightElem });
+          gExpectedEvents.push({ screenX: 150, screenY: 170, type: "mouseover", target: leftElem });
+          focusAndThen(left, function () { SimpleTest.executeSoon(callback); });
         },
         // It's active, so it should respond to mousemove events now.
         [150, 170, NSMouseMoved, null, left, [
-          { type: "mouseover", target: leftElem },
           { type: "mousemove", target: leftElem },
         ]],
 
         // This was boring... let's introduce a popup. It will overlap both the left
         // and the right window.
         function openPopupInLeftWindow(callback) {
           eventListenOnce(gPopup, "popupshown", callback);
           gPopup.openPopupAtScreen(150, 50, true);
@@ -323,29 +332,28 @@
         [400, 170, NSMouseMoved, null, right, [
           { type: "mouseout", target: gPopup },
           { type: "mouseout", target: gPopup, todoShouldNotHaveFired: true },
         ]],
         // Again, no mouse events please, even though a popup is open. (bug 425556)
         [400, 180, NSMouseMoved, null, right, [
         ]],
         // Activate the right window with a click.
-        // This will close the popup.
+        // This will close the popup and make the mouse enter the right window.
         [400, 180, NSLeftMouseDown, null, right, [
+          { type: "mouseover", target: rightElem },
         ]],
         [400, 180, NSLeftMouseUp, null, right, [
         ]],
         function verifyPopupClosed(callback) {
           is(gPopup.popupBoxObject.popupState, "closed", "popup should have closed when clicking");
           callback();
         },
         // Now the right window is active; click it again, just for fun.
-        // (Would be good to have a mouseover event here.)
         [400, 180, NSLeftMouseDown, null, right, [
-          { type: "mouseover", target: rightElem, todoShouldHaveFired: true },
           { type: "mousedown", target: rightElem },
         ]],
         [400, 180, NSLeftMouseUp, null, right, [
           { type: "mouseup", target: rightElem },
           { type: "click", target: rightElem },
         ]],
 
         // Time for our next trick: a tooltip!