mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-30 04:50:08 +00:00
c226be612f
This allows the Macintosh and ST to fade out volume and settings even without having captured the mouse.
387 lines
11 KiB
Objective-C
387 lines
11 KiB
Objective-C
//
|
|
// CSScanTargetView
|
|
// CLK
|
|
//
|
|
// Created by Thomas Harte on 16/07/2015.
|
|
// Copyright 2015 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#import "CSScanTargetView.h"
|
|
#import "CSApplication.h"
|
|
#import "CSScanTarget.h"
|
|
@import CoreVideo;
|
|
@import GLKit;
|
|
|
|
#include <stdatomic.h>
|
|
|
|
static const NSTimeInterval standardMouseHideInterval = 3.0;
|
|
static const NSTimeInterval quickMouseHideInterval = 0.1;
|
|
|
|
@interface CSScanTargetView () <NSDraggingDestination, CSApplicationEventDelegate>
|
|
@end
|
|
|
|
@implementation CSScanTargetView {
|
|
CVDisplayLinkRef _displayLink;
|
|
NSNumber *_currentScreenNumber;
|
|
|
|
NSTrackingArea *_mouseTrackingArea;
|
|
NSTimer *_mouseHideTimer;
|
|
BOOL _mouseIsCaptured;
|
|
|
|
atomic_int _isDrawingFlag;
|
|
BOOL _isInvalid;
|
|
|
|
CSScanTarget *_scanTarget;
|
|
}
|
|
|
|
- (void)setupDisplayLink {
|
|
// Kill the existing link if there is one.
|
|
if(_displayLink) {
|
|
[self stopDisplayLink];
|
|
CVDisplayLinkRelease(_displayLink);
|
|
}
|
|
|
|
// Create a display link for the display the window is currently on.
|
|
NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"];
|
|
_currentScreenNumber = screenNumber;
|
|
CVDisplayLinkCreateWithCGDisplay(screenNumber.unsignedIntValue, &_displayLink);
|
|
|
|
// Set the renderer output callback function.
|
|
CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
|
|
|
|
// Activate the display link.
|
|
CVDisplayLinkStart(_displayLink);
|
|
}
|
|
|
|
static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, __unused CVOptionFlags flagsIn, __unused CVOptionFlags *flagsOut, void *displayLinkContext) {
|
|
CSScanTargetView *const view = (__bridge CSScanTargetView *)displayLinkContext;
|
|
|
|
// Schedule an opportunity to check that the display link is still linked to the correct display.
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[view checkDisplayLink];
|
|
});
|
|
|
|
// Ensure _isDrawingFlag has value 1 when drawing, 0 otherwise.
|
|
atomic_store(&view->_isDrawingFlag, 1);
|
|
|
|
[view.displayLinkDelegate scanTargetViewDisplayLinkDidFire:view now:now outputTime:outputTime];
|
|
/*
|
|
Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink.
|
|
Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback,
|
|
it doesn't wait for completion of any current calls. So I've set up a usleep for one callback's duration,
|
|
so code in here gets one callback's duration to access the display link.
|
|
|
|
In practice, it should do so only upon entry, and before calling into the view. The view promises not to
|
|
access the display link itself as part of -drawAtTime:frequency:.
|
|
*/
|
|
|
|
atomic_store(&view->_isDrawingFlag, 0);
|
|
|
|
return kCVReturnSuccess;
|
|
}
|
|
|
|
- (void)checkDisplayLink {
|
|
// Don't do anything if this view has already been invalidated.
|
|
if(_isInvalid) {
|
|
return;
|
|
}
|
|
|
|
// Test now whether the screen this view is on has changed since last time it was checked.
|
|
// There's likely a callback available for this, on NSWindow if nowhere else, or an NSNotification,
|
|
// but since this method is going to be called repeatedly anyway, and the test is cheap, polling
|
|
// feels fine.
|
|
NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"];
|
|
if(![_currentScreenNumber isEqual:screenNumber]) {
|
|
// Also switch display links, to make sure synchronisation is with the display
|
|
// the window is actually on, and at its rate.
|
|
[self setupDisplayLink];
|
|
}
|
|
}
|
|
|
|
- (void)invalidate {
|
|
_isInvalid = YES;
|
|
[self stopDisplayLink];
|
|
}
|
|
|
|
- (void)stopDisplayLink {
|
|
const double duration = CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
|
|
CVDisplayLinkStop(_displayLink);
|
|
|
|
// This is a workaround; CVDisplayLinkStop does not wait for any existing call to the
|
|
// display-link callback to stop. Furthermore there's a race condition between a callback
|
|
// and any ability by me to set state.
|
|
//
|
|
// So: wait for a whole display link tick to avoid the second race condition. Then spin
|
|
// on an atomic flag.
|
|
usleep((useconds_t)ceil(duration * 1000000.0));
|
|
|
|
// Spin until _isDrawingFlag is 0 (and leave it as 0).
|
|
int expected_value = 0;
|
|
while(!atomic_compare_exchange_weak(&_isDrawingFlag, &expected_value, 0)) {
|
|
expected_value = 0;
|
|
}
|
|
}
|
|
|
|
- (void)dealloc {
|
|
// Stop and release the display link
|
|
CVDisplayLinkStop(_displayLink);
|
|
CVDisplayLinkRelease(_displayLink);
|
|
}
|
|
|
|
- (CSScanTarget *)scanTarget {
|
|
return _scanTarget;
|
|
}
|
|
|
|
- (void)willChangeScanTargetOwner {
|
|
[_scanTarget willChangeOwner];
|
|
}
|
|
|
|
- (void)updateBacking {
|
|
[_scanTarget updateFrameBuffer];
|
|
}
|
|
|
|
- (void)awakeFromNib {
|
|
// Use the preferred device if available.
|
|
if(@available(macOS 10.15, *)) {
|
|
self.device = self.preferredDevice;
|
|
} else {
|
|
self.device = MTLCreateSystemDefaultDevice();
|
|
}
|
|
|
|
// Configure for explicit drawing.
|
|
self.paused = YES;
|
|
self.enableSetNeedsDisplay = NO;
|
|
|
|
// Create the scan target.
|
|
_scanTarget = [[CSScanTarget alloc] initWithView:self];
|
|
self.delegate = _scanTarget;
|
|
|
|
// Register to receive dragged and dropped file URLs.
|
|
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
|
|
|
|
// Setup the [initial] display link.
|
|
[self setupDisplayLink];
|
|
}
|
|
|
|
#pragma mark - NSResponder
|
|
|
|
- (BOOL)acceptsFirstResponder {
|
|
return YES;
|
|
}
|
|
|
|
- (void)keyDown:(NSEvent *)event {
|
|
[self.responderDelegate keyDown:event];
|
|
}
|
|
|
|
- (void)keyUp:(NSEvent *)event {
|
|
[self.responderDelegate keyUp:event];
|
|
}
|
|
|
|
- (void)flagsChanged:(NSEvent *)event {
|
|
// Release the mouse upon a control + command.
|
|
if(_mouseIsCaptured &&
|
|
event.modifierFlags & NSEventModifierFlagControl &&
|
|
event.modifierFlags & NSEventModifierFlagCommand) {
|
|
[self releaseMouse];
|
|
}
|
|
|
|
[self.responderDelegate flagsChanged:event];
|
|
}
|
|
|
|
- (BOOL)application:(nonnull CSApplication *)application shouldSendEvent:(nonnull NSEvent *)event {
|
|
switch(event.type) {
|
|
default: return YES;
|
|
case NSEventTypeKeyUp: [self keyUp:event]; return NO;
|
|
case NSEventTypeKeyDown: [self keyDown:event]; return NO;
|
|
case NSEventTypeFlagsChanged: [self flagsChanged:event]; return NO;
|
|
}
|
|
}
|
|
|
|
- (void)paste:(id)sender {
|
|
[self.responderDelegate paste:sender];
|
|
}
|
|
|
|
- (NSBitmapImageRep *)imageRepresentation {
|
|
return self.scanTarget.imageRepresentation;
|
|
}
|
|
|
|
#pragma mark - NSDraggingDestination
|
|
|
|
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
|
|
for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems]) {
|
|
NSURL *URL = [NSURL URLWithString:[item stringForType:(__bridge NSString *)kUTTypeFileURL]];
|
|
[self.responderDelegate scanTargetView:self didReceiveFileAtURL:URL];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender {
|
|
return NSDragOperationLink;
|
|
}
|
|
|
|
#pragma mark - Mouse hiding
|
|
|
|
- (void)setShouldCaptureMouse:(BOOL)shouldCaptureMouse {
|
|
_shouldCaptureMouse = shouldCaptureMouse;
|
|
}
|
|
|
|
- (void)updateTrackingAreas {
|
|
[super updateTrackingAreas];
|
|
|
|
if(_mouseTrackingArea) {
|
|
[self removeTrackingArea:_mouseTrackingArea];
|
|
}
|
|
_mouseTrackingArea =
|
|
[[NSTrackingArea alloc]
|
|
initWithRect:self.bounds
|
|
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveWhenFirstResponder
|
|
owner:self
|
|
userInfo:nil];
|
|
[self addTrackingArea:_mouseTrackingArea];
|
|
}
|
|
|
|
- (void)scheduleMouseHideAfter:(NSTimeInterval)interval {
|
|
[_mouseHideTimer invalidate];
|
|
|
|
_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:interval repeats:NO block:^(__unused NSTimer * _Nonnull timer) {
|
|
// Don't actually hide the mouse if this is a mouse-capture machine; that makes
|
|
// it fairly confusing as to current application state.
|
|
if(!self.shouldCaptureMouse) {
|
|
[NSCursor setHiddenUntilMouseMoves:YES];
|
|
}
|
|
[self.responderDelegate scanTargetViewWouldHideOSMouseCursor:self];
|
|
}];
|
|
}
|
|
|
|
- (void)mouseEntered:(NSEvent *)event {
|
|
[super mouseEntered:event];
|
|
|
|
[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self];
|
|
[self scheduleMouseHideAfter:standardMouseHideInterval];
|
|
}
|
|
|
|
- (void)mouseExited:(NSEvent *)event {
|
|
[super mouseExited:event];
|
|
|
|
// Schedule a really short mouse-hiding interval.
|
|
[self scheduleMouseHideAfter:quickMouseHideInterval];
|
|
}
|
|
|
|
- (void)releaseMouse {
|
|
if(_mouseIsCaptured) {
|
|
_mouseIsCaptured = NO;
|
|
CGAssociateMouseAndMouseCursorPosition(true);
|
|
[NSCursor unhide];
|
|
[self.responderDelegate scanTargetViewDidReleaseMouse:self];
|
|
[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self];
|
|
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = nil;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Mouse motion
|
|
|
|
- (void)applyMouseMotion:(NSEvent *)event {
|
|
if(!_mouseIsCaptured) {
|
|
// Mouse capture is off, so don't play games with the cursor, just schedule it to
|
|
// hide in the near future.
|
|
[self scheduleMouseHideAfter:standardMouseHideInterval];
|
|
[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self];
|
|
} else {
|
|
// 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) {
|
|
if(!_mouseIsCaptured) {
|
|
_mouseIsCaptured = YES;
|
|
[NSCursor hide];
|
|
CGAssociateMouseAndMouseCursorPosition(false);
|
|
[self.responderDelegate scanTargetViewWouldHideOSMouseCursor:self];
|
|
[self.responderDelegate scanTargetViewDidCaptureMouse:self];
|
|
if(self.shouldUsurpCommand) {
|
|
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = self;
|
|
}
|
|
|
|
// Don't report the first click to the delegate; treat that as merely
|
|
// an invitation to capture the cursor.
|
|
return;
|
|
}
|
|
|
|
[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
|