From 2c9902ce77ea902a9dc479926145ac4ba945cbf0 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Wed, 25 Feb 2026 00:38:24 +0800 Subject: [PATCH] feat: auto hover --- VoidLink/Base.lproj/iPad.storyboard | 22 ++++++++++ VoidLink/Input/CustomOSC/OSCProfile.h | 1 + VoidLink/Input/CustomOSC/OSCProfile.m | 2 + VoidLink/Input/PencilHandler.swift | 42 +++++++++++++++---- VoidLink/Input/StreamView.m | 2 +- VoidLink/Localization/Localizable.xcstrings | 29 +++++++++++++ .../ViewControllers/SettingsViewController.h | 2 + .../ViewControllers/SettingsViewController.m | 28 ++++++++++++- VoidLink/mul.lproj/iPad.xcstrings | 30 +++++++++++++ 9 files changed, 147 insertions(+), 11 deletions(-) diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index db043bfb..38d788c2 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -2088,6 +2088,26 @@ + + + + + + + + + + + + + + + @@ -2103,6 +2123,8 @@ + + diff --git a/VoidLink/Input/CustomOSC/OSCProfile.h b/VoidLink/Input/CustomOSC/OSCProfile.h index cfc3ddd8..7136ce9d 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.h +++ b/VoidLink/Input/CustomOSC/OSCProfile.h @@ -58,6 +58,7 @@ typedef NS_ENUM(NSInteger, MapGyroTo) { @property (nonatomic, assign) bool squeezeShorcutEnabled; @property NSString *squeezeStartShortcut; @property NSString *squeezeEndShortcut; +@property (nonatomic, assign) bool autoPencilHover; @property (nonatomic, assign) bool pencilPausesNativeTouch; @property (nonatomic, assign) bool disablePencilSlideGestures; diff --git a/VoidLink/Input/CustomOSC/OSCProfile.m b/VoidLink/Input/CustomOSC/OSCProfile.m index 7f3e8ac0..ff474ff2 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.m +++ b/VoidLink/Input/CustomOSC/OSCProfile.m @@ -59,6 +59,7 @@ [encoder encodeBool:self.squeezeShorcutEnabled forKey:@"squeezeShorcutEnabled"]; [encoder encodeObject:self.squeezeStartShortcut forKey:@"squeezeStartShortcut"]; [encoder encodeObject:self.squeezeEndShortcut forKey:@"squeezeEndShortcut"]; + [encoder encodeBool:self.autoPencilHover forKey:@"autoPencilHover"]; [encoder encodeBool:self.pencilPausesNativeTouch forKey:@"pencilPausesNativeTouch"]; [encoder encodeBool:self.disablePencilSlideGestures forKey:@"disablePencilSlideGestures"]; } @@ -156,6 +157,7 @@ forKey:@"squeezeEndShortcut"] : @""; + self.autoPencilHover = [decoder containsValueForKey:@"autoPencilHover"] ? [decoder decodeBoolForKey:@"autoPencilHover"] : false; self.pencilPausesNativeTouch = [decoder containsValueForKey:@"pencilPausesNativeTouch"] ? [decoder decodeBoolForKey:@"pencilPausesNativeTouch"] : false; self.disablePencilSlideGestures = [decoder containsValueForKey:@"disablePencilSlideGestures"] ? [decoder decodeBoolForKey:@"disablePencilSlideGestures"] : false; } diff --git a/VoidLink/Input/PencilHandler.swift b/VoidLink/Input/PencilHandler.swift index cb5de0af..28694589 100644 --- a/VoidLink/Input/PencilHandler.swift +++ b/VoidLink/Input/PencilHandler.swift @@ -22,7 +22,7 @@ import UIKit private var pencilTickEnabled: Bool private var pressureCurveEnabled: Bool = false private var manualHoverFlag: Bool = false - private var autoHoverFlag: Bool = false + @objc static var autoHoverEnabled: Bool = false private(set) var pencilProEnabled: Bool = false private var isFirstMove: Bool = false private var moveEventIndex: Int64 = 0 @@ -59,15 +59,24 @@ import UIKit selectedProfile = profile! } - if selectedProfile.eraserShortcut != "" { - doubleTapShorcuts.append(selectedProfile.eraserShortcut) + doubleTapShorcuts.removeAll() + if selectedProfile.doubleTapShorcutEnabled { + if selectedProfile.eraserShortcut != "" { + doubleTapShorcuts.append(selectedProfile.eraserShortcut) + } + if selectedProfile.brushShortcut != "" { + doubleTapShorcuts.append(selectedProfile.brushShortcut) + } } - if selectedProfile.brushShortcut != "" { - doubleTapShorcuts.append(selectedProfile.brushShortcut) + + PencilHandler.squeezeStartShortcut = "" + PencilHandler.squeezeEndShortcut = "" + if selectedProfile.squeezeShorcutEnabled { + PencilHandler.squeezeStartShortcut = selectedProfile.squeezeStartShortcut + PencilHandler.squeezeEndShortcut = selectedProfile.squeezeEndShortcut + print("squeezeShorcutEnabled \(CACurrentMediaTime())") } - PencilHandler.squeezeStartShortcut = selectedProfile.squeezeStartShortcut - PencilHandler.squeezeEndShortcut = selectedProfile.squeezeEndShortcut pressureCurveEnabled = selectedProfile.pressureCurveEnabled if #available(iOS 15.0, *) { @@ -76,6 +85,7 @@ import UIKit self.pencilTickEnabled = self.pencilTickEnabled && info.valid self.pencilProEnabled = info.valid PencilHandler.pencilPausesNativeTouch = selectedProfile.pencilPausesNativeTouch && info.valid + PencilHandler.autoHoverEnabled = selectedProfile.autoPencilHover && info.valid if info.valid { self.setupPencilInteraction(view: self.streamView) } @@ -229,9 +239,21 @@ import UIKit let sendableForce = targetForce DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) { + + if PencilHandler.autoHoverEnabled, eventType == UInt8(LI_TOUCH_EVENT_DOWN) { + LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude))) + } + LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), sendableForce, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude))) - if eventType == UInt8(LI_TOUCH_EVENT_UP) { + + if PencilHandler.autoHoverEnabled, eventType == UInt8(LI_TOUCH_EVENT_UP) { PencilHandler.isDrawing = false + LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude))) + DispatchQueue.global().asyncAfter(deadline: .now() + 0.015){ + if !PencilHandler.isDrawing { + LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER_LEAVE), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude))) + } + } } } } @@ -255,6 +277,7 @@ import UIKit private func setupPencilInteraction(view:UIView?) { let interaction = UIPencilInteraction() guard let view else { return } + if pencilInteraction != nil {return} interaction.delegate = self view.addInteraction(interaction) pencilInteraction = interaction @@ -265,6 +288,7 @@ import UIKit if !pencilProEnabled {return} if doubleTapShorcuts.isEmpty {return} let keyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: doubleTapShorcuts[shortcutIndex]) + print("pencilInteractionDidTap \(CACurrentMediaTime())") CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: keyStrings, delay: 0.1) shortcutIndex = (shortcutIndex + 1) % doubleTapShorcuts.count } @@ -275,6 +299,8 @@ import UIKit didReceiveSqueeze squeeze: UIPencilInteraction.Squeeze ) { if !pencilProEnabled {return} + if PencilHandler.squeezeStartShortcut == "", PencilHandler.squeezeEndShortcut == "" {return} + let keepPressedUntilRelease = PencilHandler.squeezeEndShortcut.uppercased() == "NULL" let squeezePressKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeStartShortcut) let squeezeReleaseKeyStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: PencilHandler.squeezeEndShortcut) diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 73be9473..0f8dcd3f 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -837,7 +837,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } #endif - LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height, + if(!PencilHandler.autoHoverEnabled) LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height, distance, 0.0f, 0.0f, rotationAngle, tiltAngle); } diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 749a9245..cfe4c183 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -1594,6 +1594,35 @@ } } }, + "autoHoverStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resolves compatibility issues with certain PC software (such as SAI2) that cannot function properly without hover state." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "用于解决部分PC软件(如SAI2)没有悬浮状态则无法正常使用的问题。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "此功能用於解決部分 PC 軟體(例如 SAI2)在沒有懸浮狀態時無法正常使用的問題。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "此功能用於解決部分 PC 軟體(例如 SAI2)在沒有懸浮狀態時無法正常使用的問題。" + } + } + } + }, "Autotap timer " : { "extractionState" : "manual", "localizations" : { diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index e4250a2d..575d2200 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -281,6 +281,8 @@ @property (weak, nonatomic) IBOutlet UIStackView *disablePencilSlideGestureStack; @property (weak, nonatomic) IBOutlet UISwitch *disablePencilSlideGestureSwitch; +@property (weak, nonatomic) IBOutlet UIStackView *autoHoverStack; +@property (weak, nonatomic) IBOutlet UISwitch *autoHoverSwitch; #pragma clang diagnostic push diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 48b13461..999fb6ae 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -395,6 +395,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self.squeezeShortcutSwitch setOn:oscProfile.squeezeShorcutEnabled]; [self.pencilPausesNativeTouchSwitch setOn:oscProfile.pencilPausesNativeTouch]; [self.disablePencilSlideGestureSwitch setOn:oscProfile.disablePencilSlideGestures]; + [self.autoHoverSwitch setOn:oscProfile.autoPencilHover]; } - (void)saveGameProfileConfigs{ @@ -420,6 +421,7 @@ BOOL isCustomResolution(int resolutionSelected) { && oscProfile.squeezeShorcutEnabled == self.squeezeShortcutSwitch.isOn && oscProfile.pencilPausesNativeTouch == self.pencilPausesNativeTouchSwitch.isOn && oscProfile.disablePencilSlideGestures == self.disablePencilSlideGestureSwitch.isOn + && oscProfile.autoPencilHover == self.autoHoverSwitch.isOn ); if(!configNotChanged){ @@ -441,7 +443,9 @@ BOOL isCustomResolution(int resolutionSelected) { oscProfile.squeezeShorcutEnabled = self.squeezeShortcutSwitch.isOn; oscProfile.pencilPausesNativeTouch = self.pencilPausesNativeTouchSwitch.isOn; oscProfile.disablePencilSlideGestures = self.disablePencilSlideGestureSwitch.isOn; + oscProfile.autoPencilHover = self.autoHoverSwitch.isOn; [oscProfileMan replaceSelectedProfileWith:oscProfile overwriteDefault:YES]; + if(PencilHandler.shared) [PencilHandler.shared setupPressureLUTWithProfile:oscProfile]; } } @@ -885,6 +889,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self addSetting:self.pressureCurveStack ofId:@"pressureCurveStack" withInfoTag:NO withDynamicLabel:NO to:pencilSection]; [self addSetting:self.doubleTapShortcutStack ofId:@"doubleTapShortcutStack" withInfoTag:YES withDynamicLabel:NO to:pencilSection]; [self addSetting:self.squeezeShortcutStack ofId:@"squeezeShortcutStack" withInfoTag:YES withDynamicLabel:NO to:pencilSection]; + [self addSetting:self.autoHoverStack ofId:@"autoHoverStack" withInfoTag:YES withDynamicLabel:NO to:pencilSection]; [self addSetting:self.pencilPausesNativeTouchStack ofId:@"pencilPausesNativeTouchStack" withInfoTag:NO withDynamicLabel:NO to:pencilSection]; [self addSetting:self.disablePencilSlideGestureStack ofId:@"disablePencilSlideGestureStack" withInfoTag:NO withDynamicLabel:NO to:pencilSection]; [pencilSection addToParentStack:_parentStack]; @@ -1520,7 +1525,10 @@ BOOL isCustomResolution(int resolutionSelected) { tipText = [LocalizationHelper localizedStringForKey:@"asyncFrameDequeueStackTip"]; showOnlineDocAction = false; } - + if([sender.superview.accessibilityIdentifier isEqualToString: @"autoHoverStack"]){ + tipText = [LocalizationHelper localizedStringForKey:@"autoHoverStackTip"]; + showOnlineDocAction = false; + } UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Tips"] message:tipText preferredStyle:UIAlertControllerStyleAlert]; @@ -3439,6 +3447,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self.squeezeShortcutSwitch setOn:false]; [self.pencilPausesNativeTouchSwitch setOn:false]; [self.disablePencilSlideGestureSwitch setOn:false]; + [self.autoHoverSwitch setOn:false]; NSNumber *value = notification.userInfo[@"interruption"]; @@ -3512,7 +3521,21 @@ BOOL isCustomResolution(int resolutionSelected) { - (void)pencilPausesNativeTouchSwitchFlipped:(UISwitch* )sender{ if(sender.isOn && !settingsViewJustLoaded){ [IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) { - if(!info.valid) [IAPManager inAppPurchaseActionWithViewController:self product:AddOnProductPencilProPack]; + if(info.valid) nil; + else { + [IAPManager inAppPurchaseActionWithViewController:self product:AddOnProductPencilProPack]; + } + }]; + } +} + +- (void)autoHoverSwitchFlipped:(UISwitch* )sender{ + if(sender.isOn && !settingsViewJustLoaded){ + [IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) { + if(info.valid) nil; + else { + [IAPManager inAppPurchaseActionWithViewController:self product:AddOnProductPencilProPack]; + } }]; } } @@ -3536,6 +3559,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self.disablePencilSlideGestureSwitch addTarget:self action:@selector(disablePencilSlideGestureSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; [self.pencilPausesNativeTouchSwitch addTarget:self action:@selector(pencilPausesNativeTouchSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + [self.autoHoverSwitch addTarget:self action:@selector(autoHoverSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; } } diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 49e1845f..b4bc6a14 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -4747,6 +4747,36 @@ } } }, + "q78-aY-mIe.text" : { + "comment" : "Class = \"UILabel\"; text = \"Auto Hover\"; ObjectID = \"q78-aY-mIe\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Auto Hover" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动悬浮" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動懸浮" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動懸浮" + } + } + } + }, "Qat-gL-qiL.text" : { "comment" : "Class = \"UILabel\"; text = \"Pencil Tick\"; ObjectID = \"Qat-gL-qiL\";", "extractionState" : "extracted_with_value",