From 427ce77d4e2363db201a9ed5b3ded15e2bb355e4 Mon Sep 17 00:00:00 2001 From: Marco Antognini Date: Sun, 18 May 2014 00:22:20 +0200 Subject: [PATCH] Added OS X implementation of sf::Window::setCursorGrabbed This implementation uses the following workaround: - resize flag is removed from the window when the cursor is grabbed - when grabbed, the cursor is projected inside the view if needed - to avoid letting the user clic outside the view, the cursor is disconnected and manually moved using relative motion - the initial potential projection of the cursor results in a big delta for the next move and needed to be somehow ignored (see note about m_deltaXBuffer and m_deltaYBuffer in SFOpenGLView.h) --- src/SFML/Window/OSX/SFOpenGLView+mouse.mm | 161 +++++++++++++++--- src/SFML/Window/OSX/SFOpenGLView+mouse_priv.h | 31 ++++ src/SFML/Window/OSX/SFOpenGLView.h | 36 ++++ src/SFML/Window/OSX/SFOpenGLView.mm | 11 +- src/SFML/Window/OSX/SFViewController.mm | 7 + src/SFML/Window/OSX/SFWindowController.h | 6 + src/SFML/Window/OSX/SFWindowController.mm | 29 ++++ src/SFML/Window/OSX/WindowImplCocoa.hpp | 2 +- src/SFML/Window/OSX/WindowImplCocoa.mm | 2 +- .../Window/OSX/WindowImplDelegateProtocol.h | 4 +- 10 files changed, 263 insertions(+), 26 deletions(-) diff --git a/src/SFML/Window/OSX/SFOpenGLView+mouse.mm b/src/SFML/Window/OSX/SFOpenGLView+mouse.mm index d12df1c2..d42f382c 100644 --- a/src/SFML/Window/OSX/SFOpenGLView+mouse.mm +++ b/src/SFML/Window/OSX/SFOpenGLView+mouse.mm @@ -27,6 +27,7 @@ // Headers //////////////////////////////////////////////////////////// #include +#include #import #import @@ -52,20 +53,29 @@ //////////////////////////////////////////////////////// -(void)updateMouseState { + // Update in/out state BOOL mouseWasIn = m_mouseIsIn; m_mouseIsIn = [self isMouseInside]; - if (m_requester == 0) - return; - // Send event if needed. - if (mouseWasIn && !m_mouseIsIn) - m_requester->mouseMovedOut(); - else if (!mouseWasIn && m_mouseIsIn) - m_requester->mouseMovedIn(); + if (m_requester != 0) + { + if (mouseWasIn && !m_mouseIsIn) + m_requester->mouseMovedOut(); + else if (!mouseWasIn && m_mouseIsIn) + m_requester->mouseMovedIn(); + } } +//////////////////////////////////////////////////////// +-(void)setCursorGrabbed:(BOOL)grabbed +{ + m_cursorGrabbed = grabbed; + + [self updateCursorGrabbed]; +} + //////////////////////////////////////////////////////// -(void)mouseDown:(NSEvent*)theEvent @@ -200,17 +210,68 @@ //////////////////////////////////////////////////////// -(void)handleMouseMove:(NSEvent*)theEvent { - if (m_requester != 0) - { - NSPoint loc = [self cursorPositionFromEvent:theEvent]; + NSPoint loc = [self cursorPositionFromEvent:theEvent]; - // Make sure the point is inside the view. - // (mouseEntered: and mouseExited: are not immediately called - // when the mouse is dragged. That would be too easy!) - [self updateMouseState]; - if (m_mouseIsIn) - m_requester->mouseMovedAt(loc.x, loc.y); + // If the cursor is grabbed, cursorPositionFromEvent: will + // return its correct position but not move actually it + // so we do it now. + if ([self isCursorCurrentlyGrabbed]) + [self moveCursorTo:loc]; + + // Make sure the point is inside the view. + // (mouseEntered: and mouseExited: are not immediately called + // when the mouse is dragged. That would be too easy!) + [self updateMouseState]; + if ((m_requester != 0) && m_mouseIsIn) + m_requester->mouseMovedAt(loc.x, loc.y); +} + + +//////////////////////////////////////////////////////// +-(BOOL)isCursorCurrentlyGrabbed +{ + return [[self window] isKeyWindow] && (m_cursorGrabbed || m_fullscreen); +} + + +//////////////////////////////////////////////////////// +-(void)updateCursorGrabbed +{ + // Disable/enable normal movements of the cursor + // and project the cursor if needed. + if ([self isCursorCurrentlyGrabbed]) + { + CGAssociateMouseAndMouseCursorPosition(NO); + + // Similarly to handleMouseMove: but without event. + NSPoint loc = [self cursorPositionFromEvent:nil]; + [self moveCursorTo:loc]; } + else + { + CGAssociateMouseAndMouseCursorPosition(YES); + } +} + + +//////////////////////////////////////////////////////// +-(void)moveCursorTo:(NSPoint)loc +{ + // Convert the point from SFML coord system to screen coord system. + NSPoint screenLocation = [self computeGlobalPositionOfRelativePoint:loc]; + + // This won't produce a move event, which is perfect if the cursor was grabbed + // as we move it manually based on delta values of the cursor. + CGDisplayMoveCursorToPoint([self displayId], NSPointToCGPoint(screenLocation)); +} + + +//////////////////////////////////////////////////////// +-(CGDirectDisplayID)displayId +{ + NSScreen* screen = [[self window] screen]; + NSNumber* displayId = [[screen deviceDescription] objectForKey:@"NSScreenNumber"]; + return [displayId intValue]; } @@ -247,18 +308,74 @@ //////////////////////////////////////////////////////// -(NSPoint)cursorPositionFromEvent:(NSEvent*)eventOrNil { - NSPoint loc; + NSPoint rawPos; + // If no event given then get current mouse pos. if (eventOrNil == nil) - { - NSPoint rawPos = [[self window] mouseLocationOutsideOfEventStream]; - loc = [self convertPoint:rawPos fromView:nil]; - } + rawPos = [[self window] mouseLocationOutsideOfEventStream]; else + rawPos = [eventOrNil locationInWindow]; + + if ([self isCursorCurrentlyGrabbed]) { - loc = [self convertPoint:[eventOrNil locationInWindow] fromView:nil]; + if (eventOrNil != nil) + { + // Special case when the mouse is grabbed: + // we need to take into account the delta since the cursor + // is dissociated from its position. + + // Ignore any non-move related event + if (([eventOrNil type] == NSMouseMoved) || + ([eventOrNil type] == NSLeftMouseDragged) || + ([eventOrNil type] == NSRightMouseDragged) || + ([eventOrNil type] == NSOtherMouseDragged)) + { + // Without this factor, the cursor flies around waaay too fast! + // But I don't know if it because of retina display or because + // some event are sent twice (and that in itself is another mystery). + CGFloat factor = 2; + + // Also, this factor is not the same when keeping track of how much + // we move the cursor (buffers) when projecting the cursor into the + // view when grabbing the cursor for the first time. + CGFloat factorBuffer = m_fullscreen ? 1 : 2; + + CGFloat deltaX = [eventOrNil deltaX]; + CGFloat deltaY = [eventOrNil deltaY]; + + // If the buffer for X is empty, move the cursor; + // otherwise decrement this buffer a bit. + if (m_deltaXBuffer <= 0) + rawPos.x += deltaX / factor; + else + m_deltaXBuffer -= std::abs(deltaX / factorBuffer); + + // Rinse and repeat for Y. + if (m_deltaYBuffer <= 0) + rawPos.y -= deltaY / factor; + else + m_deltaYBuffer -= std::abs(deltaY / factorBuffer); + } + } + + // We also make sure the new point is inside the view + NSSize size = [self frame].size; + NSPoint origin = [self frame].origin; + NSPoint oldPos = rawPos; + rawPos.x = std::min(std::max(origin.x, rawPos.x), origin.x + size.width - 1); + rawPos.y = std::min(std::max(origin.y + 1, rawPos.y), origin.y + size.height); + // Note: the `-1` and `+1` on the two lines above prevent the user to click + // on the left or below the window, repectively, and therefore prevent the + // application to lose focus by accident. The sign of this offset is determinded + // by the direction of the x and y axis. + + // Increase X and Y buffer with the distance of the projection + m_deltaXBuffer += std::abs(rawPos.x - oldPos.x); + m_deltaYBuffer += std::abs(rawPos.y - oldPos.y); } + NSPoint loc = [self convertPoint:rawPos fromView:nil]; + // Don't forget to change to SFML coord system. float h = [self frame].size.height; loc.y = h - loc.y; diff --git a/src/SFML/Window/OSX/SFOpenGLView+mouse_priv.h b/src/SFML/Window/OSX/SFOpenGLView+mouse_priv.h index d020b796..4efa64c3 100644 --- a/src/SFML/Window/OSX/SFOpenGLView+mouse_priv.h +++ b/src/SFML/Window/OSX/SFOpenGLView+mouse_priv.h @@ -66,6 +66,37 @@ //////////////////////////////////////////////////////////// -(void)handleMouseMove:(NSEvent*)theEvent; +//////////////////////////////////////////////////////////// +/// \brief Check whether the cursor is grabbed or not +/// +/// The cursor is grabbed if the window is active (key) and +/// either it is in fullscreen mode or the user wants to +/// grab it. +/// +//////////////////////////////////////////////////////////// +-(BOOL)isCursorCurrentlyGrabbed; + +//////////////////////////////////////////////////////////// +/// \brief (Dis)connect the cursor's movements from/to the system +/// and project the cursor into the view +/// +//////////////////////////////////////////////////////////// +-(void)updateCursorGrabbed; + +//////////////////////////////////////////////////////////// +/// \brief Move the cursor to the given location +/// +/// \param loc location expressed in SFML coordinate system +/// +//////////////////////////////////////////////////////////// +-(void)moveCursorTo:(NSPoint)loc; + +//////////////////////////////////////////////////////////// +/// \brief Get the display identifier on which the view is +/// +//////////////////////////////////////////////////////////// +-(CGDirectDisplayID)displayId; + //////////////////////////////////////////////////////////// /// \brief Convert the NSEvent mouse button type to SFML type /// diff --git a/src/SFML/Window/OSX/SFOpenGLView.h b/src/SFML/Window/OSX/SFOpenGLView.h index ec4078c6..d397f2bf 100644 --- a/src/SFML/Window/OSX/SFOpenGLView.h +++ b/src/SFML/Window/OSX/SFOpenGLView.h @@ -52,6 +52,19 @@ namespace sf { /// implementation. However, all attributes are defined in the main /// interface declaration right below. /// +/// Note about deltaXBuffer and deltaYBuffer: when grabbing the cursor +/// for the first time, either by entering fullscreen or through +/// setCursorGrabbed:, the cursor might be projected into the view. +/// Doing this will result in a big delta (relative movement) in the +/// next move event (cursorPositionFromEvent:), because no move event +/// is generated, which in turn will give the impression that the user +/// want to move the cursor by the same distance it was projected. To +/// prevent the cursor to fly twice the distance we keep track of how +/// much the cursor was projected in deltaXBuffer and deltaYBuffer. In +/// cursorPositionFromEvent: we can then reduce/augment those buffers +/// to determine when a move event should result in an actual move of +/// the cursor (that was disconnected from the system). +/// //////////////////////////////////////////////////////////// @interface SFOpenGLView : NSOpenGLView { @@ -61,6 +74,9 @@ namespace sf { NSTrackingArea* m_trackingArea; ///< Mouse tracking area BOOL m_fullscreen; ///< Indicate whether the window is fullscreen or not CGFloat m_scaleFactor; ///< Display scale factor (e.g. 1x for classic display, 2x for retina) + BOOL m_cursorGrabbed; ///< Is the mouse cursor trapped? + CGFloat m_deltaXBuffer; ///< See note about cursor grabbing above + CGFloat m_deltaYBuffer; ///< See note about cursor grabbing above // Hidden text view used to convert key event to actual chars. // We use a silent responder to prevent sound alerts. @@ -143,6 +159,8 @@ namespace sf { /// /// \param eventOrNil if nil the cursor position is the current one /// +/// \return the mouse position in SFML coord system +/// //////////////////////////////////////////////////////////// -(NSPoint)cursorPositionFromEvent:(NSEvent*)eventOrNil; @@ -154,4 +172,22 @@ namespace sf { //////////////////////////////////////////////////////////// -(BOOL)isMouseInside; +//////////////////////////////////////////////////////////// +/// Clips or releases the mouse cursor +/// +/// Generate a MouseEntered event when it makes sense. +/// +/// \param grabbed YES to grab, NO to release +/// +//////////////////////////////////////////////////////////// +-(void)setCursorGrabbed:(BOOL)grabbed; + +//////////////////////////////////////////////////////////// +/// Update the cursor position according to the grabbing behaviour +/// +/// This function has to be called when the window's state change +/// +//////////////////////////////////////////////////////////// +-(void)updateCursorGrabbed; + @end diff --git a/src/SFML/Window/OSX/SFOpenGLView.mm b/src/SFML/Window/OSX/SFOpenGLView.mm index 2e7ba1e7..15b4cc25 100644 --- a/src/SFML/Window/OSX/SFOpenGLView.mm +++ b/src/SFML/Window/OSX/SFOpenGLView.mm @@ -108,6 +108,9 @@ m_fullscreen = isFullscreen; m_scaleFactor = 1.0; // Default value; it will be updated in finishInit + m_cursorGrabbed = NO; + m_deltaXBuffer = 0; + m_deltaYBuffer = 0; // Create a hidden text view for parsing key down event properly m_silentResponder = [[SFSilentResponder alloc] init]; @@ -163,8 +166,9 @@ name:NSWindowDidChangeScreenProfileNotification object:[self window]]; - // Now that we have a window, set up correctly the scale factor + // Now that we have a window, set up correctly the scale factor and cursor grabbing [self updateScaleFactor]; + [self updateCursorGrabbed]; // update for fullscreen } @@ -245,6 +249,7 @@ // Update mouse internal state. [self updateMouseState]; + [self updateCursorGrabbed]; // Update the OGL view to fit the new size. [self update]; @@ -263,6 +268,8 @@ { (void)notification; + [self updateCursorGrabbed]; + if (m_requester) m_requester->windowGainedFocus(); @@ -276,6 +283,8 @@ { (void)notification; + [self updateCursorGrabbed]; + if (m_requester) m_requester->windowLostFocus(); diff --git a/src/SFML/Window/OSX/SFViewController.mm b/src/SFML/Window/OSX/SFViewController.mm index 8e35f792..ac6a9625 100644 --- a/src/SFML/Window/OSX/SFViewController.mm +++ b/src/SFML/Window/OSX/SFViewController.mm @@ -121,6 +121,13 @@ } +//////////////////////////////////////////////////////// +-(void)setCursorGrabbed:(BOOL)grabbed +{ + [m_oglView setCursorGrabbed:grabbed]; +} + + //////////////////////////////////////////////////////////// -(NSPoint)position { diff --git a/src/SFML/Window/OSX/SFWindowController.h b/src/SFML/Window/OSX/SFWindowController.h index 3736b92a..dd817ad6 100644 --- a/src/SFML/Window/OSX/SFWindowController.h +++ b/src/SFML/Window/OSX/SFWindowController.h @@ -49,6 +49,11 @@ namespace sf { /// Used when SFML handle everything and when a NSWindow* is given /// as handle to WindowImpl. /// +/// When grabbing the cursor, if the window is resizeable, m_restoreResize is +/// set to YES and the window is marked as not resizeable. This is to prevent +/// accidental resize by the user. When the cursor is released, the window +/// style is restored. +/// //////////////////////////////////////////////////////////// @interface SFWindowController : NSResponder { @@ -56,6 +61,7 @@ namespace sf { SFOpenGLView* m_oglView; ///< OpenGL view for rendering sf::priv::WindowImplCocoa* m_requester; ///< Requester BOOL m_fullscreen; ///< Indicate whether the window is fullscreen or not + BOOL m_restoreResize; ///< See note above } //////////////////////////////////////////////////////////// diff --git a/src/SFML/Window/OSX/SFWindowController.mm b/src/SFML/Window/OSX/SFWindowController.mm index 72bd29d4..9b8d79ea 100644 --- a/src/SFML/Window/OSX/SFWindowController.mm +++ b/src/SFML/Window/OSX/SFWindowController.mm @@ -97,6 +97,8 @@ m_window = nil; m_oglView = nil; m_requester = 0; + m_fullscreen = NO; // assuming this is the case... too hard to handle anyway. + m_restoreResize = NO; // Retain the window for our own use. m_window = [window retain]; @@ -148,6 +150,7 @@ m_oglView = nil; m_requester = 0; m_fullscreen = (style & sf::Style::Fullscreen); + m_restoreResize = NO; if (m_fullscreen) [self setupFullscreenViewWithMode:mode]; @@ -345,6 +348,29 @@ } +//////////////////////////////////////////////////////// +-(void)setCursorGrabbed:(BOOL)grabbed +{ + // Remove or restore resizeable style if needed + BOOL resizeable = [m_window styleMask] & NSResizableWindowMask; + if (grabbed && resizeable) + { + m_restoreResize = YES; + NSUInteger newStyle = [m_window styleMask] & ~NSResizableWindowMask; + [m_window setStyleMask:newStyle]; + } + else if (!grabbed && m_restoreResize) + { + m_restoreResize = NO; + NSUInteger newStyle = [m_window styleMask] | NSResizableWindowMask; + [m_window setStyleMask:newStyle]; + } + + // Forward to our view + [m_oglView setCursorGrabbed:grabbed]; +} + + //////////////////////////////////////////////////////////// -(NSPoint)position { @@ -379,6 +405,9 @@ // Place the window. [m_window setFrameTopLeftPoint:point]; + + // In case the cursor was grabbed we need to update its position + [m_oglView updateCursorGrabbed]; } diff --git a/src/SFML/Window/OSX/WindowImplCocoa.hpp b/src/SFML/Window/OSX/WindowImplCocoa.hpp index 1e88be08..e87acd9b 100644 --- a/src/SFML/Window/OSX/WindowImplCocoa.hpp +++ b/src/SFML/Window/OSX/WindowImplCocoa.hpp @@ -316,7 +316,7 @@ public: //////////////////////////////////////////////////////////// /// \brief Grab or release the mouse cursor /// - /// \param grabbed True to enable, false to disable + /// \param grabbed True to grab, false to release /// //////////////////////////////////////////////////////////// virtual void setMouseCursorGrabbed(bool grabbed); diff --git a/src/SFML/Window/OSX/WindowImplCocoa.mm b/src/SFML/Window/OSX/WindowImplCocoa.mm index 63635885..653dcd86 100644 --- a/src/SFML/Window/OSX/WindowImplCocoa.mm +++ b/src/SFML/Window/OSX/WindowImplCocoa.mm @@ -489,7 +489,7 @@ void WindowImplCocoa::setMouseCursorVisible(bool visible) //////////////////////////////////////////////////////////// void WindowImplCocoa::setMouseCursorGrabbed(bool grabbed) { - + [m_delegate setCursorGrabbed:grabbed]; } diff --git a/src/SFML/Window/OSX/WindowImplDelegateProtocol.h b/src/SFML/Window/OSX/WindowImplDelegateProtocol.h index 3f769c50..971fa87a 100644 --- a/src/SFML/Window/OSX/WindowImplDelegateProtocol.h +++ b/src/SFML/Window/OSX/WindowImplDelegateProtocol.h @@ -99,8 +99,10 @@ namespace sf { //////////////////////////////////////////////////////////// /// \brief Grab or release the mouse cursor /// +/// \param grabbed YES to grab, NO to release +/// //////////////////////////////////////////////////////////// --(void)setCursorGrabbed:(bool)grabbed; +-(void)setCursorGrabbed:(BOOL)grabbed; //////////////////////////////////////////////////////////// /// \brief Get window position