Files
VoidLink/VoidLink/Input/AbsoluteTouchHandler.m
T
TrueZhuanjia 5750b99af3 - fixed: memory leak and UI stuck caused by MenuSenctionView
- feat: memorize content offset for settings menu
- optimize gesture passthrough for relative touch & absolute touch
- feat: passthroughGesturesSwitch, ctrlDownForPinchSwitch, leftClickDelaySlider, touchModeSelector2 in experimental section
2025-11-07 17:34:19 +08:00

288 lines
11 KiB
Objective-C

//
// AbsoluteTouchHandler.m
// Moonlight
//
// Created by Cameron Gutman on 11/1/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved
//
// Modified by True砖家 since 2024.6.1
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved
//
#import "AbsoluteTouchHandler.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
// How long the fingers must be stationary to start a right click
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
// How far the finger can move before it cancels a right click
#define LONG_PRESS_ACTIVATION_DELTA 0.02f
// How long the double tap deadzone stays in effect between touch up and touch down
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
// How far the finger can move before it can override the double tap deadzone
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
@implementation AbsoluteTouchHandler {
StreamView* streamView;
bool multiTouchesDetected;
bool passthroughGestures;
NSTimer* longPressTimer;
UITouch* lastTouchDown;
CGPoint lastTouchDownLocation;
UITouch* lastTouchUp;
CGPoint lastTouchUpLocation;
UITouch* capturedTouch;
CGPoint touchBeganLocation;
CGPoint movingTouchLocation;
NSTimeInterval touchBeganTimeStamp;
NSTimeInterval leftClickTimeThreshold;
// upper screen edge check
bool touchPointSpawnedAtUpperScreenEdge;
CGFloat slideGestureVerticalThreshold;
CGFloat screenWidthWithThreshold;
CGFloat _edgeTolerance;
bool _delayMouseLeftClick;
NSTimeInterval leftClickDelay;
bool dragButtonDown;
UInt8 currentTouchesCount;
bool rightButtonClicked;
}
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
self = [self init];
self->streamView = view;
multiTouchesDetected = false;
passthroughGestures = settings.passthroughGestures;
_delayMouseLeftClick = settings.delayLeftClick;
// _delayMouseLeftClick = true;
dragButtonDown = false;
leftClickTimeThreshold = 0.1;
// upper screen check
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - _edgeTolerance;
self->touchPointSpawnedAtUpperScreenEdge = false;
_mouseButtonForCursorMove = BUTTON_LEFT;
leftClickDelay = ((CGFloat)settings.leftClickDelayMs.intValue)/1000;
return self;
}
- (void)onLongPressStart:(NSTimer*)timer {
// Raise the left click and start a right click
if(currentTouchesCount>=2) return;
if([self touchDidntMoveOnScreen:movingTouchLocation]){
if(_delayMouseLeftClick){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
if(_mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, _mouseButtonForCursorMove);
}
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC));
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
self->rightButtonClicked = true;
});
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
rightButtonClicked = false;
if([UITouchUtil touchesIn:streamView from:event].count>=2) multiTouchesDetected = true;
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < _edgeTolerance || initialPoint.x > screenWidthWithThreshold)) {
self->touchPointSpawnedAtUpperScreenEdge = true;
return; // we're done here. this touch event will not be sent to the remote PC.
}
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.
// Ignore touch down events with more than one finger
/*
if ([[event allTouches] count] > 1) {
return;
}*/
capturedTouch = [touches anyObject];
CGPoint touchLocation = [capturedTouch locationInView:streamView];
touchBeganTimeStamp = capturedTouch.timestamp;
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
if (capturedTouch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchUpLocation.x / streamView.bounds.size.width), 2) +
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchUpLocation.y / streamView.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
[streamView updateCursorLocation:touchLocation isMouse:NO];
}
// Press the left button down
if(!_delayMouseLeftClick){
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); //deprecated
}
// Start the long press timer
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
target:self
selector:@selector(onLongPressStart:)
userInfo:nil
repeats:NO];
lastTouchDown = capturedTouch;
lastTouchDownLocation = touchLocation;
movingTouchLocation = touchLocation;
touchBeganLocation = touchLocation;
currentTouchesCount = [UITouchUtil touchesIn:streamView from:event].count;
}
- (void)pauseLeftButtonDrag{
if(dragButtonDown){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
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, self->_mouseButtonForCursorMove);
});
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
// Ignore touch move events with more than one finger
/*
if ([[event allTouches] count] > 1) {
return;
}*/
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
currentTouchesCount = currentTouches.count;
if(currentTouchesCount == 2){
if(_mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, _mouseButtonForCursorMove);
if(passthroughGestures) [TouchPadGestureHandler handleGestureIn:streamView with:event];
}
if(multiTouchesDetected) return;
if(![currentTouches containsObject:capturedTouch]) return;
movingTouchLocation = [capturedTouch locationInView:streamView];
if (sqrt(pow((movingTouchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
pow((movingTouchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
// Moved too far since touch down. Cancel the long press timer.
[longPressTimer invalidate];
longPressTimer = nil;
NSTimeInterval dragDelay = _mouseButtonForCursorMove == BUTTON_LEFT ? leftClickTimeThreshold : 0;
if(_delayMouseLeftClick && (CACurrentMediaTime()-touchBeganTimeStamp>dragDelay) && !dragButtonDown){
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, _mouseButtonForCursorMove);
dragButtonDown = true;
}
}
if(!rightButtonClicked) [streamView updateCursorLocation:movingTouchLocation isMouse:NO];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
if(multiTouchesDetected) {
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
return;
};
if([UITouchUtil touchesIn:streamView from:event].count == touches.count) multiTouchesDetected = false;
// Only fire this logic if all touches have ended
if ([touches containsObject:capturedTouch]) {
// Cancel the long press timer
[longPressTimer invalidate];
longPressTimer = nil;
// Remember this last touch for touch-down deadzoning
CGPoint touchEndLocation = [capturedTouch locationInView:streamView];
if(_delayMouseLeftClick){
if(CACurrentMediaTime()-touchBeganTimeStamp<leftClickTimeThreshold) {
if(CACurrentMediaTime()-lastTouchUp.timestamp<0.15
&& ![self isAdjacentPoints:touchEndLocation from:lastTouchUpLocation tolerance:30]) [streamView updateCursorLocation:touchEndLocation isMouse:NO];
if([self touchDidntMoveOnScreen:touchEndLocation] && !rightButtonClicked) [self sendShortMouseLeftButtonClickEvent];
}
else if(!rightButtonClicked){
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
if(_mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, _mouseButtonForCursorMove);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
}
else{
// Left button up on finger up
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
// Raise right button too in case we triggered a long press gesture
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
lastTouchUp = [touches anyObject];
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
dragButtonDown = false;
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
// Treat this as a normal touchesEnded event
[self touchesEnded:touches withEvent:event];
}
- (void)sendShortMouseLeftButtonClickEvent{
dispatch_time_t delayShort = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(leftClickDelay * NSEC_PER_SEC));
dispatch_time_t delayLong = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.03 * NSEC_PER_SEC));
dispatch_after(delayShort, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
dispatch_after(delayLong, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
});
});
}
- (bool)touchDidntMoveOnScreen:(CGPoint)touchLocation{
return [self isAdjacentPoints:touchLocation from:touchBeganLocation tolerance:3];
}
- (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;
}
@end