From 053893005b56b8314956bdfda4018151966bcbc7 Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Fri, 5 Dec 2025 10:15:53 +0800 Subject: [PATCH 1/3] rename varialbles --- VoidLink/Database/DataManager.h | 2 +- VoidLink/Database/DataManager.m | 4 ++-- VoidLink/ViewControllers/SettingsViewController.m | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/VoidLink/Database/DataManager.h b/VoidLink/Database/DataManager.h index 29dccf6c..e26e5cf1 100644 --- a/VoidLink/Database/DataManager.h +++ b/VoidLink/Database/DataManager.h @@ -116,7 +116,7 @@ typedef NS_ENUM(NSInteger, WidgetSizeTransition) { preferredCodec:(uint32_t)preferredCodec enableYUV444:(BOOL)enableYUV444 enablePIP:(BOOL)enablePIP - fullRange:(BOOL)fullRange + fullColorRange:(BOOL)fullColorRange enableHdr:(BOOL)enableHdr btMouseSupport:(BOOL)btMouseSupport // absoluteTouchMode:(BOOL)absoluteTouchMode diff --git a/VoidLink/Database/DataManager.m b/VoidLink/Database/DataManager.m index 1e506b34..4f0741de 100644 --- a/VoidLink/Database/DataManager.m +++ b/VoidLink/Database/DataManager.m @@ -88,7 +88,7 @@ preferredCodec:(uint32_t)preferredCodec enableYUV444:(BOOL)enableYUV444 enablePIP:(BOOL)enablePIP - fullRange:(BOOL)fullRange + fullColorRange:(BOOL)fullColorRange enableHdr:(BOOL)enableHdr btMouseSupport:(BOOL)btMouseSupport // absoluteTouchMode:(BOOL)absoluteTouchMode @@ -162,7 +162,7 @@ settingsToSave.preferredCodec = preferredCodec; settingsToSave.enableYUV444 = enableYUV444; settingsToSave.enablePIP = enablePIP; - settingsToSave.fullColorRange = fullRange; + settingsToSave.fullColorRange = fullColorRange; settingsToSave.enableHdr = enableHdr; settingsToSave.btMouseSupport = btMouseSupport; // settingsToSave.absoluteTouchMode = absoluteTouchMode; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 25264f74..8cb7461a 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -755,7 +755,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self addSetting:self.codecStack ofId:@"codecStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; [self addSetting:self.hdrStack ofId:@"hdrStack" withInfoTag:![self hdrSupported] withDynamicLabel:NO to:videoSection]; [self addSetting:self.yuv444Stack ofId:@"yuv444Stack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; - [self addSetting:self.fullColorRangeStack ofId:@"fullRangeStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; + [self addSetting:self.fullColorRangeStack ofId:@"fullColorRangeStack" withInfoTag:NO withDynamicLabel:NO to:videoSection]; [self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; [self addSetting:self.framePacingStack ofId:@"framePacingStack" withInfoTag:YES withDynamicLabel:NO to:videoSection]; [self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" withInfoTag:NO withDynamicLabel:YES to:videoSection]; @@ -3316,7 +3316,7 @@ BOOL isCustomResolution(int resolutionSelected) { uint32_t preferredCodec = [self getChosenCodecPreference]; BOOL enableYUV444 = self.yuv444Switch.isOn; BOOL enablePIP = self.pipSwitch.isOn; - BOOL fullRange = self.fullColorRangeSwitch.isOn; + BOOL fullColorRange = self.fullColorRangeSwitch.isOn; BOOL btMouseSupport = self.citrixX1MouseSwitch.isOn; NSInteger touchMode = [self isNotNativeTouchOnly] ? self.touchModeSelector1.selectedSegmentIndex : NativeTouchOnly; NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex]; @@ -3388,7 +3388,7 @@ BOOL isCustomResolution(int resolutionSelected) { preferredCodec:preferredCodec enableYUV444:enableYUV444 enablePIP:enablePIP - fullRange:fullRange + fullColorRange:fullColorRange enableHdr:enableHdr btMouseSupport:btMouseSupport touchMode:touchMode @@ -3446,7 +3446,8 @@ BOOL isCustomResolution(int resolutionSelected) { uint32_t codec = [self getChosenCodecPreference]; BOOL isAV1 = (codec == CODEC_PREF_AV1); BOOL isH264 = (codec == CODEC_PREF_H264); - + BOOL isHEVC = (codec == CODEC_PREF_HEVC); + if (isAV1) { // AV1 must use limited range, so disable fullRange and turn it off [self.fullColorRangeSwitch setOn:NO animated:NO]; @@ -3459,6 +3460,8 @@ BOOL isCustomResolution(int resolutionSelected) { [self.fullColorRangeSwitch setEnabled:YES]; [self.yuv444Switch setEnabled:YES]; } + + if(isHEVC || isH264) [self.fullColorRangeSwitch setOn:YES animated:NO]; // H264 doesn't support HDR, so disable it and turn it off if (isH264) { From adafacbf81403e6fe34061415b7ea00060ba17f5 Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Fri, 5 Dec 2025 10:28:08 +0800 Subject: [PATCH 2/3] - feat: on-screen button "oscHome" --- VoidLink/Input/CommandManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/VoidLink/Input/CommandManager.swift b/VoidLink/Input/CommandManager.swift index 5ca6b298..3d95ca94 100644 --- a/VoidLink/Input/CommandManager.swift +++ b/VoidLink/Input/CommandManager.swift @@ -98,6 +98,7 @@ import UIKit "PADDLE3" : PADDLE3_FLAG, "PADDLE4" : PADDLE4_FLAG, "MISC" : MISC_FLAG, + "OSCHOME" : SPECIAL_FLAG, "OSCL2" : 0, "L2" : 0, "LT" : 0, From 7caa778ff82fd94c2a9344ba4a073d4eaf744dea Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Tue, 9 Dec 2025 16:38:28 +0800 Subject: [PATCH 3/3] - fixed memory leaks caused by on-screen button auto-tap timer - feat: Inertial RsvPad/LsvPad --- VoidLink.xcodeproj/project.pbxproj | 4 + VoidLink/Input/CommandManager.swift | 1 + VoidLink/Input/ControllerSupport.m | 1 - VoidLink/Input/CustomOSC/OSCProfilesManager.m | 2 +- VoidLink/Input/InertialScroller.swift | 35 +++++ VoidLink/Input/MicHandler.swift | 4 +- VoidLink/Input/OnScreenWidgetView.swift | 120 ++++++++++++++---- VoidLink/Input/SafeTimer.swift | 108 +++++++++++----- VoidLink/Input/StreamView.m | 5 +- .../LayoutOnScreenControlsViewController.m | 10 +- .../StreamFrameViewController.m | 2 + 11 files changed, 217 insertions(+), 75 deletions(-) create mode 100644 VoidLink/Input/InertialScroller.swift diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index c008dd6d..c2facdcb 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 18951D1D2EBB6D2300A86E12 /* TouchPadGestureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */; }; 189AD29A2DE7446000E24B88 /* HostCollectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */; }; 18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; }; + 18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B317022EE67A39009B62C2 /* InertialScroller.swift */; }; 18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; }; 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; 841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 841BAC912DE35D6B00DA639E /* ThemeManager.m */; }; @@ -182,6 +183,7 @@ 189AD2962DE743B700E24B88 /* HostCollectionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HostCollectionViewController.h; sourceTree = ""; }; 189AD2992DE7446000E24B88 /* HostCollectionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostCollectionViewController.m; sourceTree = ""; }; 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = ""; }; + 18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = ""; }; 18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = ""; }; 18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = ""; }; 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -714,6 +716,7 @@ 988FCD3F293B091B003050E2 /* KeyboardInputField.h */, 988FCD40293B091B003050E2 /* KeyboardInputField.m */, 18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */, + 18B317022EE67A39009B62C2 /* InertialScroller.swift */, 18951D1C2EBB6D2300A86E12 /* TouchPadGestureHandler.swift */, 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */, 1850C8292E667E4000845308 /* MicHandler.swift */, @@ -1106,6 +1109,7 @@ 1888C1922E24C33100108730 /* AboutViewController.swift in Sources */, FBD349621A0089F6002D2A60 /* DataManager.m in Sources */, 97E20DE72E216510000D5DCA /* ImGuiRenderer.mm in Sources */, + 18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */, FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */, FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */, 84351B2B2C29AD6100A77989 /* LayoutOnScreenControlsViewController.m in Sources */, diff --git a/VoidLink/Input/CommandManager.swift b/VoidLink/Input/CommandManager.swift index 3d95ca94..5d45382a 100644 --- a/VoidLink/Input/CommandManager.swift +++ b/VoidLink/Input/CommandManager.swift @@ -125,6 +125,7 @@ import UIKit @objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"] @objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"] @objc public static let vectorTouchPads: [String] = ["LSVPAD", "RSVPAD", "MOUSEPAD", "TRACKBALL"] + @objc public static let inertialTouchPads: [String] = ["LSVPAD", "RSVPAD", "TRACKBALL"] @objc public static let verticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"] @objc public static let bidirectionalVerticalTouchPads: [String] = ["LTPAD", "RTPAD", "MOUSEWHEEL", "WHEEL", "DISCRETEWHEEL", "DSWHEEL"] @objc public static let functionalButtonCmds: [String] = ["SETTINGS", "TOOLBOX", "WIDGETTOOL", "WIDGETPROFILES", "PROFILES", "SOFTKEYBOARD", "ABSTCHDRAG"] diff --git a/VoidLink/Input/ControllerSupport.m b/VoidLink/Input/ControllerSupport.m index 562f135d..9e4e6a44 100644 --- a/VoidLink/Input/ControllerSupport.m +++ b/VoidLink/Input/ControllerSupport.m @@ -1014,7 +1014,6 @@ double rc_expo(double x, double expo) { - (void)sendStickToMouseMoveEventWithStickX:(float)stickX stickY:(float)stickY expo:(float)expo { CGFloat mouseDeltaX = _stickToMouseVelocity*rc_expo(stickX, expo); CGFloat mouseDeltaY = _stickToMouseVelocity*rc_expo(stickY, expo); - NSLog(@"mouseDeltaX %f, mouseDeltaY %f, %f, %ld", mouseDeltaX, mouseDeltaY, CACurrentMediaTime(), (long)_displayLink.preferredFramesPerSecond); LiSendMouseMoveEvent(mouseDeltaX, -mouseDeltaY); } diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m index 34364e25..c0f165fe 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m @@ -406,7 +406,7 @@ static CGRect layoutViewBounds; buttonState.yawFactor = widgetView.yawFactor; buttonState.pitchFactor = widgetView.pitchFactor; buttonState.rollFactor = widgetView.rollFactor; - buttonState.decelerationRate = widgetView.trackballDecelerationRate; + buttonState.decelerationRate = widgetView.decelerationRate; buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset; buttonState.widgetShape = widgetView.shape; buttonState.minStickOffset = widgetView.minStickOffset; diff --git a/VoidLink/Input/InertialScroller.swift b/VoidLink/Input/InertialScroller.swift new file mode 100644 index 00000000..659c0970 --- /dev/null +++ b/VoidLink/Input/InertialScroller.swift @@ -0,0 +1,35 @@ +// +// InertialScroller.swift +// VoidLink +// +// Created by True砖家 on 2025/11/5. +// Copyright © 2025 True砖家 on Bilibili. All rights reserved. +// + +class InertialScroller { + public var decelerationRate: CGFloat + + public lazy var timer: SafeTimer? = { + SafeTimer(interval: 1/displayLinkRate) { [weak self] in + guard let self = self, handler != nil else {return} + let deceratedDeltaX = self.vector.dx * decelerationRate + let deceratedDeltaY = self.vector.dy * decelerationRate + self.vector = CGVector(dx: deceratedDeltaX, dy: deceratedDeltaY) + if abs(deceratedDeltaX) < self.timerSuspendThreshold && abs(deceratedDeltaY) < self.timerSuspendThreshold { + self.timer?.pause() + } + (self.handler ?? {})() + } + }() + + public var vector: CGVector = .zero + public var displayLinkRate: CGFloat + public var timerSuspendThreshold: CGFloat = 0.06 + public var handler: (() -> Void)? + + init(decelerationRate: CGFloat = 0.93, displayLinkRate: CGFloat = 60, handler: (() -> Void)? = nil) { + self.decelerationRate = decelerationRate + self.displayLinkRate = displayLinkRate + self.handler = handler + } +} diff --git a/VoidLink/Input/MicHandler.swift b/VoidLink/Input/MicHandler.swift index 6bf9a447..c6824111 100644 --- a/VoidLink/Input/MicHandler.swift +++ b/VoidLink/Input/MicHandler.swift @@ -162,7 +162,7 @@ public class MicHandler: NSObject { @objc public func startTapping() { // recordedBuffers.removeAll() isRecording = true - self.timer?.resume() + self.timer?.start() /* DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in guard let self = self else { return } @@ -175,7 +175,7 @@ public class MicHandler: NSObject { @objc public func stopTapping(stopEngine:Bool) { isRecording = false - self.timer?.suspend() + self.timer?.pause() // engine.inputNode.removeTap(onBus: 0) if(stopEngine){ playerNode.stop() diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 75792524..c6f44799 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -42,9 +42,14 @@ import UIKit case touchPad } + private static let MinAutotapInterval:Int = 50 + private let oscProfileMan: OSCProfilesManager = OSCProfilesManager.sharedManager(CGRectZero) private var oscProfile: OSCProfile + private let dataMan: DataManager = DataManager() + private let tempSettings: TemporarySettings + @objc public var widgetType: WidgetTypeEnum = WidgetTypeEnum.uninitialized @objc static public var editMode: Bool = false @@ -93,6 +98,9 @@ import UIKit private var vibrationGenerator = UIImpactFeedbackGenerator(style: .light) private var vibrationOn: Bool = false + private var inertialScroller:InertialScroller + @objc public var displayLinkRate:CGFloat = 60; + // for movable buttons during streaming @objc public var relocatedDuringStreaming: Bool = false @@ -120,7 +128,7 @@ import UIKit @objc public var hasAutoTap: Bool = false @objc public var isMousePadWithButtonActions: Bool = false - @objc public var hasTrackBall: Bool = false + @objc public var hasInertia: Bool = false @objc public var isFuncationalButton: Bool = false @objc public var hasHapticFeedback: Bool = false @objc public var isDirectionPad: Bool = false @@ -206,7 +214,7 @@ import UIKit // trackball private var trackballVelocity: CGPoint = .zero private var trackballDecelerationTimer: Timer? - @objc public var trackballDecelerationRate: CGFloat = 0.93 + @objc public var decelerationRate: CGFloat = 0.93 private let trackballVelocityThreshold: CGFloat = 0.1 @objc public var slideThreshold: CGFloat = 6.0 @@ -324,6 +332,8 @@ import UIKit } self.activePointerIds = [] self.oscProfile = oscProfileMan.getSelectedProfile() + self.tempSettings = dataMan.getSettings() + self.inertialScroller = InertialScroller() super.init(frame: .zero) // helps widget panel to hide/show stacks @@ -375,7 +385,7 @@ import UIKit self.hasAutoTap = self.widgetType == WidgetTypeEnum.button && self.functionalButtonString == "" && self.motionControlButtonString == "" self.isMousePadWithButtonActions = CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && widgetType == WidgetTypeEnum.touchPad - self.hasTrackBall = self.touchPadString == "TRACKBALL" + self.hasInertia = CommandManager.inertialTouchPads.contains(self.touchPadString) self.isFuncationalButton = self.functionalButtonString != "" self.hasHapticFeedback = !self.comboButtonStrings.isEmpty || CommandManager.directionPads.contains(self.touchPadString) self.isDirectionPad = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.directionPads.contains(self.touchPadString) @@ -384,8 +394,9 @@ import UIKit // ====================================================================================================== @objc public func setupAutoTapTimer() { - if self.widgetType == WidgetTypeEnum.button { + if self.widgetType == WidgetTypeEnum.button, autoTapInterval >= OnScreenWidgetView.MinAutotapInterval { self.autoTapTimer = SafeTimer(interval:0.001 * Double(autoTapInterval)) { + // print("timer instance \(Unmanaged.passUnretained(self.autoTapTimer!).toOpaque()), \(CACurrentMediaTime())") self.handleButtonDown() DispatchQueue.main.asyncAfter(deadline: .now() + 0.025) { self.handleButtonUp() @@ -394,6 +405,12 @@ import UIKit } } + @objc public func setupInertialScroller() { + if self.hasInertia { + self.inertialScroller = InertialScroller(decelerationRate: self.decelerationRate, displayLinkRate: CGFloat(self.tempSettings.framerate.intValue)) + } + } + @objc public func setVibration(style: Int) { if #available(iOS 13.0, *) { vibrationOn = style < UIImpactFeedbackGenerator.FeedbackStyle.rigid.rawValue + 1 @@ -1106,29 +1123,22 @@ import UIKit //mousepad-trackball behavior======================================================== private func startTrackballMomentum() { - stopTrackballMomentum() + // stopTrackballMomentum() - trackballDecelerationTimer = Timer.scheduledTimer(withTimeInterval: 1/Double(UIScreen.main.maximumFramesPerSecond), repeats: true) { [weak self] _ in - guard let self = self else { return } - - LiSendMouseMoveEvent( - Int16(truncatingIfNeeded: Int(self.trackballVelocity.x)), - Int16(truncatingIfNeeded: Int(self.trackballVelocity.y)) - ) - - self.trackballVelocity.x *= self.trackballDecelerationRate - self.trackballVelocity.y *= self.trackballDecelerationRate - - if abs(self.trackballVelocity.x) < self.trackballVelocityThreshold && - abs(self.trackballVelocity.y) < self.trackballVelocityThreshold { - self.stopTrackballMomentum() + if self.inertialScroller.handler == nil { + self.inertialScroller.handler = { + LiSendMouseMoveEvent( + Int16(self.inertialScroller.vector.dx*1.7), + Int16(self.inertialScroller.vector.dy*1.7) + ) } } + + self.inertialScroller.timer?.restart() } private func stopTrackballMomentum() { - trackballDecelerationTimer?.invalidate() - trackballDecelerationTimer = nil + self.inertialScroller.timer?.pause() } @@ -1136,7 +1146,7 @@ import UIKit //==== Button widget tap down============================================= private func handleTapDownOrSlidein() { - if autoTapInterval < 50 { + if autoTapInterval < OnScreenWidgetView.MinAutotapInterval { handleButtonDown() } else{ @@ -1145,11 +1155,11 @@ import UIKit } private func handleFingerUpOrSlideout() { - if autoTapInterval < 50 { + if autoTapInterval < OnScreenWidgetView.MinAutotapInterval { handleButtonUp() } else{ - self.autoTapTimer?.suspend() + self.autoTapTimer?.pause() self.handleButtonUp() } } @@ -1448,7 +1458,19 @@ import UIKit if quickDoubleTapDetected { self.showl3r3Indicator() self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} - case "LSVPAD","RSVPAD","DS4TOUCH": + case "LSVPAD": + self.clearLeftStickTouchPadFlag() + self.inertialScroller.timer?.pause() + if quickDoubleTapDetected { + self.showl3r3Indicator() + self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} + case "RSVPAD": + self.clearRightStickTouchPadFlag() + self.inertialScroller.timer?.pause() + if quickDoubleTapDetected { + self.showl3r3Indicator() + self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} + case "DS4TOUCH": if quickDoubleTapDetected { self.showl3r3Indicator() self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} @@ -1694,6 +1716,11 @@ import UIKit self.deltaX = currentTouchLocation.x - self.latestTouchLocation.x self.deltaY = currentTouchLocation.y - self.latestTouchLocation.y + + if self.hasInertia && firstTouchMoved { + self.inertialScroller.vector = UITouchUtil.vector(of: touch, in: self) + } + self.offSetX = currentTouchLocation.x - self.touchBeganLocation.x self.offSetY = currentTouchLocation.y - self.touchBeganLocation.y } @@ -1988,6 +2015,7 @@ import UIKit self.handleMousePadButtonActionUp() } + /* if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && self.touchPadString == "TRACKBALL" && allSpawnedTouchesCount == 1 && !twoTouchesDetected { if(mousePointerMoved){ self.startTrackballMomentum() @@ -1996,7 +2024,7 @@ import UIKit else{ self.stopTrackballMomentum() } - } + }*/ if !OnScreenWidgetView.editMode && self.widgetType == WidgetTypeEnum.touchPad && CommandManager.mousePadWithButtonActions.contains(self.touchPadString) && twoTouchesDetected && touches.count == allSpawnedTouchesCount { // need to enable multi-touch first // touches.count == allCapturedTouchesCount means allfingers are lifting @@ -2015,9 +2043,41 @@ import UIKit self.clearRightStickTouchPadFlag() if widgetType == WidgetTypeEnum.touchPad {self.resetStickBallPositionAndHideIndicator()} case "LSVPAD": - self.clearLeftStickTouchPadFlag() + if self.decelerationRate > 0.50001 { + if(!firstTouchMoved) {self.inertialScroller.vector = CGVector(dx: 0, dy: 0)} + if self.inertialScroller.handler == nil { + self.inertialScroller.handler = { + let weightedDeltaX = self.inertialScroller.vector.dx*1.5167*self.sensitivityFactorX + let weightedDeltaY = self.inertialScroller.vector.dy*1.5167*self.sensitivityFactorY + self.sendLeftStickTouchPadEvent(inputX: weightedDeltaX, inputY: weightedDeltaY) + } + } + self.inertialScroller.timer?.restart() + } + else {self.clearLeftStickTouchPadFlag()} case "RSVPAD": - self.clearRightStickTouchPadFlag() + if self.decelerationRate > 0.50001 { + if(!firstTouchMoved) {self.inertialScroller.vector = CGVector(dx: 0, dy: 0)} + if self.inertialScroller.handler == nil { + self.inertialScroller.handler = { + let weightedDeltaX = self.inertialScroller.vector.dx*1.5167*self.sensitivityFactorX + let weightedDeltaY = self.inertialScroller.vector.dy*1.5167*self.sensitivityFactorY + self.sendRightStickTouchPadEvent(inputX: weightedDeltaX, inputY: weightedDeltaY) + } + } + self.inertialScroller.timer?.restart() + } + else {self.clearRightStickTouchPadFlag()} + case "TRACKBALL": + if allSpawnedTouchesCount == 1, self.decelerationRate > 0 { + if(mousePointerMoved){ + self.startTrackballMomentum() + mousePointerMoved = false //reset flag + } + else{ + self.stopTrackballMomentum() + } + } case "LTPAD": self.onScreenControls.updateLeftTrigger(0x00) case "RTPAD": @@ -2205,6 +2265,10 @@ import UIKit leftIndicator.removeFromSuperlayer() rightIndicator.removeFromSuperlayer() lrudIndicatorBall.removeFromSuperlayer() + self.inertialScroller.timer?.clean() + self.clearLeftStickTouchPadFlag() + self.clearRightStickTouchPadFlag() + self.autoTapTimer?.clean() } else{ self.superViewWidth = (superview?.bounds.size.width)! diff --git a/VoidLink/Input/SafeTimer.swift b/VoidLink/Input/SafeTimer.swift index f96897b5..3a9436db 100644 --- a/VoidLink/Input/SafeTimer.swift +++ b/VoidLink/Input/SafeTimer.swift @@ -6,47 +6,85 @@ // Copyright © 2025 True砖家@Bilibili. All rights reserved. // +import Foundation +import QuartzCore -class SafeTimer { - private var timer: DispatchSourceTimer - private(set) var isRunning = false - private var interval: TimeInterval = 0 - private var delay: TimeInterval = 0 - - init(interval: TimeInterval = 1.0, queue: DispatchQueue = .global(), delay: TimeInterval = 0, handler: @escaping () -> Void) { - timer = DispatchSource.makeTimerSource(queue: queue) - timer.schedule(deadline: .now() + delay, repeating: interval) - timer.setEventHandler(handler: handler) +/// SafeTimer: 高精度、可暂停/重启、可彻底清理 +final class SafeTimer { + private var timer: DispatchSourceTimer? + private let timerQueue: DispatchQueue // 私有串行队列 + private let userHandler: () -> Void + private let interval: TimeInterval + private let delay: TimeInterval + + private var shouldRunHandler: Bool = false // 控制逻辑上的暂停/恢复 + private var isCleaned: Bool = false // 是否已经 clean + + init(interval: TimeInterval = 1.0, + delay: TimeInterval = 0, + queueLabel: String = "com.example.safetimer", + handler: @escaping () -> Void) { + self.interval = interval self.delay = delay - // 注意:这里不调用 resume,保持 suspended,等 start() 时才运行 - } - - public func resume() { - guard !isRunning else { return } - timer.resume() - isRunning = true - } - - public func suspend() { - guard isRunning else { return } - timer.suspend() - isRunning = false - } - - func restart() { - if isRunning { - timer.suspend() - isRunning = false + self.userHandler = handler + self.timerQueue = DispatchQueue(label: queueLabel) + + let t = DispatchSource.makeTimerSource(queue: timerQueue) + t.schedule(deadline: .now() + delay, repeating: interval) + + t.setEventHandler { [weak self] in + guard let self = self, !self.isCleaned else { return } + if !self.shouldRunHandler { return } + self.userHandler() } - timer.schedule(deadline: .now() + self.delay, repeating: self.interval) // 重置时间 - timer.resume() - isRunning = true + + t.resume() // timer 一开始就 resume,避免 suspend/resume 的计数问题 + self.timer = t } - + deinit { - if !timer.isCancelled { - timer.cancel() + clean() + } + + /// 开始逻辑上的计时 + func start() { + timerQueue.async { [weak self] in + guard let self = self, !self.isCleaned else { return } + self.shouldRunHandler = true + } + } + + /// 暂停逻辑上的计时 + func pause() { + timerQueue.async { [weak self] in + guard let self = self, !self.isCleaned else { return } + self.shouldRunHandler = false + } + } + + /// 重置下一次触发时间,并开始执行 handler + func restart() { + timerQueue.async { [weak self] in + guard let self = self, let t = self.timer, !self.isCleaned else { return } + t.schedule(deadline: .now() + self.delay, repeating: self.interval) + self.shouldRunHandler = true + } + } + + /// 彻底清理 timer,返回后保证 handler 不再执行 + func clean() { + if isCleaned { return } + // 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过) + timerQueue.sync { + self.isCleaned = true + self.shouldRunHandler = false + + if let t = self.timer { + t.setEventHandler {} // 清空 handler + t.cancel() // 取消 timer + self.timer = nil + } } } } diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 636283f1..294494ce 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -568,7 +568,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; widgetView.yawFactor = buttonState.yawFactor; widgetView.pitchFactor = buttonState.pitchFactor; widgetView.rollFactor = buttonState.rollFactor; - widgetView.trackballDecelerationRate = buttonState.decelerationRate; + widgetView.decelerationRate = buttonState.decelerationRate; widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; widgetView.minStickOffset = buttonState.minStickOffset; widgetView.buttonMode = buttonState.buttonMode; @@ -585,6 +585,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha]; [widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha]; [widgetView setupAutoTapTimer]; + [widgetView setupInertialScroller]; } } } @@ -838,8 +839,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; - (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event { - - // NSLog(@"mouse click time: %f", CACurrentMediaTime()*1000); #if !TARGET_OS_TV if (@available(iOS 13.4, *)) { UITouch* touch = [touches anyObject]; diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index 0d69e8b3..aa33cbea 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -157,7 +157,7 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) { widgetView.yawFactor = buttonState.yawFactor; widgetView.pitchFactor = buttonState.pitchFactor; widgetView.rollFactor = buttonState.rollFactor; - widgetView.trackballDecelerationRate = buttonState.decelerationRate; + widgetView.decelerationRate = buttonState.decelerationRate; widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; widgetView.minStickOffset = buttonState.minStickOffset; widgetView.buttonMode = buttonState.buttonMode; @@ -731,7 +731,7 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) { newWidget.yawFactor = widget.yawFactor; newWidget.pitchFactor = widget.pitchFactor; newWidget.rollFactor = widget.rollFactor; - newWidget.trackballDecelerationRate = widget.trackballDecelerationRate; + newWidget.decelerationRate = widget.decelerationRate; newWidget.stickIndicatorOffset = widget.stickIndicatorOffset; newWidget.minStickOffset = widget.minStickOffset; [newWidget setVibrationWithStyle:widget.vibrationStyle]; @@ -977,8 +977,8 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) { }]; [self autoFitLabel:self.autoTapLabel]; - self.decelerationRateStack.hidden = !selectedWidgetView.hasTrackBall; - [self.decelerationRateSlider setValue:selectedWidgetView.trackballDecelerationRate]; + self.decelerationRateStack.hidden = !selectedWidgetView.hasInertia; + [self.decelerationRateSlider setValue:selectedWidgetView.decelerationRate]; [self autoFitLabel:self.decelerationRateLabel]; [self decelerationRateSliderMoved:self.decelerationRateSlider]; @@ -1210,7 +1210,7 @@ typedef NS_ENUM(NSUInteger, BorderWidthSliderMode) { - (void)decelerationRateSliderMoved:(UISlider* )sender{ [self.decelerationRateLabel setText:[LocalizationHelper localizedStringForKey:@"Deceleration Rate: %.3f ", sender.value]]; - if(self->selectedWidgetView != nil && self->widgetViewSelected) self->selectedWidgetView.trackballDecelerationRate = sender.value; + if(self->selectedWidgetView != nil && self->widgetViewSelected) self->selectedWidgetView.decelerationRate = sender.value; return; } diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index e2db3407..a689046f 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -927,6 +927,8 @@ } - (void) returnToMainFrame { + [_streamView clearOnScreenWidgets]; + // Reset display mode back to default [self updatePreferredDisplayMode:NO]; if (@available(iOS 13.0, *)) {