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)
This commit is contained in:
Marco Antognini 2014-05-18 00:22:20 +02:00 committed by Lukas Dürrenberger
parent 90c01d3030
commit 427ce77d4e
10 changed files with 263 additions and 26 deletions

View File

@ -27,6 +27,7 @@
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Window/OSX/WindowImplCocoa.hpp>
#include <cmath>
#import <SFML/Window/OSX/SFOpenGLView.h>
#import <SFML/Window/OSX/SFOpenGLView+mouse_priv.h>
@ -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;

View File

@ -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
///

View File

@ -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

View File

@ -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();

View File

@ -121,6 +121,13 @@
}
////////////////////////////////////////////////////////
-(void)setCursorGrabbed:(BOOL)grabbed
{
[m_oglView setCursorGrabbed:grabbed];
}
////////////////////////////////////////////////////////////
-(NSPoint)position
{

View File

@ -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 <WindowImplDelegateProtocol, NSWindowDelegate>
{
@ -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
}
////////////////////////////////////////////////////////////

View File

@ -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];
}

View File

@ -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);

View File

@ -489,7 +489,7 @@ void WindowImplCocoa::setMouseCursorVisible(bool visible)
////////////////////////////////////////////////////////////
void WindowImplCocoa::setMouseCursorGrabbed(bool grabbed)
{
[m_delegate setCursorGrabbed:grabbed];
}

View File

@ -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