Files
VoidLink/VoidLink/Input/RelativeTouchHandler.m
T

302 lines
14 KiB
Objective-C

//
// RelativeTouchHandler.m
// Moonlight
//
// Completely refactored by True砖家 on 2024.9.13
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
//
#import "RelativeTouchHandler.h"
#import "DataManager.h"
#include <Limelight.h>
static const float QUICK_TAP_TIME_INTERVAL = 0.2;
@implementation RelativeTouchHandler {
TemporarySettings* currentSettings;
CGPoint latestMousePointerLocation, initialMousePointerLocation;
CGPoint twoFingerTouchLocation;
NSTimeInterval mousePointerTimestamp;
CGRect streamViewBounds;
BOOL firstTouchMoved;
BOOL mousePointerMoved;
BOOL quickTapDetected;
// upper screen edge check
bool touchPointSpawnedAtUpperScreenEdge;
CGFloat slideGestureVerticalThreshold;
CGFloat screenWidthWithThreshold;
CGFloat _edgeTolerance;
UITouch* touchLockedForMouseMove;
CADisplayLink *displayLink;
#if TARGET_OS_TV
UIGestureRecognizer* remotePressRecognizer;
UIGestureRecognizer* remoteLongPressRecognizer;
#endif
StreamView* streamView;
}
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
self = [self init];
self->streamView = view;
self->streamViewBounds = view.bounds;
self->currentSettings = settings;
// replace righclick recoginizing with my CustomTapGestureRecognizer for better experience, higher recoginizing rate.
_mouseRightClickTapRecognizer = [[CustomTapGestureRecognizer alloc] initWithTarget:self action:@selector(mouseRightClick)];
_mouseRightClickTapRecognizer.numberOfTouchesRequired = 2;
_mouseRightClickTapRecognizer.tapDownTimeThreshold = RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S; // tap down time in seconds.
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
_mouseRightClickTapRecognizer.touchCapturingView = streamView;
firstTouchMoved = false;
mousePointerMoved = false;
mousePointerTimestamp = 0;
// upper screen check
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
self->touchPointSpawnedAtUpperScreenEdge = false;
// self->displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
// [self->displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
#if TARGET_OS_TV
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)];
remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
[self->view addGestureRecognizer:remotePressRecognizer];
[self->view addGestureRecognizer:remoteLongPressRecognizer];
#endif
return self;
}
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
for(UITouch* touch in touches){
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) return true;
}
return false;
}
- (void)mouseRightClick {
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC));
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
Log(LOG_D, @"Sending right mouse button press");
// Wait 100 ms to simulate a real button press
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
});
});
}
/*
- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
// Movements of greater than 1 point are considered confirmed
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 1;
}*/
- (BOOL)isAdjacentTouches:(CGPoint)currentPoint from:(CGPoint)originalPoint {
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= 300;
}
- (BOOL)isAdjacentPoints:(CGPoint)currentPoint from:(CGPoint)originalPoint tolerance:(CGFloat)tolerance {
bool isAdjacent = hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= hypot(tolerance, tolerance);
return isAdjacent;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
firstTouchMoved = false;
//check if touch point is spawned on the left or right upper half screen edges, this is the highest priority
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
self->touchPointSpawnedAtUpperScreenEdge = true;
return;
}
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
UITouch* candidateTouch = nil;
// the onscreen controllers are implmented by CALayer, which can not intercept UITouch event, touch will penetrate to the streamView level and captured in the touches callback of this touchHandler class.
// the onscreen button are UIViews, they intercept UITouch events, so we don't need to worry about them.
for(UITouch* touch in touches){
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
continue;
}
else candidateTouch = touch;
}
// quick double tap detection for dragging. simulates a real notebook computer touchpad
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
if(tapInterval < QUICK_TAP_TIME_INTERVAL && [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation] ) {
// NSLog(@"quick click detected");
quickTapDetected = true;
NSLog(@"quick Tap Detected");
// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); // do not press down mouse button here, or it wiil easily turn to double click on remote PC
}
// use [event allTouches] to check if touchLockedForMouseMove is captured, if already captured, don't update touchLockedForMouseMove
if(candidateTouch != nil && ![[event allTouches] containsObject:touchLockedForMouseMove]){
touchLockedForMouseMove = candidateTouch;
// NSLog(@"Candidate touch for mouse movement locked");
mousePointerTimestamp = CACurrentMediaTime();
initialMousePointerLocation = latestMousePointerLocation = [touchLockedForMouseMove locationInView:streamView];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(@"%f, touchesMoved callback, is scrolling: %d, touches count: %d", CACurrentMediaTime(), isInMouseWheelScrollingMode, (uint32_t)[touches count]);
/*
if(isInMouseWheelScrollingMode && [[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:touches]){
NSSet* twoTouches = [event allTouches];
CGPoint firstLocation = [[[twoTouches allObjects] objectAtIndex:0] locationInView:streamView];
CGPoint secondLocation = [[[twoTouches allObjects] objectAtIndex:1] locationInView:streamView];
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
if ((CACurrentMediaTime() - _mouseRightClickTapRecognizer.gestureCapturedTime > RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S) && twoFingerTouchLocation.y != avgLocation.y) { //prevent sending scrollevent while right click gesture is being recognized. The time threshold is only 150ms, resulting in a barely noticeable delay before the scroll event is activated.
// and we must exclude onscreen button taps & on-screen controller taps
LiSendHighResScrollEvent((avgLocation.y - twoFingerTouchLocation.y) * 10);
}
twoFingerTouchLocation = avgLocation;
return;
}
*/
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
uint8_t currentTouchesCount = currentTouches.count;
if(![self isOnScreenControllerBeingPressed:currentTouches]) [TouchPadGestureHandler handleGestureIn:streamView with:event];
if(currentTouchesCount>=2) return;
if([touches containsObject:touchLockedForMouseMove]){
CGPoint currentLocation = [touchLockedForMouseMove locationInView:streamView];
bool isAdjacentPoints = [self isAdjacentPoints:initialMousePointerLocation from:currentLocation tolerance:currentSettings.singleTapSensitivity.doubleValue];
if(!mousePointerMoved && !isAdjacentPoints){
mousePointerMoved = true;
}
[self sendMouseMoveEvent:currentLocation];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if([touches containsObject:touchLockedForMouseMove]){
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
if(!mousePointerMoved && !self->quickTapDetected) [self sendLongMouseLeftButtonClickEvent];
// dealing with a second quick tap following the first tap:
if(self->quickTapDetected){
// we're in at least the second tap release of the very short time interval after the first tap.
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); // must release the button anyway, because the button is likely being held down since the long click turned into a dragging event.
if(!mousePointerMoved) [self sendShortMouseLeftButtonClickEvent]; // if it is a quick tap and the pointer was not moved, we must send another click to simulate double click.
self->quickTapDetected = false; // reset flag
}
touchLockedForMouseMove = nil;
mousePointerMoved = false;
}
for(UITouch* touch in touches){
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
}
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
if([UITouchUtil touchesIn:streamView from:event].count == [touches count]){
touchLockedForMouseMove = nil;
mousePointerMoved = false; // need to reset this anyway
}
touchPointSpawnedAtUpperScreenEdge = false;
}
- (void)sendMouseMoveEvent:(CGPoint)currentLocation{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
int deltaX = ([UITouchUtil getDeltaXFrom:self->touchLockedForMouseMove in:self->streamView]) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
int deltaY = ([UITouchUtil getDeltaYFrom:self->touchLockedForMouseMove in:self->streamView]) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
if(self->firstTouchMoved) LiSendMouseMoveEvent(deltaX, deltaY);
bool isAdjacentPoints = [self isAdjacentPoints:self->initialMousePointerLocation from:currentLocation tolerance:self->currentSettings.relativeTouchSlideThreshold.floatValue];
if (!self->firstTouchMoved && !isAdjacentPoints) {
self->latestMousePointerLocation = currentLocation;
self->firstTouchMoved = true;
}
});
}
// this will turn into a dragging anytime...
- (void)sendLongMouseLeftButtonClickEvent{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
// if (!self->isDragging){
Log(LOG_D, @"Sending left mouse button press");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Wait 100 ms to simulate a real button press
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(QUICK_TAP_TIME_INTERVAL * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if(!self->quickTapDetected){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
// else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
});
// do not release the button if we're still dragging, this will prevent the dragging from being interrupted.
});
}
- (void)sendShortMouseLeftButtonClickEvent{
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
});
});
}
#if TARGET_OS_TV
- (void)remoteButtonPressed:(id)sender {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
Log(LOG_D, @"Sending left mouse button press");
// Mark this as touchMoved to avoid a duplicate press on touch up
self->touchMoved = true;
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
});
}
- (void)remoteButtonLongPressed:(id)sender {
Log(LOG_D, @"Holding left mouse button");
isDragging = true;
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
#endif
@end