- enhanced relative touch (touchpad mode & "mousePad" widget) pointer accuracy

- touchpad gesture simulation for touchPad & single point mode
This commit is contained in:
TrueZhuanjia
2025-11-06 11:52:13 +08:00
parent a23a5491ae
commit e933c5de60
15 changed files with 250 additions and 172 deletions
+8
View File
@@ -15,12 +15,14 @@
182760FD2E83E68B00EC1C5C /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 182760FC2E83E68B00EC1C5C /* Collections */; };
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */; };
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 18341A032DEB662C0094E5F2 /* FixedTintImageView.m */; };
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */; };
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
18729EDE2E8BD68800101194 /* UUIDHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18729EDD2E8BD68800101194 /* UUIDHelper.swift */; };
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
188973D32E386A670025E48C /* widgetTemplatesIPhone.bin in Resources */ = {isa = PBXBuildFile; fileRef = 188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */; };
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; };
189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; };
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; };
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
@@ -167,6 +169,7 @@
1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = "<group>"; };
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FixedTintImageView.m; sourceTree = "<group>"; };
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITouchUtil.swift; sourceTree = "<group>"; };
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
18729EDD2E8BD68800101194 /* UUIDHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDHelper.swift; sourceTree = "<group>"; };
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
@@ -174,6 +177,7 @@
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
1888C1912E24C33100108730 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
188973D22E386A670025E48C /* widgetTemplatesIPhone.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = widgetTemplatesIPhone.bin; path = PresetConfigs/widgetTemplatesIPhone.bin; sourceTree = "<group>"; };
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPadGestureHandler.swift; sourceTree = "<group>"; };
189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = "<group>"; };
189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = "<group>"; };
18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = "<group>"; };
@@ -707,6 +711,8 @@
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */,
18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */,
1850C8292E667E4000845308 /* MicHandler.swift */,
18E4BCE92E891895000A8657 /* MotionHandler.swift */,
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
@@ -1011,6 +1017,7 @@
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */,
97E20DDF2E2164A9000D5DCA /* Frame.m in Sources */,
97E20DE02E2164A9000D5DCA /* FrameQueue.m in Sources */,
18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */,
97E20DE12E2164A9000D5DCA /* FloatBuffer.m in Sources */,
97E20DE22E2164A9000D5DCA /* PaddedLabel.m in Sources */,
97E20DE32E2164A9000D5DCA /* BandwidthTracker.m in Sources */,
@@ -1100,6 +1107,7 @@
84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */,
84351B5C2C29ADBE00A77989 /* ProfileTableViewCell.m in Sources */,
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */,
18416EA42EBB3DCE00AE0124 /* UITouchUtil.swift in Sources */,
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
84351B2D2C29AD6100A77989 /* ToolBarContainerView.m in Sources */,
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */,
+22 -3
View File
@@ -10,6 +10,7 @@
//
#import "AbsoluteTouchHandler.h"
#import "VoidLink-Swift.h"
#include <Limelight.h>
@@ -50,6 +51,7 @@
bool _delayMouseLeftClick;
bool dragButtonDown;
UInt8 currentTouchesCount;
bool rightButtonClicked;
}
@@ -62,7 +64,7 @@
_delayMouseLeftClick = true; // deprecate legacy absolute touch
dragButtonDown = false;
leftClickTimeThreshold = 0.15;
leftClickTimeThreshold = 0.1;
// upper screen check
_edgeTolerance = settings.edgeSlidingSensitivity.floatValue;
@@ -77,6 +79,8 @@
- (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);
@@ -137,6 +141,7 @@
lastTouchDownLocation = touchLocation;
movingTouchLocation = touchLocation;
touchBeganLocation = touchLocation;
currentTouchesCount = [UITouchUtil touchesIn:streamView from:event].count;
}
- (void)pauseLeftButtonDrag{
@@ -159,7 +164,17 @@
return;
}*/
if(![touches containsObject:capturedTouch]) return;
NSSet* currentTouches = [UITouchUtil touchesIn:streamView from:event];
currentTouchesCount = currentTouches.count;
if(currentTouchesCount == 2){
if(_mouseButtonForCursorMove!=BUTTON_LEFT) LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, _mouseButtonForCursorMove);
[TouchPadGestureHandler handleGestureIn:streamView with:event];
}
if(currentTouchesCount>=2) return;
if(![currentTouches containsObject:capturedTouch]) return;
movingTouchLocation = [capturedTouch locationInView:streamView];
@@ -169,7 +184,9 @@
[longPressTimer invalidate];
longPressTimer = nil;
if(_delayMouseLeftClick && (CACurrentMediaTime()-touchBeganTimeStamp>leftClickTimeThreshold) && !dragButtonDown){
NSTimeInterval dragDelay = _mouseButtonForCursorMove == BUTTON_LEFT ? leftClickTimeThreshold : 0;
if(_delayMouseLeftClick && (CACurrentMediaTime()-touchBeganTimeStamp>dragDelay) && !dragButtonDown){
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, _mouseButtonForCursorMove);
dragButtonDown = true;
}
@@ -212,6 +229,8 @@
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); */
}
if(TouchPadGestureHandler.ctrlDown) LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[@"CTRL"].shortValue,KEY_ACTION_UP,0);
lastTouchUp = [touches anyObject];
lastTouchUpLocation = [lastTouchUp locationInView:streamView];
+4 -2
View File
@@ -61,7 +61,9 @@ import UIKit
"M_X1" : BUTTON_X1,
"MX1" : BUTTON_X1,
"M_X2" : BUTTON_X2,
"MX2" : BUTTON_X2
"MX2" : BUTTON_X2,
"WHEELUP" : 0xFF,
"WHEELDOWN" : -0xFF,
]
@@ -127,7 +129,7 @@ import UIKit
// @objc public static let specialGameWidgets: [String] = ["YSRSV", "YSLT", "YSRT", "YSRB", "YSB", "YSRT2", "YSRB2", "YSB2", "YSEM", "YSML", "YSMR", "YSWASD"]
static let keyboardButtonMappings: [String: Int16] = [
@objc public static let keyboardButtonMappings: [String: Int16] = [
// Windows Key Codes
"NULL": 0xFF,
"CTRL": 0x11, // VK_CONTROL
+1 -1
View File
@@ -14,7 +14,7 @@
CGFloat lowestTouchPointYCoord;
}
@property (nonatomic, weak) UIView* touchCapturingView;
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
@property (nonatomic, assign) double tapDownTimeThreshold; // tap down threshold in seconds.
+11 -27
View File
@@ -39,11 +39,18 @@ static CGFloat screenWidthInPoints;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// Check if the number of touches and taps meets the required criteria
if ([[event allTouches] count] == _numberOfTouchesRequired) {
NSSet* allTouches = [event allTouches];
if (allTouches.count == _numberOfTouchesRequired) {
if([UITouchUtil touchesIn:_touchCapturingView from:event].count != allTouches.count){
_gestureCaptured = false;
self.state = UIGestureRecognizerStateFailed;
return;
}
_gestureCapturedTime = CACurrentMediaTime();
_gestureCaptured = true;
lowestTouchPointYCoord = 0;
for(UITouch *touch in [event allTouches]){
for(UITouch *touch in allTouches){
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
// NSLog(@"lowest point coord: %f, %f", lowestTouchPointYCoord, CACurrentMediaTime());
}
@@ -62,7 +69,7 @@ static CGFloat screenWidthInPoints;
}
self.state = UIGestureRecognizerStatePossible;
}
if ([[event allTouches] count] > _numberOfTouchesRequired) {
if (allTouches.count > _numberOfTouchesRequired) {
_gestureCaptured = false;
self.state = UIGestureRecognizerStateFailed;
}
@@ -79,8 +86,7 @@ static CGFloat screenWidthInPoints;
}
else if(_gestureCaptured
&& allTouchesCount == [touches count]
&& !_isOnScreenControllerBeingPressed
&& (![self isOnScreenWidgetViewBeingPressed] || _numberOfTouchesRequired ==1)){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
&& !_isOnScreenControllerBeingPressed){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
_gestureCaptured = false; //reset for next recognition
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
// lowestTouchPointYCoord = 0.0; //reset for next recognition
@@ -91,26 +97,4 @@ static CGFloat screenWidthInPoints;
if (allTouchesCount == [touches count]) _isOnScreenControllerBeingPressed = false; // need to reset this flag anyway, when all fingers are lefting
}
// this method will only be called when the recognizer is active & and the taps passes all the checks and is about to ge triggered.
- (bool)isOnScreenWidgetViewBeingPressed {
NSTimeInterval allFingersTapDownTime = CACurrentMediaTime() - _gestureCapturedTime; //RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S has been passed to this recognizer as _tapDownTimeShreshold, we'll decide how to deal with pressed flag of on-screen widget views based on tapDownTime
bool gotOneWidgetPressed = false;
// the tap gestures are all added to the streamFrameTopLayerView, where all the OnScreenWidgetViews are added. so we can iterate them in this way:
for(UIView* view in self.view.subviews){
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
if(gotOneWidgetPressed && allFingersTapDownTime <= _tapDownTimeThreshold){ // once we have just 1 button pressed already,& the tapDownTime has not exceeded the threshold, we'll reset pressed flag for the all the widgetViews.
widgetView.pressedFlagForTapGesture = false; //reset the flag for widgetView
continue;
}
if(widgetView.pressedFlagForTapGesture){
gotOneWidgetPressed = true; //got one button pressed
if(allFingersTapDownTime <= _tapDownTimeThreshold) widgetView.pressedFlagForTapGesture = false; // reset the flag for current widgetView if tapDowntime is still within the threshold, if not, leave the flag for mouse scroller gesture in relative touch mode.
}
}
}
return gotOneWidgetPressed;
}
@end
+5 -6
View File
@@ -213,7 +213,7 @@
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
[self handleTouchDown:touch]; //generate & populate pointerId
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
@@ -222,7 +222,7 @@
else{
for (UITouch* touch in touches){
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
[self handleTouchDown:touch]; //generate & populate pointerId
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
@@ -233,7 +233,7 @@
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_main_queue(), ^{
for (UITouch* touch in touches){
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue;
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
[[self getPointerObjFromDict:touch] doesNeedResetCoords];
@@ -244,9 +244,8 @@
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, moveEventIntervalNSec), dispatch_get_main_queue(), ^{
for (UITouch* touch in touches){
NSNumber* touchAddrObj = @((uintptr_t)touch);
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:touchAddrObj]){
[OnScreenControls.touchAddrsCapturedByOnScreenControls removeObject:touchAddrObj];
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
[OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch];
continue;
}
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
+1 -1
View File
@@ -110,7 +110,7 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
+ (NSMutableSet* )touchAddrsCapturedByOnScreenControls;
+ (NSMutableSet* )touchesCapturedByOnScreenControls;
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
- (void) clearRightStickTouchPadFlag;
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
+7 -7
View File
@@ -23,7 +23,7 @@
#define UPDATE_BUTTON(x, y) (buttonFlags = \
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
static NSMutableSet* touchAddrsCapturedByOnScreenControls;
static NSMutableSet* touchesCapturedByOnScreenControls;
static CGRect standardRoundButtonBounds;
static CGRect standardRectangleButtonBounds;
static CGRect standardStickBounds;
@@ -147,8 +147,8 @@ static float L2_Y;
static float L3_X;
static float L3_Y;
+ (NSMutableSet* )touchAddrsCapturedByOnScreenControls{
return touchAddrsCapturedByOnScreenControls;
+ (NSMutableSet* )touchesCapturedByOnScreenControls{
return touchesCapturedByOnScreenControls;
}
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY{
@@ -316,7 +316,7 @@ static float L3_Y;
_standardLeftRightButtonBounds = standardLeftRightButtonBounds = CGRectMake(0, 0, [UIImage imageNamed:@"LeftButton"].size.width, [UIImage imageNamed:@"LeftButton"].size.height);
_activeCustomOscButtonPositionDict = [[NSMutableDictionary alloc] init];
touchAddrsCapturedByOnScreenControls = [[NSMutableSet alloc] init];
touchesCapturedByOnScreenControls = [[NSMutableSet alloc] init];
return self;
}
@@ -1413,12 +1413,12 @@ static float L3_Y;
touchEventCapturedByOsc = true;
}
// additionally, populate the touchesCapturedByOSButton set, for native/relative touch handler to deal with.
if(touchEventCapturedByOsc) [touchAddrsCapturedByOnScreenControls addObject:@((uintptr_t)touch)];
if(touchEventCapturedByOsc) [touchesCapturedByOnScreenControls addObject:touch];
}
if (updated) {
[_controllerSupport updateFinished:_controller];
}
// NSLog(@"captured by OSB touches, OSC Class: %d", (uint32_t)[touchAddrsCapturedByOnScreenControls count]);
// NSLog(@"captured by OSB touches, OSC Class: %d", (uint32_t)[touchesCapturedByOnScreenControls count]);
bool oscTouched = updated || stickTouch;
@@ -1442,7 +1442,7 @@ static float L3_Y;
for (UITouch* touch in touches) {
// remove the touch obj from touchesCapturedByOnScreenButtons
// if([touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) [touchAddrsCapturedByOnScreenControls removeObject:@((uintptr_t)touch)];
// if([touchesCapturedByOnScreenControls containsObject:touch]) [touchesCapturedByOnScreenControls removeObject:touch)];
if (touch == _aTouch) {
[_controllerSupport clearButtonFlag:_controller flags:A_FLAG];
+63 -46
View File
@@ -60,7 +60,6 @@ import UIKit
@objc public var comboButtonStrings: [String] = []
private var comboKeyTimeIntervalMs: UInt32 = 0
@objc public var pressedFlagForTapGesture: Bool
@objc public var logicallyDown: Bool = false
@objc public var widthFactor: CGFloat = 1.0
@objc public var heightFactor: CGFloat = 1.0
@@ -287,7 +286,6 @@ import UIKit
self.shape = shape
self.label = UILabel()
// self.originalBackgroundColor = UIColor(white: 0.2, alpha: 0.7)
self.pressedFlagForTapGesture = false
// self.widthFactor = 1.0
// self.heightFactor = 1.0
// self.backgroundAlpha = 0.5
@@ -740,7 +738,7 @@ import UIKit
stickMarkerRelativeLocation = CGPointMake(touchBeganLocation.x, touchBeganLocation.y)
}
showStickBall(at: stickMarkerRelativeLocation)
getStickBallReady(at: stickMarkerRelativeLocation)
showCrossMark(at: stickMarkerRelativeLocation)
}
@@ -810,7 +808,7 @@ import UIKit
return stickBallLayer
}
private func showStickBall(at point: CGPoint) {
private func getStickBallReady(at point: CGPoint) {
// let path = UIBezierPath(arcCenter: center, radius: 8, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
CATransaction.begin()
@@ -820,7 +818,7 @@ import UIKit
// self.stickBallLayer.path = path.cgPath // Assign the circular path to the shape layer
// self.stickBallLayer.position = CGPointMake(CGRectGetMidX(self.crossMarkLayer.frame), CGRectGetMidY(self.crossMarkLayer.frame))
self.lrudIndicatorBall.position = point
self.lrudIndicatorBall.isHidden = false
self.lrudIndicatorBall.isHidden = true
CATransaction.commit()
}
@@ -831,14 +829,18 @@ import UIKit
CATransaction.setDisableActions(true)
self.lrudIndicatorBall.removeAllAnimations()
if !OnScreenWidgetView.editMode {
let realOffsetX = touchInputToStickBallCoord(input: offSetX*sensitivityFactorX)
let realOffsetY = touchInputToStickBallCoord(input: offSetY*sensitivityFactorY)
self.lrudIndicatorBall.position = CGPointMake(touchBeganLocation.x + realOffsetX, touchBeganLocation.y + realOffsetY - self.stickIndicatorOffset)
if self.firstTouchMoved {
if self.lrudIndicatorBall.isHidden {self.lrudIndicatorBall.isHidden = false}
let realOffsetX = touchInputToStickBallCoord(input: offSetX*sensitivityFactorX)
let realOffsetY = touchInputToStickBallCoord(input: offSetY*sensitivityFactorY)
self.lrudIndicatorBall.position = CGPointMake(touchBeganLocation.x + realOffsetX, touchBeganLocation.y + realOffsetY - self.stickIndicatorOffset)
}
/*
if fabs(realOffsetX) == stickBallMaxOffset || fabs(realOffsetY) == stickBallMaxOffset {handleStickBallReachingBorder()}
else{handleStickBallLeavingBorder()}*/
}
else{
if self.lrudIndicatorBall.isHidden {self.lrudIndicatorBall.isHidden = false}
self.lrudIndicatorBall.position = CGPointMake(CGRectGetMidX(self.crossMarkLayer.frame), CGRectGetMidY(self.crossMarkLayer.frame)-stickIndicatorOffset)
}
CATransaction.commit()
@@ -1096,7 +1098,7 @@ import UIKit
private func startTrackballMomentum() {
stopTrackballMomentum()
trackballDecelerationTimer = Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { [weak self] _ in
trackballDecelerationTimer = Timer.scheduledTimer(withTimeInterval: 1/Double(UIScreen.main.maximumFramesPerSecond), repeats: true) { [weak self] _ in
guard let self = self else { return }
LiSendMouseMoveEvent(
@@ -1356,7 +1358,11 @@ import UIKit
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings[comboString]!,Int8(KEY_ACTION_DOWN), 0)
}
if CommandManager.mouseButtonMappings.keys.contains(comboString), self.functionalButtonString != "ABSTCHDRAG" {
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), Int32(CommandManager.mouseButtonMappings[comboString]!))
let button = Int32(CommandManager.mouseButtonMappings[comboString]!)
if abs(button) == 0xFF {
LiSendScrollEvent(button>0 ? 3 : -3)
}
else {LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), button)}
}
if comboString != comboStrings.last {
usleep(self.comboKeyTimeIntervalMs*1000) // delay xxx ms
@@ -1388,7 +1394,7 @@ import UIKit
// Touch event handling
private func getAllSpawnedTouchesCount(with event: UIEvent?)->Int{
return event?.allTouches?.filter({ $0.view == self }).count ?? 0
return UITouchUtil.touches(in: self, from: event).count
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
@@ -1426,8 +1432,6 @@ import UIKit
self.twoTouchesDetected = true
}
self.pressedFlagForTapGesture = true
if !OnScreenWidgetView.editMode {
if self.widgetType == WidgetTypeEnum.touchPad && touches.count == 1{ // don't use event?.allTouches?.count here, it will counts all touches including the ones captured by other UIViews
switch self.touchPadString {
@@ -1667,46 +1671,52 @@ import UIKit
return distance <= threshold
}
private func updateTouchLocation (touch: UITouch) {
private func getVector (touch: UITouch) {
let currentTouchLocation: CGPoint = (touch.location(in: self))
if !self.mousePointerMoved, !self.isAdjacentPoints(currentTouchLocation, from: latestTouchLocation, tolerance: 2.0){
self.mousePointerMoved = true
}
if !firstTouchMoved, !self.isAdjacentPoints(currentTouchLocation, from: latestTouchLocation, tolerance: 0.5) {
self.deltaX = UITouchUtil.getDeltaX(from: touch, in: self)
self.deltaY = UITouchUtil.getDeltaY(from: touch, in: self)
self.offSetX = currentTouchLocation.x - self.touchBeganLocation.x
self.offSetY = currentTouchLocation.y - self.touchBeganLocation.y
}
private func updateTouchLocation(_ touches: Set<UITouch>){
let currentTouchLocation = touches.first!.location(in: self)
//if !firstTouchMoved, !self.isAdjacentPoints(currentTouchLocation, from: touchBeganLocation, tolerance: 0.5) {
if !firstTouchMoved, !self.isAdjacentPoints(currentTouchLocation, from: touchBeganLocation, tolerance: 5) {
// First move event
self.latestTouchLocation = currentTouchLocation
self.firstTouchMoved = true
}
self.deltaX = currentTouchLocation.x - self.latestTouchLocation.x
self.deltaY = currentTouchLocation.y - self.latestTouchLocation.y
self.offSetX = currentTouchLocation.x - self.touchBeganLocation.x
self.offSetY = currentTouchLocation.y - self.touchBeganLocation.y
self.latestTouchLocation = currentTouchLocation
}
private func handleTouchPadMoveEvent (_ touches: Set<UITouch>, with event: UIEvent?){
if touches.count == 1{ // don't use event.alltouches.count here, it will counts all touches
self.getVector(touch: touches.first!)
switch self.touchPadString{
case "MOUSEPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.updateTouchLocation(touch: touches.first!)
LiSendMouseMoveEvent(Int16(truncatingIfNeeded: Int(self.deltaX * 1.7 * self.sensitivityFactorX)), Int16(truncatingIfNeeded: Int(self.deltaY * 1.7 * self.sensitivityFactorY)))
if self.firstTouchMoved {LiSendMouseMoveEvent(Int16(self.deltaX * 1.7 * self.sensitivityFactorX), Int16(self.deltaY * 1.7 * self.sensitivityFactorY))}
self.updateTouchLocation(touches)
}
break
case "TRACKBALL":
DispatchQueue.global(qos: .userInteractive).async {
self.updateTouchLocation(touch: touches.first!)
LiSendMouseMoveEvent(Int16(truncatingIfNeeded: Int(self.deltaX * 1.7 * self.sensitivityFactorX)), Int16(truncatingIfNeeded: Int(self.deltaY * 1.7 * self.sensitivityFactorY)))
self.trackballVelocity = CGPoint(x: self.deltaX * 1.7 * self.sensitivityFactorX, y: self.deltaY * 1.7 * self.sensitivityFactorY)
self.stopTrackballMomentum()
if self.firstTouchMoved {
LiSendMouseMoveEvent(Int16(self.deltaX * 1.7 * self.sensitivityFactorX), Int16(self.deltaY * 1.7 * self.sensitivityFactorY))
self.trackballVelocity = CGPoint(x: self.deltaX * 1.7 * self.sensitivityFactorX, y: self.deltaY * 1.7 * self.sensitivityFactorY)
self.stopTrackballMomentum()
}
self.updateTouchLocation(touches)
}
break
case "ABSMOUSE":
self.updateTouchLocation(touch: touches.first!)
if !self.absMousePaused {
if self.firstTouchMoved {
if !self.absMousePaused {
let reachedEdgeMask = LiSendMouseMoveAsMousePositionEvent(Int16(truncatingIfNeeded: Int(self.deltaX * 1.7 * self.sensitivityFactorX)), Int16(truncatingIfNeeded: Int(self.deltaY * 1.7 * self.sensitivityFactorY)), Int16(self.superViewWidth), Int16(self.superViewHeight))
if reachedEdgeMask>0 {
self.handleMousePadButtonActionUp()
@@ -1718,54 +1728,60 @@ import UIKit
}
}
}
}
self.updateTouchLocation(touches)
break
case "LSPAD":
self.updateTouchLocation(touch: touches.first!)
DispatchQueue.global(qos: .userInteractive).async {
self.sendLeftStickTouchPadEvent(inputX: self.offSetX * self.sensitivityFactorX, inputY: self.offSetY * self.sensitivityFactorY)
}
if widgetType == WidgetTypeEnum.touchPad {updateStickIndicator()}
self.updateTouchLocation(touches)
case "RSPAD":
self.updateTouchLocation(touch: touches.first!)
DispatchQueue.global(qos: .userInteractive).async {
self.sendRightStickTouchPadEvent(inputX: self.offSetX * self.sensitivityFactorX, inputY: self.offSetY * self.sensitivityFactorY)
}
if widgetType == WidgetTypeEnum.touchPad {updateStickIndicator()}
self.updateTouchLocation(touches)
case "LSVPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.updateTouchLocation(touch: touches.first!)
self.sendLeftStickTouchPadEvent(inputX: self.deltaX*1.5167*self.sensitivityFactorX, inputY: self.deltaY*1.5167*self.sensitivityFactorY)
if self.firstTouchMoved {
self.sendLeftStickTouchPadEvent(inputX: self.deltaX*1.5167*self.sensitivityFactorX, inputY: self.deltaY*1.5167*self.sensitivityFactorY)
}
self.updateTouchLocation(touches)
}
case "RSVPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.updateTouchLocation(touch: touches.first!)
self.sendRightStickTouchPadEvent(inputX: self.deltaX*1.5167*self.sensitivityFactorX, inputY: self.deltaY*1.5167*self.sensitivityFactorY)
if self.firstTouchMoved {
self.sendRightStickTouchPadEvent(inputX: self.deltaX*1.5167*self.sensitivityFactorX, inputY: self.deltaY*1.5167*self.sensitivityFactorY)
}
self.updateTouchLocation(touches)
}
case "LTPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.updateTouchLocation(touch: touches.first!)
self.sendLeftTriggerTouchPadEvent(inputY: (-self.offSetY*4.5*self.sensitivityFactorY))
self.updateTouchLocation(touches)
}
case "RTPAD":
DispatchQueue.global(qos: .userInteractive).async {
self.updateTouchLocation(touch: touches.first!)
self.sendRightTriggerTouchPadEvent(inputY: (-self.offSetY*4.5*self.sensitivityFactorY))
self.updateTouchLocation(touches)
}
break
case "DPAD", "WASDPAD", "ARROWPAD":
self.updateTouchLocation(touch: touches.first!)
handleLrudTouchMove()
self.updateTouchLocation(touches)
case "MOUSEWHEEL","WHEEL":
self.updateTouchLocation(touch: touches.first!)
if firstTouchMoved {LiSendHighResScrollEvent(Int16(self.deltaY*7.5*self.sensitivityFactorY))}
self.updateTouchLocation(touches)
case "DISCRETEWHEEL", "DSWHEEL":
self.updateTouchLocation(touch: touches.first!)
let currentLocation = touches.first!.location(in: self)
tickFlag = (tickFlag+1)%UInt8(CGFloat(tickCycle)/abs(sensitivityFactorY))
var delta = 0.0
if self.deltaY==0 {delta=self.latestTouchLocation.y-self.touchBeganLocation.y}
else {delta = self.deltaY}
var delta = self.deltaY
if delta==0 {delta=self.touchBeganLocation.y-currentLocation.y}
delta = delta * CGFloat(copysign(1.0, sensitivityFactorY))
if(tickFlag == 1) {LiSendScrollEvent(delta > 0 ? -3 : 3)}
if tickFlag == 1, delta != 0 {LiSendScrollEvent(delta > 0 ? -3 : 3)}
// self.latestTouchLocation = currentLocation
self.updateTouchLocation(touches)
default:
break
}
@@ -1957,6 +1973,7 @@ import UIKit
// touches.count == allCapturedTouchesCount means allfingers are lifting
if(self.mouseButtonAction == MouseButtonAction.hovering) {self.sendMouseRightButtonClickEvent()}
twoTouchesDetected = false
firstTouchMoved = false
}
// then other types of pads or buttons with touchPad function
+27 -74
View File
@@ -23,7 +23,6 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
BOOL firstTouchMoved;
BOOL mousePointerMoved;
BOOL quickTapDetected;
BOOL isInMouseWheelScrollingMode;
// upper screen edge check
bool touchPointSpawnedAtUpperScreenEdge;
@@ -55,8 +54,8 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
_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;
isInMouseWheelScrollingMode = false;
firstTouchMoved = false;
mousePointerMoved = false;
mousePointerTimestamp = 0;
@@ -86,35 +85,11 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
for(UITouch* touch in touches){
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) return true;
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) return true;
}
return false;
}
- (bool)isOnScreenWidgetViewBeingPressed {
bool gotOneButtonPressed = false;
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
if(widgetView.pressedFlagForTapGesture){
gotOneButtonPressed = true; //got one button pressed
}
}
}
return gotOneButtonPressed;
}
- (void)resetAllPressedFlagsForOnScreenWidgetViews {
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
widgetView.pressedFlagForTapGesture = 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), ^{
@@ -153,23 +128,13 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
}
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.
if([[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:[event allTouches]]){
NSLog(@"get in scrolling mode");
isInMouseWheelScrollingMode = true;
return; // if we got 2 touches on the blank area, it's gonna be a mouse scroll touch, and must prevent UITtouch object for mouse pointer being captured & locked
}
// NSLog(@"touches count in began stage: %llu", (uint64_t)[touches count]);
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){
// NSLog(@"candidate touch test: %llu", (uint64_t)touch);
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]){
// NSLog(@"%f controller tap detected", CACurrentMediaTime());
if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]){
continue;
}
else candidateTouch = touch;
@@ -185,7 +150,7 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); // do not press down mouse button here, or it wiil easily turn to double click on remote PC
}
// we must use [event allTouches] to check if touchLockedForMouseMove is captured, because the UITouch object could be captured by upper layer of UIView(in cases like tap gestures), not passed to the touches callbacks in this class, but still available in [event allTouches]
// 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");
@@ -197,7 +162,7 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
- (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];
@@ -211,8 +176,14 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
twoFingerTouchLocation = avgLocation;
return;
}
// NSLog(@"%f touchesMoved callback, locked touch: %llu", CACurrentMediaTime(), (uintptr_t)touchLockedForMouseMove);
*/
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];
@@ -225,10 +196,6 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(isInMouseWheelScrollingMode){
isInMouseWheelScrollingMode = false;
return;
}
if([touches containsObject:touchLockedForMouseMove]){
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
@@ -245,48 +212,34 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2;
mousePointerMoved = false;
}
if([[event allTouches] count] == [touches count]){
isInMouseWheelScrollingMode = 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
[self resetAllPressedFlagsForOnScreenWidgetViews]; // reset all pressed flag for on-screen widget views after all fingers lifted from screen.
}
touchPointSpawnedAtUpperScreenEdge = false;
}
- (void)displayLinkCallback:(CADisplayLink *)link {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
if(self->touchLockedForMouseMove && !self->mousePointerMoved){
CGPoint testpoint = [self->touchLockedForMouseMove locationInView:self->streamView];
NSLog(@"displayLinkCallback %f: %f, %f", CACurrentMediaTime(), testpoint.x, testpoint.y);
//[self sendMouseMoveEvent:touchLockedForMouseMove];
}
});
}
- (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;
bool isAdjacentPoints = [self isAdjacentPoints:self->initialMousePointerLocation from:currentLocation tolerance:0.5];
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:5];
if (!self->firstTouchMoved && !isAdjacentPoints) {
self->latestMousePointerLocation = currentLocation;
self->firstTouchMoved = true;
}
if (self->latestMousePointerLocation.x != currentLocation.x ||
self->latestMousePointerLocation.y != currentLocation.y)
{
int deltaX = (currentLocation.x - self->latestMousePointerLocation.x) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
int deltaY = (currentLocation.y - self->latestMousePointerLocation.y) * 1.35 * self->currentSettings.mousePointerVelocityFactor.floatValue;
if (deltaX != 0 || deltaY != 0) {
self->latestMousePointerLocation = currentLocation;
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);
}
}
});
}
+2 -1
View File
@@ -170,7 +170,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[_onScreenControls setLevel:OnScreenControlsLevelOff];
//pass touchesCaptureByOnScreenButtons Set to the native touchhandler, this NSSet is init witihin onscreencontrols class, don't do it again in native touch handler class
[OnScreenControls.touchAddrsCapturedByOnScreenControls removeAllObjects]; // reset the attribute to nil
[OnScreenControls.touchesCapturedByOnScreenControls removeAllObjects]; // reset the attribute to nil
/*
if(settings.touchMode.intValue == NativeTouch){
@@ -237,6 +237,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
keyboardToggleRecognizer.delaysTouchesBegan = NO;
keyboardToggleRecognizer.delaysTouchesEnded = NO;
[self->_streamFrameTopLayerView addGestureRecognizer:keyboardToggleRecognizer];
keyboardToggleRecognizer.touchCapturingView = self;
}
- (void)keyboardWillShow:(NSNotification *)notification{
@@ -0,0 +1,42 @@
//
// TouchPadGestureHandler.swift
// VoidLink
//
// Created by True砖家 on 2025/11/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
@objc class TouchPadGestureHandler: NSObject {
@objc public static var ctrlDown:Bool = false
@objc public static func handleGesture(in view: UIView, with event: UIEvent) {
let currentTouches = UITouchUtil.touches(in: view, from: event)
guard currentTouches.count == 2 else { return }
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_RIGHT)
guard let touch1 = currentTouches.first else { return }
var mutable = Array(currentTouches)
mutable.removeAll { $0 == touch1 }
guard let touch2 = mutable.first else { return }
let currentDistance = UITouchUtil.distance(between: touch1, and: touch2, in: view)
let previousDistance = UITouchUtil.previousDistance(between: touch1, and: touch2, in: view)
let midPointDeltaY = UITouchUtil.midPointDeltaY(between: touch1, and: touch2, in: view)
let midPointDeltaX = UITouchUtil.midPointDeltaX(between: touch1, and: touch2, in: view)
LiSendHighResScrollEvent(Int16((currentDistance - previousDistance) * 5 + midPointDeltaY * 5))
LiSendHighResHScrollEvent(Int16(-midPointDeltaX) * 5)
if abs(currentDistance - previousDistance) > abs(midPointDeltaY) {
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_DOWN), 0)
ctrlDown = true
} else {
LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["CTRL"]!, CChar(KEY_ACTION_UP), 0)
ctrlDown = false
}
}
}
+51
View File
@@ -0,0 +1,51 @@
//
// UITouchUtil.swift
// VoidLink
//
// Created by True砖家 on 2025/11/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
@objc class UITouchUtil: NSObject {
@objc public static func touches(in view: UIView, from event: UIEvent?) -> Set<UITouch> {
guard let allTouches = event?.allTouches else { return [] }
return event?.allTouches?.filter({ $0.view == view }) ?? []
}
@objc public static func distance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
let p1 = touch1.location(in: view)
let p2 = touch2.location(in: view)
return hypot(p1.x - p2.x, p1.y - p2.y)
}
@objc public static func previousDistance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
let p1 = touch1.previousLocation(in: view)
let p2 = touch2.previousLocation(in: view)
return hypot(p1.x - p2.x, p1.y - p2.y)
}
@objc public static func getDeltaX(from touch: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch = touch else { return 0 }
return touch.location(in: view).x - touch.previousLocation(in: view).x
}
@objc public static func getDeltaY(from touch: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch = touch else { return 0 }
return touch.location(in: view).y - touch.previousLocation(in: view).y
}
@objc public static func midPointDeltaX(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
return (touch1.location(in: view).x+touch2.location(in: view).x)/2 - (touch1.previousLocation(in: view).x+touch2.previousLocation(in: view).x)/2
}
@objc public static func midPointDeltaY(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat {
guard let view = view, let touch1 = touch1, let touch2 = touch2 else { return 0 }
return (touch1.location(in: view).y+touch2.location(in: view).y)/2 - (touch1.previousLocation(in: view).y+touch2.previousLocation(in: view).y)/2
}
}
@@ -716,7 +716,7 @@ BOOL isCustomResolution(int resolutionSelected) {
[self addSetting:self.pointerVelocityFactorStack ofId:@"pointerVelocityFactorStack" withInfoTag:YES withDynamicLabel:YES to:touchAndControlSection];
[self addSetting:self.mousePointerVelocityStack ofId:@"mousePointerVelocityStack" withInfoTag:NO withDynamicLabel:YES to:touchAndControlSection];
[self addSetting:self.singleTapSensitivityStack ofId:@"singleTapSensitivityStack" withInfoTag:NO withDynamicLabel:YES to:touchAndControlSection];
[self addSetting:self.delayLeftClickStack ofId:@"delayLeftClickStack" withInfoTag:YES withDynamicLabel:NO to:touchAndControlSection];
// [self addSetting:self.delayLeftClickStack ofId:@"delayLeftClickStack" withInfoTag:YES withDynamicLabel:NO to:touchAndControlSection];
[self addSetting:self.onScreenWidgetStack ofId:@"onScreenWidgetStack" withInfoTag:YES withDynamicLabel:YES to:touchAndControlSection];
[self addSetting:self.buttonVisualFeedbackStack ofId:@"buttonVisualFeedbackStack" withInfoTag:NO withDynamicLabel:NO to:touchAndControlSection];
[self addSetting:self.swapAbxyStack ofId:@"swapAbaxyStack" withInfoTag:NO withDynamicLabel:NO to:touchAndControlSection];
@@ -2879,7 +2879,8 @@ BOOL isCustomResolution(int resolutionSelected) {
NSInteger hapticEngine = self.hapticEngineSelector.selectedSegmentIndex;
CGFloat edgeSlidingSensitivity = self.edgeSlidingSensitivitySlider.value;
NSInteger audioEngine = self.audioEngineSelector.selectedSegmentIndex;
BOOL delayLeftClick = self.delayLeftClickSwitch.isOn;
// BOOL delayLeftClick = self.delayLeftClickSwitch.isOn;
BOOL delayLeftClick = true;
BOOL duckOtherApps = self.duckOtherAppSwitch.isOn;
BOOL muteInBackground = self.muteInBackgroundSwitch.isOn;
NSInteger backgroundSessionTimer = self.backgroundSessionTimerSlider.value == self.backgroundSessionTimerSlider.maximumValue ? (uint32_t) INT16_MAX : (uint32_t)self.backgroundSessionTimerSlider.value;
@@ -304,7 +304,8 @@
_oscLayoutTapRecoginizer.delaysTouchesBegan = NO;
_oscLayoutTapRecoginizer.delaysTouchesEnded = NO;
if(_settings.touchMode.intValue == AbsoluteTouch) _oscLayoutTapRecoginizer.immediateTriggering = true; // make immediate triggering on for absolute touch mode
[self.view addGestureRecognizer:_oscLayoutTapRecoginizer]; //
[self.view addGestureRecognizer:_oscLayoutTapRecoginizer];
_oscLayoutTapRecoginizer.touchCapturingView = _streamView;
}
}
@@ -325,7 +326,7 @@
[_scrollView setShowsHorizontalScrollIndicator:NO];
[_scrollView setShowsVerticalScrollIndicator:NO];
[_scrollView setDelegate:self];
[_scrollView setMaximumZoomScale:10.0f];
[_scrollView setMaximumZoomScale:1.0f];
if(!_mainFrameViewcontroller.settingsExpandedInStreamView){
// Add StreamView inside a UIScrollView for absolute mode
[_scrollView addSubview:_streamView];