diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h index 0fd718a27..4bf010a1e 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h @@ -63,6 +63,29 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { */ - (void)paste:(nonnull id)sender; +@optional + +/*! + Supplies a mouse moved event to the delegate. This functions only if + shouldCaptureMouse is set to YES, in which case the view will ensure it captures + the mouse and returns only relative motion + (Cf. CGAssociateMouseAndMouseCursorPosition). It will also elide mouseDragged: + (and rightMouseDragged:, etc) and mouseMoved: events. +*/ +- (void)mouseMoved:(nonnull NSEvent *)event; + +/*! + Supplies a mouse button down event. This elides mouseDown, rightMouseDown and otherMouseDown. + @c shouldCaptureMouse must be set to @c YES to receive these events. +*/ +- (void)mouseDown:(nonnull NSEvent *)event; + +/*! + Supplies a mouse button up event. This elides mouseUp, rightMouseUp and otherMouseUp. + @c shouldCaptureMouse must be set to @c YES to receive these events. +*/ +- (void)mouseUp:(nonnull NSEvent *)event; + @end /*! @@ -74,6 +97,8 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { @property (atomic, weak, nullable) id delegate; @property (nonatomic, weak, nullable) id responderDelegate; +@property (nonatomic, assign) BOOL shouldCaptureMouse; + /*! Ends the timer tracking time; should be called prior to giving up the last owning reference to ensure that any retain cycles implied by the timer are resolved. @@ -89,4 +114,9 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { */ - (void)performWithGLContext:(nonnull dispatch_block_t)action; +/*! + Instructs that the mouse cursor, if currently captured, should be released. +*/ +- (void)releaseMouse; + @end diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index 158e7c688..c65c813da 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -19,6 +19,7 @@ NSTrackingArea *_mouseTrackingArea; NSTimer *_mouseHideTimer; + BOOL _mouseIsCaptured; } - (void)prepareOpenGL { @@ -148,6 +149,13 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt - (void)flagsChanged:(NSEvent *)theEvent { [self.responderDelegate flagsChanged:theEvent]; + + // Release the mouse upon a control + command. + if(_mouseIsCaptured && + theEvent.modifierFlags & NSEventModifierFlagControl && + theEvent.modifierFlags & NSEventModifierFlagCommand) { + [self releaseMouse]; + } } - (void)paste:(id)sender { @@ -170,6 +178,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt #pragma mark - Mouse hiding +- (void)setShouldCaptureMouse:(BOOL)shouldCaptureMouse { + _shouldCaptureMouse = shouldCaptureMouse; +} + - (void)updateTrackingAreas { [super updateTrackingAreas]; @@ -185,9 +197,14 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self addTrackingArea:_mouseTrackingArea]; } -- (void)mouseMoved:(NSEvent *)event { - [super mouseMoved:event]; - [self scheduleMouseHide]; +- (void)scheduleMouseHide { + if(!self.shouldCaptureMouse) { + [_mouseHideTimer invalidate]; + + _mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) { + [NSCursor setHiddenUntilMouseMoves:YES]; + }]; + } } - (void)mouseEntered:(NSEvent *)event { @@ -195,18 +212,111 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self scheduleMouseHide]; } -- (void)scheduleMouseHide { - [_mouseHideTimer invalidate]; - _mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) { - [NSCursor setHiddenUntilMouseMoves:YES]; - }]; -} - - (void)mouseExited:(NSEvent *)event { [super mouseExited:event]; - [_mouseHideTimer invalidate]; _mouseHideTimer = nil; } +- (void)releaseMouse { + _mouseIsCaptured = NO; + CGAssociateMouseAndMouseCursorPosition(true); + [NSCursor unhide]; +} + +#pragma mark - Mouse motion + +- (void)applyMouseMotion:(NSEvent *)event { + if(!self.shouldCaptureMouse) { + // Mouse capture is off, so don't play games with the cursor, just schedule it to + // hide in the near future. + [self scheduleMouseHide]; + } else { + if(_mouseIsCaptured) { + // Mouse capture is on, so move the cursor back to the middle of the window, and + // forward the deltas to the listener. + // + // TODO: should I really need to invert the y coordinate myself? It suggests I + // might have an error in mapping here. + const NSPoint windowCentre = [self convertPoint:CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5) toView:nil]; + const NSPoint screenCentre = [self.window convertPointToScreen:windowCentre]; + const CGRect screenFrame = self.window.screen.frame; + CGWarpMouseCursorPosition(NSMakePoint( + screenFrame.origin.x + screenCentre.x, + screenFrame.origin.y + screenFrame.size.height - screenCentre.y + )); + + [self.responderDelegate mouseMoved:event]; + } + } +} + +- (void)mouseDragged:(NSEvent *)event { + [self applyMouseMotion:event]; + [super mouseDragged:event]; +} + +- (void)rightMouseDragged:(NSEvent *)event { + [self applyMouseMotion:event]; + [super rightMouseDragged:event]; +} + +- (void)otherMouseDragged:(NSEvent *)event { + [self applyMouseMotion:event]; + [super otherMouseDragged:event]; +} + +- (void)mouseMoved:(NSEvent *)event { + [self applyMouseMotion:event]; + [super mouseMoved:event]; +} + +#pragma mark - Mouse buttons + +- (void)applyButtonDown:(NSEvent *)event { + if(self.shouldCaptureMouse) { + _mouseIsCaptured = YES; + [NSCursor hide]; + CGAssociateMouseAndMouseCursorPosition(false); + + [self.responderDelegate mouseDown:event]; + } +} + +- (void)applyButtonUp:(NSEvent *)event { + if(self.shouldCaptureMouse) { + [self.responderDelegate mouseUp:event]; + } +} + +- (void)mouseDown:(NSEvent *)event { + [self applyButtonDown:event]; + [super mouseDown:event]; +} + +- (void)rightMouseDown:(NSEvent *)event { + [self applyButtonDown:event]; + [super rightMouseDown:event]; +} + +- (void)otherMouseDown:(NSEvent *)event { + [self applyButtonDown:event]; + [super otherMouseDown:event]; +} + +- (void)mouseUp:(NSEvent *)event { + [self applyButtonUp:event]; + [super mouseUp:event]; +} + +- (void)rightMouseUp:(NSEvent *)event { + [self applyButtonUp:event]; + [super rightMouseUp:event]; +} + +- (void)otherMouseUp:(NSEvent *)event { + [self applyButtonUp:event]; + [super otherMouseUp:event]; +} + @end