diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index d57918aa..25b08b9a 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 180345E12E819877004AF36F /* libopus-sim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 180345E02E819877004AF36F /* libopus-sim.a */; }; + 180E6E372E93BFDB0067C73A /* CountdownAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 180E6E362E93BFDB0067C73A /* CountdownAlertController.swift */; }; 1817B61E2E6FD04D000D938D /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1817B61D2E6FD04D000D938D /* SafeTimer.swift */; }; 182760F72E83E2EA00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F62E83E2EA00EC1C5C /* DequeModule */; }; 182760FA2E83E57D00EC1C5C /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 182760F92E83E57D00EC1C5C /* DequeModule */; }; @@ -160,6 +161,7 @@ /* Begin PBXFileReference section */ 180345E02E819877004AF36F /* libopus-sim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libopus-sim.a"; sourceTree = ""; }; + 180E6E362E93BFDB0067C73A /* CountdownAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownAlertController.swift; sourceTree = ""; }; 1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = ""; }; 1829CE3D2E2154DF0074A99A /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; @@ -635,6 +637,7 @@ 847D2FDA2DDA015F00937A62 /* MenuSectionView.m */, FBDE86E419F82297001C18A8 /* UIAppView.h */, FBDE86E519F82297001C18A8 /* UIAppView.m */, + 180E6E362E93BFDB0067C73A /* CountdownAlertController.swift */, 1888C1902E24C2C800108730 /* AboutView.swift */, 1888C1912E24C33100108730 /* AboutViewController.swift */, FB89463719F6473800339C8A /* Launch Screen.xib */, @@ -1054,6 +1057,7 @@ 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */, 187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */, FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */, + 180E6E372E93BFDB0067C73A /* CountdownAlertController.swift in Sources */, 841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */, 98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */, FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */, diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index 0b7df650..3095c8bf 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -61,7 +61,7 @@ - + @@ -997,6 +997,31 @@ + + + + + + + + + + + + + + + + + + + + @@ -1201,10 +1226,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1242,6 +1380,7 @@ + @@ -1250,6 +1389,8 @@ + + @@ -1266,6 +1407,8 @@ + + @@ -1279,6 +1422,9 @@ + + + @@ -1298,6 +1444,7 @@ + @@ -1310,6 +1457,10 @@ + + + + @@ -1327,7 +1478,7 @@ - + @@ -1426,7 +1577,7 @@ - + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index c176ecf1..6c2b2fea 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -796,7 +796,7 @@ - + @@ -1932,10 +1932,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1973,6 +2112,7 @@ + @@ -1981,6 +2121,8 @@ + + @@ -1996,6 +2138,8 @@ + + @@ -2009,6 +2153,9 @@ + + + @@ -2040,6 +2187,10 @@ + + + + diff --git a/VoidLink/CountdownAlertController.swift b/VoidLink/CountdownAlertController.swift new file mode 100644 index 00000000..da5580c2 --- /dev/null +++ b/VoidLink/CountdownAlertController.swift @@ -0,0 +1,69 @@ +// +// CountdownAlertController.swift +// VoidLink +// +// Created by Weimin on 2025/10/6. +// Copyright © 2025 True砖家 @ Bilibili. All rights reserved. +// +import UIKit + +@objc class CountdownAlertController: NSObject { + + /// 类方法,直接在任何 UIViewController 上显示倒计时弹窗 + /// - Parameters: + /// - viewController: 要显示弹窗的控制器 + /// - title: 弹窗标题 + /// - message: 弹窗内容 + /// - buttonTitle: 按钮初始文字(倒计时结束后显示) + /// - countdown: 倒计时秒数 + /// - completion: 点击确认或倒计时结束的回调 + + @objc public static var actionCancelled:Bool = false + + @objc class func showAlert( + in viewController: UIViewController, + title: String?, + message: String?, + withCancel: Bool, + buttonTitle: String, + countdown: Int, + completion: (() -> Void)? = nil + ) { + var remainingSeconds = countdown + + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let confirmAction = UIAlertAction(title: "\(remainingSeconds)", style: .default) { _ in + actionCancelled = false + completion?() + } + + let cancelAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Cancel"), style: .cancel) { _ in + actionCancelled = true + completion?() + } + + confirmAction.isEnabled = false + + if withCancel {alert.addAction(cancelAction)} + alert.addAction(confirmAction) + + viewController.present(alert, animated: true, completion: nil) + + // 倒计时 + let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main) + timer.schedule(deadline: .now(), repeating: 1.0, leeway: .milliseconds(50)) + timer.setEventHandler { + remainingSeconds -= 1 + + if remainingSeconds <= 0 { + timer.cancel() + confirmAction.isEnabled = true + confirmAction.setValue(buttonTitle, forKey: "title") + } else { + confirmAction.setValue("\(remainingSeconds)", forKey: "title") + } + } + timer.resume() + } +} diff --git a/VoidLink/Database/TemporarySettings.h b/VoidLink/Database/TemporarySettings.h index f1709fa8..15f36b8a 100644 --- a/VoidLink/Database/TemporarySettings.h +++ b/VoidLink/Database/TemporarySettings.h @@ -42,6 +42,9 @@ @property (nonatomic, retain) NSNumber * resolutionSelected; @property (nonatomic, retain) NSNumber * externalDisplayMode; @property (nonatomic, retain) NSNumber * localMousePointerMode; +@property (nonatomic, retain) NSNumber * gyroBiasX; +@property (nonatomic, retain) NSNumber * gyroBiasY; +@property (nonatomic, retain) NSNumber * gyroBiasZ; @property (nonatomic, retain) NSNumber * backgroundSessionTimer; @property (nonatomic) enum { CODEC_PREF_AUTO, diff --git a/VoidLink/Database/TemporarySettings.m b/VoidLink/Database/TemporarySettings.m index 535e49d0..ffe11d4a 100644 --- a/VoidLink/Database/TemporarySettings.m +++ b/VoidLink/Database/TemporarySettings.m @@ -130,6 +130,9 @@ self.framePacingMode = settings.framePacingMode; self.sendDummyEvent = settings.sendDummyEvent; self.rememberFoldState = settings.rememberFoldState; + self.gyroBiasX = settings.gyroBiasX; + self.gyroBiasY = settings.gyroBiasY; + self.gyroBiasZ = settings.gyroBiasZ; self.backgroundSessionTimer = settings.backroundSessionTimer; #endif self.uniqueId = settings.uniqueId; diff --git a/VoidLink/Images.xcassets/gyroscope.imageset/Contents.json b/VoidLink/Images.xcassets/gyroscope.imageset/Contents.json new file mode 100644 index 00000000..fee5d7aa --- /dev/null +++ b/VoidLink/Images.xcassets/gyroscope.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "gyroscope.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/VoidLink/Images.xcassets/gyroscope.imageset/gyroscope.svg b/VoidLink/Images.xcassets/gyroscope.imageset/gyroscope.svg new file mode 100644 index 00000000..7efe8219 --- /dev/null +++ b/VoidLink/Images.xcassets/gyroscope.imageset/gyroscope.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/VoidLink/Input/CustomOSC/OSCProfile.h b/VoidLink/Input/CustomOSC/OSCProfile.h index 83ccb14c..d81a31f4 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.h +++ b/VoidLink/Input/CustomOSC/OSCProfile.h @@ -16,15 +16,31 @@ NS_ASSUME_NONNULL_BEGIN #define DEFAULT_TEMPLATE_NAME1 @"控件模板 / Widget templates" #define DEFAULT_TEMPLATE_NAME2 @"旧版控件 / Legacy widgets" +typedef NS_ENUM(NSInteger, MapGyroTo) { + mapGyroToMouse, + mapGyroToControllerStick, + driftCorrection +}; + /** This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults. Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches */ -@interface OSCProfile : NSObject +@interface OSCProfile : NSObject @property NSString *name; @property NSMutableArray *buttonStatesEncoded; @property BOOL isSelected; +@property (nonatomic, assign) MapGyroTo mapGyroTo; +@property (nonatomic, assign) bool yawPitchToRightStick; +@property (nonatomic, assign) bool rollToLeftStick; +@property (nonatomic, assign) CGFloat gyroSensitivityYaw; +@property (nonatomic, assign) CGFloat gyroSensitivityPitch; +@property (nonatomic, assign) CGFloat gyroSensitivityRoll; +@property (nonatomic, assign) CGFloat accelSensitivityX; +@property (nonatomic, assign) CGFloat accelSensitivityY; +@property (nonatomic, assign) CGFloat accelSensitivityZ; + - (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected; diff --git a/VoidLink/Input/CustomOSC/OSCProfile.m b/VoidLink/Input/CustomOSC/OSCProfile.m index 7f88a815..937d7f26 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.m +++ b/VoidLink/Input/CustomOSC/OSCProfile.m @@ -31,6 +31,16 @@ [encoder encodeObject:self.name forKey:@"name"]; [encoder encodeObject:self.buttonStatesEncoded forKey:@"buttonStates"]; [encoder encodeBool:self.isSelected forKey:@"isSelected"]; + [encoder encodeInt64:self.mapGyroTo forKey:@"mapGyroTo"]; + [encoder encodeBool:self.yawPitchToRightStick forKey:@"yawPitchToRightStick"]; + [encoder encodeBool:self.rollToLeftStick forKey:@"rollToLeftStick"]; + [encoder encodeInt64:self.mapGyroTo forKey:@"mapGyroTo"]; + [encoder encodeFloat:self.gyroSensitivityYaw forKey:@"gyroSensitivityYaw"]; + [encoder encodeFloat:self.gyroSensitivityPitch forKey:@"gyroSensitivityPitch"]; + [encoder encodeFloat:self.gyroSensitivityRoll forKey:@"gyroSensitivityRoll"]; + [encoder encodeFloat:self.accelSensitivityX forKey:@"accelSensitivityX"]; + [encoder encodeFloat:self.accelSensitivityY forKey:@"accelSensitivityY"]; + [encoder encodeFloat:self.accelSensitivityZ forKey:@"accelSensitivityZ"]; } - (id) initWithCoder:(NSCoder*)decoder { @@ -38,9 +48,35 @@ self.name = [decoder decodeObjectForKey:@"name"]; self.buttonStatesEncoded = [decoder decodeObjectForKey:@"buttonStates"]; self.isSelected = [decoder decodeBoolForKey:@"isSelected"]; + self.mapGyroTo = [decoder containsValueForKey:@"mapGyroTo"] ? [decoder decodeInt64ForKey:@"mapGyroTo"] : mapGyroToMouse; + self.yawPitchToRightStick = [decoder containsValueForKey:@"yawPitchToRightStick"] ? [decoder decodeBoolForKey:@"yawPitchToRightStick"] : true; + self.rollToLeftStick = [decoder containsValueForKey:@"rollToLeftStick"] ? [decoder decodeBoolForKey:@"rollToLeftStick"] : false; + self.gyroSensitivityYaw = [decoder containsValueForKey:@"gyroSensitivityYaw"] ? [decoder decodeFloatForKey:@"gyroSensitivityYaw"] : 1.0; + self.gyroSensitivityPitch = [decoder containsValueForKey:@"gyroSensitivityPitch"] ? [decoder decodeFloatForKey:@"gyroSensitivityPitch"] : 1.0; + self.gyroSensitivityRoll = [decoder containsValueForKey:@"gyroSensitivityRoll"] ? [decoder decodeFloatForKey:@"gyroSensitivityRoll"] : 1.0; + self.accelSensitivityX = [decoder containsValueForKey:@"accelSensitivityX"] ? [decoder decodeFloatForKey:@"accelSensitivityX"] : 1.0; + self.accelSensitivityY = [decoder containsValueForKey:@"accelSensitivityY"] ? [decoder decodeFloatForKey:@"accelSensitivityY"] : 1.0; + self.accelSensitivityZ = [decoder containsValueForKey:@"accelSensitivityZ"] ? [decoder decodeFloatForKey:@"accelSensitivityZ"] : 1.0; } return self; } +- (nonnull id)mutableCopyWithZone:(nullable NSZone *)zone { + OSCProfile *copy = [[[self class] allocWithZone:zone] init]; + copy.name = [self.name mutableCopy]; // NSString → NSMutableString + copy.buttonStatesEncoded = [[NSMutableArray alloc] initWithArray:self.buttonStatesEncoded copyItems:YES]; + copy.isSelected = self.isSelected; + copy.mapGyroTo = self.mapGyroTo; + copy.yawPitchToRightStick = self.yawPitchToRightStick; + copy.rollToLeftStick = self.rollToLeftStick; + copy.gyroSensitivityYaw = self.gyroSensitivityYaw; + copy.gyroSensitivityPitch = self.gyroSensitivityPitch; + copy.gyroSensitivityRoll = self.gyroSensitivityRoll; + copy.accelSensitivityX = self.accelSensitivityX; + copy.accelSensitivityY = self.accelSensitivityY; + copy.accelSensitivityZ = self.accelSensitivityZ; + return copy; +} + @end diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.h b/VoidLink/Input/CustomOSC/OSCProfilesManager.h index d83897d1..11f994bc 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.h +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.h @@ -45,10 +45,10 @@ NS_ASSUME_NONNULL_BEGIN * Returns the index of the 'selected' profile within the array it's in */ - (NSInteger) getIndexOfSelectedProfile; -- (void)replaceSelectedProfileWith:(OSCProfile*)newProfile; +- (void)replaceSelectedProfileWith:(OSCProfile*)newProfile overwriteDefault:(bool)overwriteDefault; - (NSMutableArray *) getEncodedProfiles; -- (NSData* )getSelectedEncodedProfile; +// - (NSData* )getSelectedEncodedProfile; - (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded; - (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data; - (void) importDefaultTemplates; @@ -62,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Saves a profile object with a particular 'name' and an array of button layers (the CALayer button layers are the objects currently visible on screen) to persistent storage */ -- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)buttonLayers; +- (void) duplicateSelectedProfileWithName:(NSString*)name; - (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers; /** diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m index c1e581c4..89f62b53 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m @@ -25,7 +25,7 @@ static CGRect layoutViewBounds; + (OSCProfilesManager *) sharedManager:(CGRect)viewBounds { static OSCProfilesManager *_sharedManager = nil; static dispatch_once_t onceToken; - layoutViewBounds = viewBounds; + if(!CGRectEqualToRect(viewBounds, CGRectZero)) layoutViewBounds = viewBounds; // NSLog(@"bounds width: %f, height: %f", layoutViewBounds.size.width, layoutViewBounds.size.height); dispatch_once(&onceToken, ^{ _sharedManager = [[self alloc] init]; @@ -105,7 +105,7 @@ static CGRect layoutViewBounds; } -- (void)replaceSelectedProfileWith:(OSCProfile*)newProfile{ +- (void)replaceSelectedProfileWith:(OSCProfile*)newProfile overwriteDefault:(bool)overwriteDefault{ NSInteger index = 0; NSMutableArray *profiles = [self getAllProfiles]; for (OSCProfile *profile in profiles) { @@ -114,6 +114,7 @@ static CGRect layoutViewBounds; } } if(index>0) profiles[index] = newProfile; + else if(overwriteDefault) profiles[index] = newProfile; NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil]; @@ -226,14 +227,16 @@ static CGRect layoutViewBounds; - (OSCProfile *) getSelectedProfile { NSMutableArray *profiles = [self getAllProfiles]; - + if(profiles.count == 0){ + [self importDefaultTemplates]; + profiles = [self getAllProfiles]; + } for (OSCProfile *profile in profiles) { - if (profile.isSelected) { return profile; } } - return nil; + return profiles[0]; } - (NSInteger) getIndexOfSelectedProfile { @@ -274,47 +277,40 @@ static CGRect layoutViewBounds; - (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers { NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers]; if([self getIndexOfSelectedProfile]==0) return false; + /* OSCProfile *newProfile = [[OSCProfile alloc] initWithName:[self getSelectedProfile].name buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view - - - [self replaceSelectedProfileWith:newProfile]; + */ + OSCProfile *selectedProfile = [self getSelectedProfile]; + selectedProfile.buttonStatesEncoded = buttonStatesEncoded; + [self replaceSelectedProfileWith:selectedProfile overwriteDefault:NO]; return true; } -- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)oscButtonLayers { - NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers]; - OSCProfile *newProfile = [[OSCProfile alloc] initWithName:name - buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view - /* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */ - NSMutableArray *profiles = [self getAllProfiles]; - for (OSCProfile *profile in profiles) { - - profile.isSelected = NO; - } - - if ([self profileNameAlreadyExist:name]) { // if a saved profile with the same 'name' already exists in persistent storage then overwrite it and save the change to persistent storage - [self replaceProfile:[self OSCProfileWithName:name] withProfile:newProfile]; - } +- (void) duplicateSelectedProfileWithName:(NSString*)name { + if ([self profileNameAlreadyExist:name]) return; else { // otherwise encode then add the new profile to the end of the OSCProfiles array - NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil]; - NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; - [profilesEncoded addObject:newProfileEncoded]; - - /* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */ - NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil]; - [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"]; - [[NSUserDefaults standardUserDefaults] synchronize]; - - // saving test: - /* - NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; - NSString *path = [documentsPath stringByAppendingPathComponent:@"newDefault.bin"]; - - [profiles writeToFile:path atomically:YES]; - NSLog(@"默认 Profile 数据已保存到: %@", path); - */ + NSMutableArray *profiles = [self getAllProfiles]; + OSCProfile* newProfile = nil; + for(OSCProfile* profile in profiles){ + if(profile.isSelected){ + profile.isSelected = false; + newProfile = [profile mutableCopy]; + newProfile.isSelected = true; + newProfile.name = name; + } + } + if(newProfile){ + NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil]; + NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; + [profilesEncoded addObject:newProfileEncoded]; + + /* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */ + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil]; + [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + } } } diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.m b/VoidLink/Input/CustomOSC/OnScreenButtonState.m index 8d0dd176..46844589 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.m +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.m @@ -70,10 +70,8 @@ self.isHidden = [decoder decodeBoolForKey:@"isHidden"]; self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"]; self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"]; - self.sensitivityFactorX = [decoder decodeFloatForKey:@"sensitivityFactorX"]; - self.sensitivityFactorX = self.sensitivityFactorX == 0 ? 1.0 : self.sensitivityFactorX; - self.sensitivityFactorY = [decoder decodeFloatForKey:@"sensitivityFactorY"]; - self.sensitivityFactorY = self.sensitivityFactorY == 0 ? 1.0 : self.sensitivityFactorY; + self.sensitivityFactorX = [decoder containsValueForKey:@"sensitivityFactorX"] ? [decoder decodeFloatForKey:@"sensitivityFactorX"] : 1.0; + self.sensitivityFactorY = [decoder containsValueForKey:@"sensitivityFactorY"] ? [decoder decodeFloatForKey:@"sensitivityFactorY"] : 1.0; self.decelerationRate = [decoder decodeFloatForKey:@"decelerationRate"]; self.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"]; self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"]; diff --git a/VoidLink/Input/MotionHandler.swift b/VoidLink/Input/MotionHandler.swift index 33455fb6..0086b6ee 100644 --- a/VoidLink/Input/MotionHandler.swift +++ b/VoidLink/Input/MotionHandler.swift @@ -15,7 +15,7 @@ import CoreMotion func gyroMixInputStarted() -> Bool } -@objc class MotionHandler: NSObject, OscInstanceProviderDelegate, OnScreenWidgetStickMixedInputDelegate{ +@objc class MotionHandler: NSObject, OscInstanceReceiverDelegate, OnScreenWidgetStickMixedInputDelegate{ func mixRightStickAndGyroInput(x: CGFloat, y: CGFloat) { rightStickTouchInputX = x rightStickTouchInputY = y @@ -27,7 +27,7 @@ import CoreMotion } func gyroMixInputStarted() -> Bool { - return gyroControlStarted + return gyroIsWorking } func getOnScreenControlsInstance(_ sender: Any!) { @@ -42,10 +42,19 @@ import CoreMotion static let shared = MotionHandler() @objc class func sharedInstance() -> MotionHandler { - return MotionHandler.shared + let sharedInstance = MotionHandler.shared + sharedInstance.oscProfile = sharedInstance.oscProfileMan.getSelectedProfile() + sharedInstance.sensitvityYaw = sharedInstance.oscProfile.gyroSensitivityYaw + sharedInstance.sensitvityPitch = sharedInstance.oscProfile.gyroSensitivityPitch + sharedInstance.sensitvityRoll = sharedInstance.oscProfile.gyroSensitivityRoll + return sharedInstance } - + + private var oscProfileMan: OSCProfilesManager + private var oscProfile: OSCProfile + private var gyroControlStarted: Bool = false + private var gyroIsWorking: Bool = false private var accelControlStarted: Bool = false private let motionManager = CMMotionManager() public var sensitvityYaw:CGFloat = 1.0 @@ -57,15 +66,26 @@ import CoreMotion public let stickMaxOffset: CGFloat = 0x7FFE private var stickInputScale: CGFloat = 35 - var yaw:Double = 0 - var pitch:Double = 0 - var roll:Double = 0 + private var yaw:Double = 0 + private var pitch:Double = 0 + private var roll:Double = 0 - var rightStickTouchInputX:Double = 0 - var rightStickTouchInputY:Double = 0 + private var isCalibrating: Bool = false + private var sumX: Double = 0 + private var sumY: Double = 0 + private var sumZ: Double = 0 + @objc public var gyroBiasX: Double = 0 + @objc public var gyroBiasY: Double = 0 + @objc public var gyroBiasZ: Double = 0 + private var yawBias:Double = 0 + private var pitchBias:Double = 0 + private var rollBias:Double = 0 + + private var rightStickTouchInputX:Double = 0 + private var rightStickTouchInputY:Double = 0 - var leftStickTouchInputX:Double = 0 - var leftStickTouchInputY:Double = 0 + private var leftStickTouchInputX:Double = 0 + private var leftStickTouchInputY:Double = 0 var updateInterval: TimeInterval = 1.0 / 120.0 { didSet { @@ -75,6 +95,8 @@ import CoreMotion } @objc public override init() { + self.oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) + self.oscProfile = oscProfileMan.getSelectedProfile() super.init() if #available(iOS 13.0, *) { let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene @@ -112,13 +134,16 @@ import CoreMotion } /// 停止更新 - @objc public func stopGyroUpdate() { + private var needInterruption:Bool = false + + @objc public func stopGyroUpdate(interruption:Bool=false) { gyroControlStarted = false + gyroIsWorking = false + needInterruption = interruption if motionManager.isGyroActive{ motionManager.stopGyroUpdates() } - self.onScreenControls.clearLeftStickTouchPadFlag() - self.onScreenControls.clearRightStickTouchPadFlag() + self.clearGyroInput(interruption: needInterruption) } @objc public func stopAccelUpdate() { @@ -126,14 +151,13 @@ import CoreMotion if motionManager.isAccelerometerActive { motionManager.stopAccelerometerUpdates() } - self.onScreenControls.clearLeftStickTouchPadFlag() - self.onScreenControls.clearRightStickTouchPadFlag() + // self.onScreenControls.clearLeftStickTouchPadFlag() + // self.onScreenControls.clearRightStickTouchPadFlag() } // MARK: - 私有方法处理数据 private func handleAccelerometerData(x: Double, y: Double, z: Double) { // 在这里处理加速度数据,比如计算方向、存储或驱动逻辑 - //print("加速度计: x=\(x), y=\(y), z=\(z)") } private func handleGyroData(x: Double, y: Double, z: Double) { @@ -142,67 +166,109 @@ import CoreMotion var pitchSource:Double = 0 var rollSource:Double = 0 + /* + let correctedX:Double = x - gyroBiasX + let correctedY:Double = y - gyroBiasY + let correctedZ:Double = z - gyroBiasZ */ + if #available(iOS 13.0, *) { let orientation = (windowScene as! UIWindowScene).interfaceOrientation - print("界面方向变化: \(orientation)") switch orientation { case .landscapeLeft: - yawSource = x - pitchSource = -y - rollSource = -z + yawBias = gyroBiasX + pitchBias = -gyroBiasY + rollBias = -gyroBiasZ + yawSource = x-yawBias + pitchSource = -y-pitchBias + rollSource = -z-rollBias case .landscapeRight: - yawSource = -x - pitchSource = y - rollSource = -z + yawBias = -gyroBiasX + pitchBias = gyroBiasY + rollBias = -gyroBiasZ + yawSource = -x-yawBias + pitchSource = y-pitchBias + rollSource = -z-rollBias case .portrait: - yawSource = -y - pitchSource = -x - rollSource = -z + yawBias = -gyroBiasY + pitchBias = -gyroBiasX + rollBias = -gyroBiasZ + yawSource = -y-yawBias + pitchSource = -x-pitchBias + rollSource = -z-rollBias case .portraitUpsideDown: - yawSource = y - pitchSource = x - rollSource = -z + yawBias = gyroBiasY + pitchBias = gyroBiasX + rollBias = -gyroBiasZ + yawSource = y-yawBias + pitchSource = x-pitchBias + rollSource = -z-rollBias default: - yawSource = x - pitchSource = -y - rollSource = -z + yawBias = gyroBiasX + pitchBias = -gyroBiasY + rollBias = -gyroBiasZ + yawSource = x-yawBias + pitchSource = -y-pitchBias + rollSource = -z-rollBias } } else { - yawSource = x - pitchSource = -y - rollSource = z + let orientation = UIApplication.shared.statusBarOrientation + if orientation.isLandscape { + yawBias = gyroBiasX + pitchBias = -gyroBiasY + rollBias = -gyroBiasZ + yawSource = x-yawBias + pitchSource = -y-pitchBias + rollSource = -z-rollBias + } else if orientation.isPortrait { + yawBias = -gyroBiasY + pitchBias = -gyroBiasX + rollBias = -gyroBiasZ + yawSource = -y-yawBias + pitchSource = -x-pitchBias + rollSource = -z-rollBias + } } if !gyroControlStarted { - self.onScreenControls.clearLeftStickTouchPadFlag() - self.onScreenControls.clearRightStickTouchPadFlag() + self.clearGyroInput(interruption: needInterruption) print("Gyro: stopped") return } - if true { - yaw = rightStickTouchInputX + gyroInputToStickInput(input:yawSource*sensitvityYaw*10) - yaw = self.clampStickInput(input: yaw) - pitch = rightStickTouchInputY - gyroInputToStickInput(input:pitchSource*sensitvityPitch*10) - pitch = self.clampStickInput(input: pitch) - self.onScreenControls.sendRightStickTouchPadEvent(yaw, pitch) - } - - if true { - roll = leftStickTouchInputX + gyroInputToStickInput(input:rollSource*sensitvityRoll*10) - roll = self.clampStickInput(input: roll) - self.onScreenControls.sendLeftStickTouchPadEvent(roll, leftStickTouchInputY) - } + gyroIsWorking = true - if false { + if oscProfile.mapGyroTo == MapGyroTo.mapGyroToMouse { yaw = yawSource*sensitvityYaw*30 pitch = pitchSource*sensitvityPitch*30 LiSendMouseMoveEvent(Int16(yaw),Int16(pitch)) } - if false { - roll = rollSource*sensitvityPitch*100 - LiSendMouseMoveEvent(Int16(roll),0) + if oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick { + if oscProfile.yawPitchToRightStick { + yaw = rightStickTouchInputX + gyroInputToStickInput(input:yawSource*sensitvityYaw*10) + yaw = self.clampStickInput(input: yaw) + pitch = rightStickTouchInputY - gyroInputToStickInput(input:pitchSource*sensitvityPitch*10) + pitch = self.clampStickInput(input: pitch) + self.onScreenControls.sendRightStickTouchPadEvent(yaw, pitch) + } + if oscProfile.rollToLeftStick { + roll = leftStickTouchInputX + gyroInputToStickInput(input:rollSource*sensitvityRoll*10) + roll = self.clampStickInput(input: roll) + self.onScreenControls.sendLeftStickTouchPadEvent(roll, leftStickTouchInputY) + } + } +} + + private func clearGyroInput(interruption:Bool){ + if oscProfile.yawPitchToRightStick{ + self.onScreenControls.sendRightStickTouchPadEvent(rightStickTouchInputX-yawBias, rightStickTouchInputY-pitchBias) + } + if oscProfile.rollToLeftStick{ + self.onScreenControls.sendLeftStickTouchPadEvent(leftStickTouchInputX-rollBias, leftStickTouchInputY) + } + if(interruption){ + self.onScreenControls.clearLeftStickTouchPadFlag() + self.onScreenControls.clearRightStickTouchPadFlag() } } @@ -213,4 +279,33 @@ import CoreMotion private func gyroInputToStickInput(input: CGFloat) -> CGFloat{ return self.clampStickInput(input: stickMaxOffset * input / 8) } + + @objc public func calibrateGyroBias(duration: TimeInterval = 5.0, completion: @escaping () -> Void) { + guard motionManager.isGyroAvailable else { return } + + isCalibrating = true + var sumX = 0.0, sumY = 0.0, sumZ = 0.0 + var sampleCount = 0 + + motionManager.startGyroUpdates(to: .main) { [weak self] gyroData, _ in + guard let self = self, self.isCalibrating, let data = gyroData else { return } + sumX += data.rotationRate.x + sumY += data.rotationRate.y + sumZ += data.rotationRate.z + sampleCount += 1 + } + + // 5秒后计算平均值 + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in + guard let self = self else { return } + self.motionManager.stopGyroUpdates() + if sampleCount > 0 { + gyroBiasX = sumX / Double(sampleCount) + gyroBiasY = sumY / Double(sampleCount) + gyroBiasZ = sumZ / Double(sampleCount) + } + self.isCalibrating = false + completion() + } + } } diff --git a/VoidLink/Input/OnScreenControls.h b/VoidLink/Input/OnScreenControls.h index 69a2749b..5f9c1938 100644 --- a/VoidLink/Input/OnScreenControls.h +++ b/VoidLink/Input/OnScreenControls.h @@ -19,7 +19,7 @@ @class StreamConfiguration; // sending self as an instance to OnScreenWidgetView or other related classes -@protocol OscInstanceProviderDelegate +@protocol OscInstanceReceiverDelegate - (void)getOnScreenControlsInstance:(id)sender; // Renamed delegate method @end @@ -54,7 +54,7 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) { -@property (nonatomic, weak) id instanceReceiverDelegate; // Delegate property +@property (nonatomic, weak) id instanceReceiverDelegate; // Delegate property - (void)sendInstance; // @property (nonatomic, assign) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrols class for areVirtualControllerTaps flag setting diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index cd6cf92d..c00bc7d6 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -8,7 +8,7 @@ import UIKit -@objc class OnScreenWidgetView: UIView, OscInstanceProviderDelegate { +@objc class OnScreenWidgetView: UIView, OscInstanceReceiverDelegate { // receiving the OnScreenControls instance from delegate @objc func getOnScreenControlsInstance(_ sender: Any) { if let controls = sender as? OnScreenControls { @@ -35,8 +35,8 @@ import UIKit @objc protocol OnScreenFunctionalButtonDelegate: AnyObject { func startGyroUpdate() func startAccelUpdate() - func stopGyroUpdate() - func stopAccelUpdate() + func stopGyroUpdate(interruption:Bool) + func stopAccelUpdate(interruption:Bool) func openWidgetLayoutTool() func switchWidgetProfile() func bringUpSoftKeyboard() @@ -51,6 +51,9 @@ import UIKit case touchPad } + private let oscProfileMan: OSCProfilesManager = OSCProfilesManager.sharedManager(CGRectZero) + private var oscProfile: OSCProfile + @objc public var widgetType: WidgetTypeEnum = WidgetTypeEnum.uninitialized @objc static public var editMode: Bool = false @@ -290,6 +293,7 @@ import UIKit self.pointerIdPool.insert(UInt32(i)) } self.activePointerIds = [] + self.oscProfile = oscProfileMan.getSelectedProfile() super.init(frame: .zero) upIndicator = createLrudDirectionLayer() @@ -1189,12 +1193,13 @@ import UIKit // vertical input must be inverted targetX = (targetX >= 0 ? 1.0 : -1.0) * self.minStickOffset + (self.stickMaxOffset - self.minStickOffset) * (targetX/self.stickMaxOffset) targetY = (targetY >= 0 ? 1.0 : -1.0) * self.minStickOffset + (self.stickMaxOffset - self.minStickOffset) * (targetY/self.stickMaxOffset) - if self.mixInputDelegate?.gyroMixInputStarted() != true { + + let mixRightStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick + && oscProfile.yawPitchToRightStick) + if !mixRightStickInputToGyro || (self.mixInputDelegate?.gyroMixInputStarted() != true) { self.onScreenControls.sendRightStickTouchPadEvent(targetX, targetY) } - else{ - self.mixInputDelegate?.mixRightStickAndGyroInput(x: targetX, y: targetY) - } + self.mixInputDelegate?.mixRightStickAndGyroInput(x: targetX, y: targetY) } private func sendLeftStickTouchPadEvent(inputX: CGFloat, inputY: CGFloat){ @@ -1202,12 +1207,13 @@ import UIKit var targetY = -self.touchInputToStickInput(input: inputY) targetX = (targetX >= 0 ? 1.0 : -1.0) * self.minStickOffset + (self.stickMaxOffset - self.minStickOffset) * (targetX/self.stickMaxOffset) targetY = (targetY >= 0 ? 1.0 : -1.0) * self.minStickOffset + (self.stickMaxOffset - self.minStickOffset) * (targetY/self.stickMaxOffset) - if self.mixInputDelegate?.gyroMixInputStarted() != true { + + let mixLeftStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick + && oscProfile.rollToLeftStick) + if !mixLeftStickInputToGyro || (self.mixInputDelegate?.gyroMixInputStarted() != true) { self.onScreenControls.sendLeftStickTouchPadEvent(targetX, targetY) } - else{ - self.mixInputDelegate?.mixLeftStickAndGyroInput(x: targetX, y: targetY) - } + self.mixInputDelegate?.mixLeftStickAndGyroInput(x: targetX, y: targetY) } //========================================================================================================== @@ -1656,33 +1662,33 @@ import UIKit if longPressed, buttonMode == ButtonMode.movable.rawValue {break} NotificationCenter.default.post(name: Notification.Name("SettingsOverlayButtonPressedNotification"), object:nil) // inform layout tool controller to fetch button size factors. self will be passed as the object of the notification case "GYRO": - self.functionalButtonDelegate?.stopGyroUpdate() + self.functionalButtonDelegate?.stopGyroUpdate(interruption: false) case "ACCEL": - self.functionalButtonDelegate?.stopAccelUpdate() + self.functionalButtonDelegate?.stopAccelUpdate(interruption: false) case "MOTION": - self.functionalButtonDelegate?.stopGyroUpdate() - self.functionalButtonDelegate?.stopAccelUpdate() + self.functionalButtonDelegate?.stopGyroUpdate(interruption: false) + self.functionalButtonDelegate?.stopAccelUpdate(interruption: false) default: break } } private func clearRightStickTouchPadFlag(){ - if self.mixInputDelegate?.gyroMixInputStarted() != true { + let mixRightStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick + && oscProfile.yawPitchToRightStick) + if !mixRightStickInputToGyro || self.mixInputDelegate?.gyroMixInputStarted() != true { self.onScreenControls.clearRightStickTouchPadFlag() } - else{ - self.mixInputDelegate?.mixRightStickAndGyroInput(x: 0, y: 0) - } + self.mixInputDelegate?.mixRightStickAndGyroInput(x: 0, y: 0) } private func clearLeftStickTouchPadFlag(){ - if self.mixInputDelegate?.gyroMixInputStarted() != true { + let mixLeftStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick + && oscProfile.rollToLeftStick) + if !mixLeftStickInputToGyro || self.mixInputDelegate?.gyroMixInputStarted() != true { self.onScreenControls.clearLeftStickTouchPadFlag() } - else{ - self.mixInputDelegate?.mixLeftStickAndGyroInput(x: 0, y: 0) - } + self.mixInputDelegate?.mixLeftStickAndGyroInput(x: 0, y: 0) } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { @@ -1844,11 +1850,11 @@ import UIKit override func didMoveToSuperview() { super.didMoveToSuperview() if superview == nil { - if self.functionalButtonString == "GYRO" {self.functionalButtonDelegate?.stopGyroUpdate()} - if self.functionalButtonString == "ACCEL" {self.functionalButtonDelegate?.stopAccelUpdate()} + if self.functionalButtonString == "GYRO" {self.functionalButtonDelegate?.stopGyroUpdate(interruption: true)} + if self.functionalButtonString == "ACCEL" {self.functionalButtonDelegate?.stopAccelUpdate(interruption: true)} if self.functionalButtonString == "MOTION" { - self.functionalButtonDelegate?.stopGyroUpdate() - self.functionalButtonDelegate?.stopAccelUpdate() + self.functionalButtonDelegate?.stopGyroUpdate(interruption: true) + self.functionalButtonDelegate?.stopAccelUpdate(interruption: true) } buttonDownVisualEffectLayer.borderWidth = 0 buttonDownVisualEffectLayer.borderColor = defaultBorderColor diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index cd568721..0de90829 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -76,7 +76,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; UIKeyModifierFlags comboKeyModifierFlags; WidgetSizeTransition _widgetSizeTransition; - OSCProfilesManager* profilesManager; + OSCProfilesManager* oscProfileMan; } - (void) setupStreamView:(ControllerSupport*)controllerSupport @@ -499,21 +499,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; if(relocatedWidgetDict.allKeys.count == 0) return; - profilesManager = [OSCProfilesManager sharedManager:self->streamFrameTopLayerView.bounds]; - OSCProfile *newProfile = [profilesManager getSelectedProfile]; + oscProfileMan = [OSCProfilesManager sharedManager:self->streamFrameTopLayerView.bounds]; + OSCProfile *newProfile = [oscProfileMan getSelectedProfile]; for (NSInteger i = 0; i < newProfile.buttonStatesEncoded.count; i++) { NSData* buttonStateEncoded = newProfile.buttonStatesEncoded[i]; - OnScreenButtonState *newButtonState = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; + OnScreenButtonState *newButtonState = [oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded]; if([relocatedWidgetDict.allKeys containsObject:newButtonState.identifier]){ OnScreenWidgetView* widget = [relocatedWidgetDict objectForKey:newButtonState.identifier]; - newButtonState.position = [profilesManager normalizeWidgetPosition:widget.center]; + newButtonState.position = [oscProfileMan normalizeWidgetPosition:widget.center]; NSData *newButtonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:newButtonState requiringSecureCoding:YES error:nil]; newProfile.buttonStatesEncoded[i] = newButtonStateEncoded; } } - [profilesManager replaceSelectedProfileWith:newProfile]; + [oscProfileMan replaceSelectedProfileWith:newProfile overwriteDefault:NO]; } - (void) reloadOnScreenWidgetViews{ @@ -532,13 +532,14 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; OnScreenWidgetView.buttonVisualFeedbackEnabled = settings.buttonVisualFeedback; - if(!profilesManager) profilesManager = [OSCProfilesManager sharedManager:self->streamFrameTopLayerView.bounds]; - OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile - + if(!oscProfileMan) oscProfileMan = [OSCProfilesManager sharedManager:self->streamFrameTopLayerView.bounds]; + OSCProfile *oscProfile = [oscProfileMan getSelectedProfile]; //returns the currently selected OSCProfile + MotionHandler* motionHandler = [MotionHandler sharedInstance]; + if(!OnScreenWidgetView.editMode){ // in edit mode, keyboard widget view will be updated within layoutool view controller. for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { - OnScreenButtonState* buttonState = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; + OnScreenButtonState* buttonState = [oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded]; if(buttonState.widgetType == CustomOnScreenWidget){ OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape]; //reconstruct widgetView //-------------------------------------------------- @@ -547,7 +548,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; //-------------------------------------------------- widgetView.functionalButtonDelegate = (id)streamFrameVC; - widgetView.mixInputDelegate = (id) [MotionHandler sharedInstance]; + widgetView.mixInputDelegate = (id)motionHandler; widgetView.identifier = buttonState.identifier; widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place diff --git a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents index 0d3b1607..a156178f 100644 --- a/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents +++ b/VoidLink/Limelight.xcdatamodeld/VoidLink v1.0.xcdatamodel/contents @@ -38,6 +38,9 @@ + + + diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index e14208de..7fed9c97 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -1025,6 +1025,29 @@ } } }, + "Calibrating..." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在校准…" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在校準…" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在校準…" + } + } + } + }, "Cancel" : { "extractionState" : "manual", "localizations" : { @@ -1583,6 +1606,29 @@ } } }, + "Drift Correction" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + } + } + }, "Edit" : { "extractionState" : "manual", "localizations" : { @@ -2027,6 +2073,29 @@ } } }, + "Finished!" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成!" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成!" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成!" + } + } + } + }, "First Launch Tips" : { "extractionState" : "manual", "localizations" : { @@ -3013,6 +3082,29 @@ } } }, + "Motion Control" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "体感控制" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "體感控制" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "體感控制" + } + } + } + }, "Mouse Pointer Velocity: %d%%" : { "extractionState" : "manual", "localizations" : { @@ -3843,6 +3935,29 @@ } } }, + "Place the device flat on a surface and keep it still, then tap ‘Start’." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "将设备静置于平面上,再点“开始”。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "將裝置靜置於平面上,然後點「開始」。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "將裝置靜置於平面上,然後點「開始」。" + } + } + } + }, "Pofiles imported" : { "extractionState" : "manual", "localizations" : { @@ -5094,6 +5209,29 @@ } } }, + "Start" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开始" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "開始" + } + } + } + }, "Stereo (surround sound available for iOS18+)" : { "extractionState" : "manual", "localizations" : { @@ -5169,6 +5307,29 @@ } } }, + "Tap 'OK' to set on-screen widget to 'Custom' and enable motion control." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "点选 ‘确定’ 开启自定义屏显控件与体感控制。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "點選「確定」以開啟自訂螢幕小工具與體感控制。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "點選「確定」以開啟自訂螢幕小工具與體感控制。" + } + } + } + }, "Tap %d Fingers to Change OSC Layout in Stream View" : { "extractionState" : "manual", "localizations" : { diff --git a/VoidLink/MenuSectionView.h b/VoidLink/MenuSectionView.h index 00d7ee20..827944d4 100644 --- a/VoidLink/MenuSectionView.h +++ b/VoidLink/MenuSectionView.h @@ -22,11 +22,14 @@ @property (class, nonatomic, assign) BOOL overridePersistedFoldState; // 外部可访问属性 +@property (nonatomic, strong) UIStackView *rootStackView; @property (nonatomic, assign) CGFloat leadingTrailingPadding; @property (nonatomic, assign) CGFloat separatorLinePadding; @property (nonatomic, copy) NSString *sectionTitle; @property (nonatomic, copy) NSString *identifier; @property (nonatomic, assign) BOOL isExpanded; +@property (nonatomic, assign) BOOL expandable; +@property (nonatomic, copy) void (^lockedSectionHandler)(void); @property (nonatomic, strong) UIColor *backgroundColor; @property (nonatomic, assign) CGFloat rootStackViewSpacing; @property (nonatomic, assign) CGFloat headerViewHeight; diff --git a/VoidLink/MenuSectionView.m b/VoidLink/MenuSectionView.m index 3edc396f..d9046b79 100644 --- a/VoidLink/MenuSectionView.m +++ b/VoidLink/MenuSectionView.m @@ -13,7 +13,6 @@ @interface MenuSectionView () -@property (nonatomic, strong) UIStackView *rootStackView; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIButton *toggleButton; @property (nonatomic, strong) UIButton *toggleArea; @@ -57,6 +56,7 @@ static BOOL overridePersistedFoldState = YES; _separatorLinePadding = 40; _sectionTitle = @"Section"; _isExpanded = YES; + _expandable = true; _backgroundColor = [UIColor clearColor]; _rootStackViewSpacing = [self isIPhone] ? 10 : 13.8; _subStackViews = [NSMutableArray array]; @@ -235,7 +235,7 @@ static BOOL overridePersistedFoldState = YES; } - (void)setExpanded:(BOOL)isExpanded { - self.isExpanded = isExpanded; + self.isExpanded = !_expandable ? false : isExpanded; [self updateViewForFoldState]; [[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier]; [[NSUserDefaults standardUserDefaults] synchronize]; @@ -323,8 +323,9 @@ static BOOL overridePersistedFoldState = YES; } - (void)toggleFold { - self.isExpanded = !self.isExpanded; + self.isExpanded = !_expandable ? false : !self.isExpanded; [self updateViewForFoldState]; + if(!_expandable) self.lockedSectionHandler(); [[NSUserDefaults standardUserDefaults] setBool:self.isExpanded forKey:self.identifier]; [[NSUserDefaults standardUserDefaults] synchronize]; } diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index 82442b69..1609a629 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -1270,10 +1270,12 @@ self->_oscProfilesTableViewController = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"]; //this part is just for registration, will not be immediately executed. + __weak typeof(self) weakSelf = self; self->_oscProfilesTableViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'OSCProfilesTableViewController' VC is dismissed. By the time the 'OSCProfilesTableViewController' VC is dismissed the user would have potentially selected a different OSC profile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the profile and then hide/show and move each OSC button to their appropriate position - NSLog(@"profile profile"); - [self reloadLegacyOnScreenControls]; - self->_oscProfilesTableViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayers; //pass updated OSCLayout to OSCProfileTableView again + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + [weakSelf reloadLegacyOnScreenControls]; + strongSelf->_oscProfilesTableViewController.currentOSCButtonLayers = weakSelf.layoutOSC.OSCButtonLayers; //pass updated OSCLayout to OSCProfileTableView again }; [self.oscProfilesTableViewController profileViewRefresh]; // execute this will make sure OSCLayout is updated from persisted profile, not any cache. @@ -1298,8 +1300,9 @@ _oscProfilesTableViewController = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"]; _oscProfilesTableViewController.layoutViewBounds = self.view.bounds; + __weak typeof(self) weakSelf = self; _oscProfilesTableViewController.needToUpdateOscLayoutTVC = ^() { // a block that will be called when the modally presented 'OSCProfilesTableViewController' VC is dismissed. By the time the 'OSCProfilesTableViewController' VC is dismissed the user would have potentially selected a different OSC ofile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the proffile and then hide/show and move each OSC button to their appropriate position - [self reloadLegacyOnScreenControls]; + [weakSelf reloadLegacyOnScreenControls]; }; self.widgetPanelStack.hidden = YES; diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m index 7e9ece99..bf23f9b9 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m @@ -159,8 +159,8 @@ const double NAV_BAR_HEIGHT = 50; [self presentViewController:savedAlertController animated:YES completion:nil]; } else { // if user entered a valid name that doesn't already exist then save the profile to persistent storage - [self->profilesManager saveProfileWithName: enteredProfileName andButtonLayers:self.currentOSCButtonLayers]; // the OSC layout here is passed from parent LayoutOSCViewController; - [self->profilesManager setProfileToSelected: [self->profilesManager getIndexOfLastProfile]]; + [self->profilesManager duplicateSelectedProfileWithName:enteredProfileName]; // the OSC layout here is passed from parent LayoutOSCViewController; + // [self->profilesManager setProfileToSelected: [self->profilesManager getIndexOfLastProfile]]; UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Profile %@ duplicated from current layout", enteredProfileName] preferredStyle:UIAlertControllerStyleAlert]; // Let user know this profile has been duplicated & saved diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index 9974e5b8..85fb5f2a 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -147,6 +147,24 @@ @property (strong, nonatomic) IBOutlet UIStackView *rememberFoldStateStack; @property (strong, nonatomic) IBOutlet UISwitch *rememberFoldStateSwitch; +@property (strong, nonatomic) IBOutlet UIStackView *mapGyroToStack; +@property (strong, nonatomic) IBOutlet UISegmentedControl *mapGyroToSelector; + +@property (strong, nonatomic) IBOutlet UIStackView *gyroToStickSwitchStack; +@property (strong, nonatomic) IBOutlet UISwitch *yawPitchToRightStickSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *rollToLeftStickSwitch; + +@property (strong, nonatomic) IBOutlet UIStackView *yawPitchSensitivityStack; +@property (strong, nonatomic) IBOutlet UIStackView *yawSensitivityStack; +@property (strong, nonatomic) IBOutlet UISlider *yawSensitivitySlider; +@property (strong, nonatomic) IBOutlet UIStackView *pitchSensitivityStack; +@property (strong, nonatomic) IBOutlet UISlider *pitchSensitivitySlider; + +@property (strong, nonatomic) IBOutlet UIStackView *rollSensitivityStack; +@property (strong, nonatomic) IBOutlet UISlider *rollSensitivitySlider; + +@property (strong, nonatomic) IBOutlet UIStackView *testStack; + @property (nonatomic, strong) MicHandler *micHandler; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 3111e3ff..76d75a00 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -14,6 +14,7 @@ #import "ThemeManager.h" #import "Connection.h" #import "Plot.h" +#import "OSCProfilesManager.h" #import #import @@ -24,6 +25,8 @@ @implementation SettingsViewController { TemporarySettings* tempSettings; DataManager* dataMan; + OSCProfilesManager* oscProfileMan; + OSCProfile* oscProfile; NSLayoutConstraint *parentStackLeadingConstraint; NSLayoutConstraint *parentStackWidthConstraint; @@ -43,6 +46,7 @@ CGFloat _scrollSpeed; CGFloat _currentRefreshRate; MenuSectionView *touchAndControlSection; + MenuSectionView *motionControlSection; MenuSectionView *videoSection; MenuSectionView *otherSection; MenuSectionView *experimentalSection; @@ -342,6 +346,47 @@ BOOL isCustomResolution(int resolutionSelected) { NSLog(@"Mic error: %@", error); } +- (void)reloadMotionControlConfigs{ + oscProfile = [oscProfileMan getSelectedProfile]; + [self.mapGyroToSelector setSelectedSegmentIndex:oscProfile.mapGyroTo]; + [self mapGyroToChanged:self.mapGyroToSelector]; + if(self.mapGyroToSelector.selectedSegmentIndex == mapGyroToControllerStick){ + [self.yawPitchToRightStickSwitch setOn:oscProfile.yawPitchToRightStick]; + [self yawPitchToRightStickSwitchFlipped:self.yawPitchToRightStickSwitch]; + [self.rollToLeftStickSwitch setOn:oscProfile.rollToLeftStick]; + [self rollToLeftStickSwitchFlipped:self.rollToLeftStickSwitch]; + } + [self.yawSensitivitySlider setValue:[self map_SliderValue_fromVelocFactor:oscProfile.gyroSensitivityYaw]]; + [self yawSensitivitySliderMoved:self.yawSensitivitySlider]; + [self.pitchSensitivitySlider setValue:[self map_SliderValue_fromVelocFactor:oscProfile.gyroSensitivityPitch]]; + [self pitchSensitivitySliderMoved:self.pitchSensitivitySlider]; + [self.rollSensitivitySlider setValue:[self map_SliderValue_fromVelocFactor:oscProfile.gyroSensitivityRoll]]; + [self rollSensitivitySliderMoved:self.rollSensitivitySlider]; +} + +- (void)saveMotionControlConfigs{ + + CGFloat yawSensitivityPercent = [self map_velocFactorDisplay_fromSliderValue:self.yawSensitivitySlider.value]; + CGFloat pitchSensitivityPercent = [self map_velocFactorDisplay_fromSliderValue:self.pitchSensitivitySlider.value]; + CGFloat rollSensitivityPercent = [self map_velocFactorDisplay_fromSliderValue:self.rollSensitivitySlider.value]; + + bool configNotChanged = (oscProfile.mapGyroTo == self.mapGyroToSelector.selectedSegmentIndex + && oscProfile.yawPitchToRightStick == self.yawPitchToRightStickSwitch.isOn + && oscProfile.rollToLeftStick == self.rollToLeftStickSwitch.isOn + && (int16_t)(oscProfile.gyroSensitivityYaw*100) == (int16_t)yawSensitivityPercent + && (int16_t)(oscProfile.gyroSensitivityPitch*100) == (int16_t)pitchSensitivityPercent + && (int16_t)(oscProfile.gyroSensitivityRoll*100) == (int16_t)rollSensitivityPercent); + if(!configNotChanged){ + oscProfile.mapGyroTo = self.mapGyroToSelector.selectedSegmentIndex; + oscProfile.yawPitchToRightStick = self.yawPitchToRightStickSwitch.isOn; + oscProfile.rollToLeftStick = self.rollToLeftStickSwitch.isOn; + oscProfile.gyroSensitivityYaw = yawSensitivityPercent/100; + oscProfile.gyroSensitivityPitch = pitchSensitivityPercent/100; + oscProfile.gyroSensitivityRoll = rollSensitivityPercent/100; + [oscProfileMan replaceSelectedProfileWith:oscProfile overwriteDefault:YES]; + } +} + - (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:NO]; @@ -350,7 +395,6 @@ BOOL isCustomResolution(int resolutionSelected) { self.micHandler = [MicHandler new]; [self.micHandler startTapping]; */ - [self updateParentStackHorizontalConstraints]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -362,6 +406,11 @@ BOOL isCustomResolution(int resolutionSelected) { selector:@selector(updateTheme) name:ThemeDidChangeNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reloadMotionControlConfigs) + name:@"OscLayoutCloseNotification" + object:nil]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ if(self.mainFrameViewController.settingsExpandedInStreamView){ @@ -383,6 +432,8 @@ BOOL isCustomResolution(int resolutionSelected) { } if(![self manuallyChangedFPS]) [self framerateChanged]; + + [self reloadMotionControlConfigs]; } - (void)viewDidAppear:(BOOL)animated{ @@ -390,10 +441,10 @@ BOOL isCustomResolution(int resolutionSelected) { [self updateResolutionTable]; [self.customResolutionSwitch addTarget:self action:@selector(customResolutionSwitched:) forControlEvents:UIControlEventValueChanged]; - tempSettings = [dataMan getSettings]; - + self->tempSettings = [self->dataMan getSettings]; + //CGSize currentResolution = CGSizeMake(currentSettings.width.intValue, currentSettings.height.intValue); - [self.customResolutionSwitch setOn: isCustomResolution(tempSettings.resolutionSelected.intValue)]; + [self.customResolutionSwitch setOn: isCustomResolution(self->tempSettings.resolutionSelected.intValue)]; [self.resolutionSelector setEnabled:!self.customResolutionSwitch.isOn]; } @@ -648,6 +699,22 @@ BOOL isCustomResolution(int resolutionSelected) { [self addSetting:self.gyroSensitivityStack ofId:@"gyroSensitivityStack" withInfoTag:NO withDynamicLabel:YES to:touchAndControlSection]; [touchAndControlSection addToParentStack:_parentStack]; // [touchAndControlSection setExpanded:NO]; + + motionControlSection = [[MenuSectionView alloc] init]; + motionControlSection.delegate = self; + motionControlSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Motion Control"]; + motionControlSection.identifier = @"SettingsSectionMotionControl"; + if (@available(iOS 13.0, *)) { + // [motionControlSection setSectionWithIcon:[UIImage imageNamed:@"gyroscope"] andSize:23]; + [motionControlSection setSectionWithIcon:[UIImage imageNamed:@"gyroscope"] andSize:23]; + } + [self addSetting:self.mapGyroToStack ofId:@"mapGyroToStack" withInfoTag:NO withDynamicLabel:NO to:motionControlSection]; + [self addSetting:self.gyroToStickSwitchStack ofId:@"gyroToStickStack" withInfoTag:NO withDynamicLabel:NO to:motionControlSection]; + [self addSetting:self.yawPitchSensitivityStack ofId:@"yawPitchSensitivityStack" withInfoTag:NO withDynamicLabel:NO to:motionControlSection]; + [self addDynamicLabelForStack:self.yawSensitivityStack]; + [self addDynamicLabelForStack:self.pitchSensitivityStack]; + [self addSetting:self.rollSensitivityStack ofId:@"rollSensitivityStack" withInfoTag:NO withDynamicLabel:YES to:motionControlSection]; + [motionControlSection addToParentStack:_parentStack]; MenuSectionView *gesturesSection = [[MenuSectionView alloc] init]; gesturesSection.delegate = self; @@ -1399,347 +1466,387 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void)viewDidLoad { - settingStackWillBeRelocatedToLowestPosition = false; - hiddenStacks = [[NSMutableSet alloc] init]; - - [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"_UIConstraintBasedLayoutLogUnsatisfiable"]; - - UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; - [self.view addGestureRecognizer:longPress]; - - _settingStackDict = [[NSMutableDictionary alloc] init]; - - - for(UIView* view in self.view.subviews){ - [view removeFromSuperview]; - } - [self initParentStack]; + [UIView animateWithDuration:0 animations:^{ - // load rememberFoldState before section layout - dataMan = [[DataManager alloc] init]; - tempSettings = [dataMan getSettings]; - [self.rememberFoldStateSwitch setOn:tempSettings.rememberFoldState];// Load old setting - MenuSectionView.overridePersistedFoldState = !tempSettings.rememberFoldState; - [self.rememberFoldStateSwitch addTarget:self action:@selector(rememberFoldStateSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; - - [self layoutSections]; + self->oscProfileMan = [OSCProfilesManager sharedManager:CGRectZero]; + self->settingStackWillBeRelocatedToLowestPosition = false; + self->hiddenStacks = [[NSMutableSet alloc] init]; - // [self swi]; + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"_UIConstraintBasedLayoutLogUnsatisfiable"]; - self->slideToCloseSettingsViewRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)]; - slideToCloseSettingsViewRecognizer.edges = UIRectEdgeLeft; - slideToCloseSettingsViewRecognizer.normalizedThresholdDistance = 0.0; - slideToCloseSettingsViewRecognizer.EDGE_TOLERANCE = 10; - slideToCloseSettingsViewRecognizer.immediateTriggering = true; - slideToCloseSettingsViewRecognizer.delaysTouchesBegan = NO; - slideToCloseSettingsViewRecognizer.delaysTouchesEnded = NO; - [self.view addGestureRecognizer:slideToCloseSettingsViewRecognizer]; + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; + [self.view addGestureRecognizer:longPress]; - settingsViewJustLoaded = true; + self->_settingStackDict = [[NSMutableDictionary alloc] init]; - // Always run settings in dark mode because we want the light fonts - if (@available(iOS 13.0, tvOS 13.0, *)) { - self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; - } - - - currentSettingsMenuMode = tempSettings.settingsMenuMode.intValue; - [self loadFavoriteSettingStackIdentifiers]; - if(tempSettings.settingsMenuMode.intValue == FavoriteSettings) [self switchToFavoriteSettings]; - - // Ensure we pick a bitrate that falls exactly onto a slider notch - _bitrate = bitrateTable[[self getSliderValueForBitrate:[tempSettings.bitrate intValue]]]; - - // Get the size of the screen with and without safe area insets - // UIWindow *window = UIApplication.sharedApplication.windows.firstObject; - // CGFloat screenScale = window.screen.scale; - // CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale; - // CGFloat fullScreenWidth = window.frame.size.width * screenScale; - // CGFloat fullScreenHeight = window.frame.size.height * screenScale; - - [self.resolutionSelector removeSegmentAtIndex:0 animated:NO]; // remove 360p - [self.resolutionSelector removeSegmentAtIndex:5 animated:NO]; // remove custom segment - // iOS12 compatibility: - self.resolutionSelector.selectedSegmentIndex = 3; - [self.resolutionSelector setNeedsLayout]; - - resolutionTable[5] = CGSizeMake([tempSettings.width integerValue], [tempSettings.height integerValue]); // custom initial value - [self updateResolutionTable]; - - - NSInteger framerate; - switch ([tempSettings.framerate integerValue]) { - case 30: - framerate = 0; - break; - default: - case 60: - framerate = 1; - break; - case 120: - framerate = 2; - break; - } - - NSInteger resolution = tempSettings.resolutionSelected.integerValue; - if(resolution >= RESOLUTION_TABLE_SIZE){ - resolution = 0; - } - - // Only show the 120 FPS option if we have a > 60-ish Hz display - bool enable120Fps = false; - if (@available(iOS 10.3, tvOS 10.3, *)) { - if ([UIScreen mainScreen].maximumFramesPerSecond > 62) { - enable120Fps = true; + + for(UIView* view in self.view.subviews){ + [view removeFromSuperview]; } - } - if (!enable120Fps) { - [self.framerateSelector removeSegmentAtIndex:2 animated:NO]; - } - - // Disable codec selector segments for unsupported codecs - #if defined(__IPHONE_16_0) || defined(__TVOS_16_0) - if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) - #endif - { - [self.codecSelector removeSegmentAtIndex:2 animated:NO]; - } - if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) { - [self.codecSelector removeSegmentAtIndex:1 animated:NO]; - // Only enable the 4K option for "recent" devices. We'll judge that by whether - // they support HEVC decoding (A9 or later). - [self.resolutionSelector setEnabled:NO forSegmentAtIndex:2]; - } - switch (tempSettings.preferredCodec) { - case CODEC_PREF_AUTO: - [self.codecSelector setSelectedSegmentIndex:self.codecSelector.numberOfSegments - 1]; - break; + [self initParentStack]; + + // load rememberFoldState before section layout + self->dataMan = [[DataManager alloc] init]; + self->tempSettings = [self->dataMan getSettings]; + [self.rememberFoldStateSwitch setOn:self->tempSettings.rememberFoldState];// Load old setting + MenuSectionView.overridePersistedFoldState = !self->tempSettings.rememberFoldState; + [self.rememberFoldStateSwitch addTarget:self action:@selector(rememberFoldStateSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + + [self layoutSections]; + + + // [self swi]; + + self->slideToCloseSettingsViewRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)]; + self->slideToCloseSettingsViewRecognizer.edges = UIRectEdgeLeft; + self->slideToCloseSettingsViewRecognizer.normalizedThresholdDistance = 0.0; + self->slideToCloseSettingsViewRecognizer.EDGE_TOLERANCE = 10; + self->slideToCloseSettingsViewRecognizer.immediateTriggering = true; + self->slideToCloseSettingsViewRecognizer.delaysTouchesBegan = NO; + self->slideToCloseSettingsViewRecognizer.delaysTouchesEnded = NO; + [self.view addGestureRecognizer:self->slideToCloseSettingsViewRecognizer]; + + self->settingsViewJustLoaded = true; + + // Always run settings in dark mode because we want the light fonts + if (@available(iOS 13.0, tvOS 13.0, *)) { + self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; + } + + + self->currentSettingsMenuMode = self->tempSettings.settingsMenuMode.intValue; + [self loadFavoriteSettingStackIdentifiers]; + if(self->tempSettings.settingsMenuMode.intValue == FavoriteSettings) [self switchToFavoriteSettings]; + + // Ensure we pick a bitrate that falls exactly onto a slider notch + self->_bitrate = bitrateTable[[self getSliderValueForBitrate:[self->tempSettings.bitrate intValue]]]; + + // Get the size of the screen with and without safe area insets + // UIWindow *window = UIApplication.sharedApplication.windows.firstObject; + // CGFloat screenScale = window.screen.scale; + // CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale; + // CGFloat fullScreenWidth = window.frame.size.width * screenScale; + // CGFloat fullScreenHeight = window.frame.size.height * screenScale; + + [self.resolutionSelector removeSegmentAtIndex:0 animated:NO]; // remove 360p + [self.resolutionSelector removeSegmentAtIndex:5 animated:NO]; // remove custom segment + // iOS12 compatibility: + self.resolutionSelector.selectedSegmentIndex = 3; + [self.resolutionSelector setNeedsLayout]; + + resolutionTable[5] = CGSizeMake([self->tempSettings.width integerValue], [self->tempSettings.height integerValue]); // custom initial value + [self updateResolutionTable]; + + + NSInteger framerate; + switch ([self->tempSettings.framerate integerValue]) { + case 30: + framerate = 0; + break; + default: + case 60: + framerate = 1; + break; + case 120: + framerate = 2; + break; + } + + NSInteger resolution = self->tempSettings.resolutionSelected.integerValue; + if(resolution >= RESOLUTION_TABLE_SIZE){ + resolution = 0; + } + + // Only show the 120 FPS option if we have a > 60-ish Hz display + bool enable120Fps = false; + if (@available(iOS 10.3, tvOS 10.3, *)) { + if ([UIScreen mainScreen].maximumFramesPerSecond > 62) { + enable120Fps = true; + } + } + if (!enable120Fps) { + [self.framerateSelector removeSegmentAtIndex:2 animated:NO]; + } + + [UIView animateWithDuration:0 animations:^{ + // Disable codec selector segments for unsupported codecs +#if defined(__IPHONE_16_0) || defined(__TVOS_16_0) + if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) +#endif + { + [self.codecSelector removeSegmentAtIndex:2 animated:NO]; + } + if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) { + [self.codecSelector removeSegmentAtIndex:1 animated:NO]; + + // Only enable the 4K option for "recent" devices. We'll judge that by whether + // they support HEVC decoding (A9 or later). + [self.resolutionSelector setEnabled:NO forSegmentAtIndex:2]; + } - case CODEC_PREF_AV1: - [self.codecSelector setSelectedSegmentIndex:2]; - break; - - case CODEC_PREF_HEVC: - [self.codecSelector setSelectedSegmentIndex:1]; - break; - - case CODEC_PREF_H264: - [self.codecSelector setSelectedSegmentIndex:0]; - break; - } + switch (self->tempSettings.preferredCodec) { + case CODEC_PREF_AUTO: + [self.codecSelector setSelectedSegmentIndex:self.codecSelector.numberOfSegments - 1]; + break; + + case CODEC_PREF_AV1: + [self.codecSelector setSelectedSegmentIndex:2]; + break; + + case CODEC_PREF_HEVC: + [self.codecSelector setSelectedSegmentIndex:1]; + break; + + case CODEC_PREF_H264: + [self.codecSelector setSelectedSegmentIndex:0]; + break; + } + }]; - if (![self hdrSupported]) { - [self.hdrSwitch setOn:NO]; - [self.hdrSwitch setEnabled:NO]; - } - else { - [self.hdrSwitch setOn:tempSettings.enableHdr]; - } + if (![self hdrSupported]) { + [self.hdrSwitch setOn:NO]; + [self.hdrSwitch setEnabled:NO]; + } + else { + [self.hdrSwitch setOn:self->tempSettings.enableHdr]; + } - [self.yuv444Switch setOn:tempSettings.enableYUV444]; - - [self.pipSwitch setOn:tempSettings.enablePIP]; - if(@available(iOS 15.0, *)) [self.pipSwitch setEnabled:true]; - else{ - [self.pipSwitch setOn:false]; - [self.pipSwitch setEnabled:false]; - } - - [self.statsOverlaySelector setSelectedSegmentIndex:tempSettings.statsOverlayLevel.intValue]; + [self.yuv444Switch setOn:self->tempSettings.enableYUV444]; + + [self.pipSwitch setOn:self->tempSettings.enablePIP]; + if(@available(iOS 15.0, *)) [self.pipSwitch setEnabled:true]; + else{ + [self.pipSwitch setOn:false]; + [self.pipSwitch setEnabled:false]; + } + + [self.statsOverlaySelector setSelectedSegmentIndex:self->tempSettings.statsOverlayLevel.intValue]; - NSInteger renderingBackend = [tempSettings.renderingBackend integerValue]; - [self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend]; - [self.renderingBackendSelector addTarget:self action:@selector(renderingBackendChanged:) forControlEvents:UIControlEventValueChanged]; + NSInteger renderingBackend = [self->tempSettings.renderingBackend integerValue]; + [self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend]; + [self.renderingBackendSelector addTarget:self action:@selector(renderingBackendChanged:) forControlEvents:UIControlEventValueChanged]; - NSInteger framePacingMode = [tempSettings.framePacingMode integerValue]; - [self.framePacingModeSelector setSelectedSegmentIndex:framePacingMode]; - [self.framePacingModeSelector addTarget:self action:@selector(framePacingModeChanged:) forControlEvents:UIControlEventValueChanged]; - [self framePacingModeChanged:self.framePacingModeSelector]; + NSInteger framePacingMode = [self->tempSettings.framePacingMode integerValue]; + [self.framePacingModeSelector setSelectedSegmentIndex:framePacingMode]; + [self.framePacingModeSelector addTarget:self action:@selector(framePacingModeChanged:) forControlEvents:UIControlEventValueChanged]; + [self framePacingModeChanged:self.framePacingModeSelector]; - [self renderingBackendChanged:self.renderingBackendSelector]; // Update PiP and frame pacing state based on current selection + [self renderingBackendChanged:self.renderingBackendSelector]; // Update PiP and frame pacing state based on current selection - [self.citrixX1MouseSwitch setOn:tempSettings.btMouseSupport]; - [self.optimizeGamesSwitch setOn: tempSettings.optimizeGames]; - [self.multiControllerSwitch setOn:tempSettings.multiController]; - [self.swapAbxySwitch setOn:tempSettings.swapABXYButtons]; - [self.buttonVisualFeedbackSwitch setOn:tempSettings.buttonVisualFeedback]; + [self.citrixX1MouseSwitch setOn:self->tempSettings.btMouseSupport]; + [self.optimizeGamesSwitch setOn: self->tempSettings.optimizeGames]; + [self.multiControllerSwitch setOn:self->tempSettings.multiController]; + [self.swapAbxySwitch setOn:self->tempSettings.swapABXYButtons]; + [self.buttonVisualFeedbackSwitch setOn:self->tempSettings.buttonVisualFeedback]; - [self.gyroModeSelector setSelectedSegmentIndex:tempSettings.gyroMode.intValue]; - [self.gyroSensitivitySlider setValue: (uint16_t)(tempSettings.gyroSensitivity.floatValue * 100) animated:YES]; // Load old setting. - [self.gyroSensitivitySlider addTarget:self action:@selector(gyroSensitivitySliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. - [self gyroSensitivitySliderMoved:self.gyroSensitivitySlider]; - - if (@available(iOS 14.0, tvOS 14.0, *)) nil; - else{ - [self.gyroModeSelector setEnabled:false forSegmentAtIndex:1]; - [self.gyroModeSelector setEnabled:false forSegmentAtIndex:3]; - } - CMMotionManager *motionManager = [[CMMotionManager alloc] init]; - [self.gyroModeSelector setEnabled:[motionManager isGyroAvailable] forSegmentAtIndex:2]; - - [self.emulatedControllerTypeSelector setSelectedSegmentIndex:[self controllerTypeToSegmentIndex:tempSettings.emulatedControllerType.intValue]]; - [self.emulatedControllerTypeSelector addTarget:self action:@selector(emulatedControllerTypeChanged:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. - [self emulatedControllerTypeChanged:self.emulatedControllerTypeSelector]; - - [self.audioOnPcSwitch setOn:tempSettings.playAudioOnPC]; - - [self.localVolumeSlider setValue:tempSettings.localVolume.floatValue*100]; - [self.localVolumeSlider addTarget:self action:@selector(localVolumeSliderMoved:) forControlEvents:UIControlEventValueChanged]; - [self localVolumeSliderMoved:self.localVolumeSlider]; - - [self.micVolumeSlider setValue:tempSettings.micVolume.floatValue*100]; - [self.micVolumeSlider addTarget:self action:@selector(micVolumeSliderMoved:) forControlEvents:UIControlEventValueChanged]; - [self micVolumeSliderMoved:self.self.micVolumeSlider]; - - [self.redirectMicSwitch setOn:tempSettings.redirectMic]; - [self.redirectMicSwitch addTarget:self action:@selector(redirectMicSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; - [self redirectMicSwitchFlipped:self.redirectMicSwitch]; - - [self.useBuiltinMicSwitch setOn:tempSettings.useBuiltinMic]; - - _lastSelectedResolutionIndex = resolution; - [self.resolutionSelector setSelectedSegmentIndex:resolution]; - [self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged]; + [self.gyroModeSelector setSelectedSegmentIndex:self->tempSettings.gyroMode.intValue]; + [self.gyroSensitivitySlider setValue: (uint16_t)(self->tempSettings.gyroSensitivity.floatValue * 100) animated:YES]; // Load old setting. + [self.gyroSensitivitySlider addTarget:self action:@selector(gyroSensitivitySliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self gyroSensitivitySliderMoved:self.gyroSensitivitySlider]; + + if (@available(iOS 14.0, tvOS 14.0, *)) nil; + else{ + [self.gyroModeSelector setEnabled:false forSegmentAtIndex:1]; + [self.gyroModeSelector setEnabled:false forSegmentAtIndex:3]; + } + CMMotionManager *motionManager = [[CMMotionManager alloc] init]; + [self.gyroModeSelector setEnabled:[motionManager isGyroAvailable] forSegmentAtIndex:2]; + + [self.emulatedControllerTypeSelector setSelectedSegmentIndex:[self controllerTypeToSegmentIndex:self->tempSettings.emulatedControllerType.intValue]]; + [self.emulatedControllerTypeSelector addTarget:self action:@selector(emulatedControllerTypeChanged:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self emulatedControllerTypeChanged:self.emulatedControllerTypeSelector]; + + [self.audioOnPcSwitch setOn:self->tempSettings.playAudioOnPC]; + + [self.localVolumeSlider setValue:self->tempSettings.localVolume.floatValue*100]; + [self.localVolumeSlider addTarget:self action:@selector(localVolumeSliderMoved:) forControlEvents:UIControlEventValueChanged]; + [self localVolumeSliderMoved:self.localVolumeSlider]; + + [self.micVolumeSlider setValue:self->tempSettings.micVolume.floatValue*100]; + [self.micVolumeSlider addTarget:self action:@selector(micVolumeSliderMoved:) forControlEvents:UIControlEventValueChanged]; + [self micVolumeSliderMoved:self.self.micVolumeSlider]; + + [self.redirectMicSwitch setOn:self->tempSettings.redirectMic]; + [self.redirectMicSwitch addTarget:self action:@selector(redirectMicSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + [self redirectMicSwitchFlipped:self.redirectMicSwitch]; + + [self.useBuiltinMicSwitch setOn:self->tempSettings.useBuiltinMic]; + + self->_lastSelectedResolutionIndex = resolution; + [self.resolutionSelector setSelectedSegmentIndex:resolution]; + [self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged]; - [self.framerateSelector setSelectedSegmentIndex:framerate]; - [self.framerateSelector addTarget:self action:@selector(framerateChanged) forControlEvents:UIControlEventValueChanged]; - - [self.bitrateSlider setMinimumValue:0]; - [self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1]; - [self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES]; - [self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged]; - [self updateBitrateText]; - [self updateResolutionDisplayLabel]; + [self.framerateSelector setSelectedSegmentIndex:framerate]; + [self.framerateSelector addTarget:self action:@selector(framerateChanged) forControlEvents:UIControlEventValueChanged]; + + [self.bitrateSlider setMinimumValue:0]; + [self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1]; + [self.bitrateSlider setValue:[self getSliderValueForBitrate:self->_bitrate] animated:YES]; + [self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged]; + [self updateBitrateText]; + [self updateResolutionDisplayLabel]; - [self.frameQueueSizeSlider setMinimumValue:1]; - [self.frameQueueSizeSlider setMaximumValue:5]; - [self.frameQueueSizeSlider setValue:tempSettings.frameQueueSize.intValue]; - [self.frameQueueSizeSlider addTarget:self action:@selector(frameQueueSizeSliderMoved:) forControlEvents:UIControlEventValueChanged]; - [self frameQueueSizeSliderMoved:self.frameQueueSizeSlider]; + [self.frameQueueSizeSlider setMinimumValue:1]; + [self.frameQueueSizeSlider setMaximumValue:5]; + [self.frameQueueSizeSlider setValue:self->tempSettings.frameQueueSize.intValue]; + [self.frameQueueSizeSlider addTarget:self action:@selector(frameQueueSizeSliderMoved:) forControlEvents:UIControlEventValueChanged]; + [self frameQueueSizeSliderMoved:self.frameQueueSizeSlider]; - [self.enableGraphsSwitch setOn:tempSettings.enableGraphs animated:NO]; // Add this line - [self.enableGraphsSwitch addTarget:self action:@selector(enableGraphsChanged:) forControlEvents:UIControlEventValueChanged]; - [self enableGraphsChanged:self.enableGraphsSwitch]; - [self.graphOpacityStepper setMinimumValue:0]; - [self.graphOpacityStepper setMaximumValue:100]; - [self.graphOpacityStepper setValue:(int)tempSettings.graphOpacity.intValue]; - [self.graphOpacityStepper addTarget:self action:@selector(graphOpacityStepperTapped:) forControlEvents:UIControlEventValueChanged]; - [self graphOpacityStepperTapped:self.graphOpacityStepper]; + [self.enableGraphsSwitch setOn:self->tempSettings.enableGraphs animated:NO]; // Add this line + [self.enableGraphsSwitch addTarget:self action:@selector(enableGraphsChanged:) forControlEvents:UIControlEventValueChanged]; + [self enableGraphsChanged:self.enableGraphsSwitch]; + [self.graphOpacityStepper setMinimumValue:0]; + [self.graphOpacityStepper setMaximumValue:100]; + [self.graphOpacityStepper setValue:(int)self->tempSettings.graphOpacity.intValue]; + [self.graphOpacityStepper addTarget:self action:@selector(graphOpacityStepperTapped:) forControlEvents:UIControlEventValueChanged]; + [self graphOpacityStepperTapped:self.graphOpacityStepper]; - if (@available(iOS 18.0, tvOS 18.0, *)) {}else{ - [self.audioConfigSelector removeSegmentAtIndex:1 animated:false]; - [self.audioConfigSelector removeSegmentAtIndex:1 animated:false]; // segment 2 goes away when you remove index 2 - [self.audioConfigSelector setTitle:[LocalizationHelper localizedStringForKey:@"Stereo (surround sound available for iOS18+)"] forSegmentAtIndex:0]; - [self.audioConfigSelector setEnabled:NO]; - } - switch ([tempSettings.audioConfig integerValue]) { - case 2: - [self.audioConfigSelector setSelectedSegmentIndex:0]; - break; - case 6: - [self.audioConfigSelector setSelectedSegmentIndex:1]; - break; - case 8: - [self.audioConfigSelector setSelectedSegmentIndex:2]; - break; - } + if (@available(iOS 18.0, tvOS 18.0, *)) {}else{ + [self.audioConfigSelector removeSegmentAtIndex:1 animated:false]; + [self.audioConfigSelector removeSegmentAtIndex:1 animated:false]; // segment 2 goes away when you remove index 2 + [self.audioConfigSelector setTitle:[LocalizationHelper localizedStringForKey:@"Stereo (surround sound available for iOS18+)"] forSegmentAtIndex:0]; + [self.audioConfigSelector setEnabled:NO]; + } + switch ([self->tempSettings.audioConfig integerValue]) { + case 2: + [self.audioConfigSelector setSelectedSegmentIndex:0]; + break; + case 6: + [self.audioConfigSelector setSelectedSegmentIndex:1]; + break; + case 8: + [self.audioConfigSelector setSelectedSegmentIndex:2]; + break; + } - // Unlock Display Orientation setting - bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired] || [self isIPhone];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation working - if(unlockDisplayOrientationSelectorEnabled) [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:tempSettings.unlockDisplayOrientation ? 1 : 0]; - else [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:1]; // can't lock screen orientation in this mode = Display Orientation always unlocked - [self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled]; + // Unlock Display Orientation setting + bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired] || [self isIPhone];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation working + if(unlockDisplayOrientationSelectorEnabled) [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:self->tempSettings.unlockDisplayOrientation ? 1 : 0]; + else [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:1]; // can't lock screen orientation in this mode = Display Orientation always unlocked + [self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled]; - [self.backgroundSessionTimerSlider setValue:(uint32_t)tempSettings.backgroundSessionTimer.floatValue]; - [self.backgroundSessionTimerSlider addTarget:self action:@selector(backgroundSessionTimerSliderMoved:) forControlEvents:UIControlEventValueChanged]; - [self backgroundSessionTimerSliderMoved:self.backgroundSessionTimerSlider]; + [self.backgroundSessionTimerSlider setValue:(uint32_t)self->tempSettings.backgroundSessionTimer.floatValue]; + [self.backgroundSessionTimerSlider addTarget:self action:@selector(backgroundSessionTimerSliderMoved:) forControlEvents:UIControlEventValueChanged]; + [self backgroundSessionTimerSliderMoved:self.backgroundSessionTimerSlider]; - // lift streamview setting - [self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:tempSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting + // lift streamview setting + [self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:self->tempSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting - // showkeyboard toolbar setting - [self.softKeyboardToolbarSwitch setOn:tempSettings.showKeyboardToolbar];// Load old setting + // showkeyboard toolbar setting + [self.softKeyboardToolbarSwitch setOn:self->tempSettings.showKeyboardToolbar];// Load old setting - // reverse mouse wheel direction setting - [self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:tempSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting + // reverse mouse wheel direction setting + [self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:self->tempSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting - // slide to menu settings - [self.slideToSettingsScreenEdgeSelector setSelectedSegmentIndex:[self getSelectorIndexFromScreenEdge:(uint32_t)tempSettings.slideToSettingsScreenEdge.integerValue]]; - // Load old setting - [self.slideToToolboxScreenEdgeSelector setEnabled:false]; - [self.slideToSettingsScreenEdgeSelector addTarget:self action:@selector(slideToSettingsScreenEdgeChanged) forControlEvents:UIControlEventValueChanged]; - [self slideToSettingsScreenEdgeChanged]; + // slide to menu settings + [self.slideToSettingsScreenEdgeSelector setSelectedSegmentIndex:[self getSelectorIndexFromScreenEdge:(uint32_t)self->tempSettings.slideToSettingsScreenEdge.integerValue]]; + // Load old setting + [self.slideToToolboxScreenEdgeSelector setEnabled:false]; + [self.slideToSettingsScreenEdgeSelector addTarget:self action:@selector(slideToSettingsScreenEdgeChanged) forControlEvents:UIControlEventValueChanged]; + [self slideToSettingsScreenEdgeChanged]; - [self.slideToMenuDistanceSlider setValue:tempSettings.slideToSettingsDistance.floatValue]; - [self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. - [self slideToMenuDistanceSliderMoved:self.slideToMenuDistanceSlider]; + [self.slideToMenuDistanceSlider setValue:self->tempSettings.slideToSettingsDistance.floatValue]; + [self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self slideToMenuDistanceSliderMoved:self.slideToMenuDistanceSlider]; - //TouchMode & OSC Related Settings: + //TouchMode & OSC Related Settings: - // pointer veloc setting, will be enable/disabled by touchMode - [self.pointerVelocityModeDividerSlider setValue: (uint8_t)(tempSettings.pointerVelocityModeDivider.floatValue * 100) animated:YES]; // Load old setting. - [self.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. - [self pointerVelocityModeDividerSliderMoved:self.pointerVelocityModeDividerSlider]; + // pointer veloc setting, will be enable/disabled by touchMode + [self.pointerVelocityModeDividerSlider setValue: (uint8_t)(self->tempSettings.pointerVelocityModeDivider.floatValue * 100) animated:YES]; // Load old setting. + [self.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self pointerVelocityModeDividerSliderMoved:self.pointerVelocityModeDividerSlider]; - // init pointer veloc setting, will be enable/disabled by touchMode - [self.touchPointerVelocityFactorSlider setValue: [self map_SliderValue_fromVelocFactor: tempSettings.touchPointerVelocityFactor.floatValue] animated:YES]; // Load old setting. - [self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. - [self touchPointerVelocityFactorSliderMoved:self.touchPointerVelocityFactorSlider]; + // init pointer veloc setting, will be enable/disabled by touchMode + [self.touchPointerVelocityFactorSlider setValue: [self map_SliderValue_fromVelocFactor: self->tempSettings.touchPointerVelocityFactor.floatValue] animated:YES]; // Load old setting. + [self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self touchPointerVelocityFactorSliderMoved:self.touchPointerVelocityFactorSlider]; - // async native touch event - // [self.asyncNativeTouchPrioritySelector setSelectedSegmentIndex:currentSettings.asyncNativeTouchPriority.intValue]; // load old setting of asyncNativeTouchPriority - // [self.asyncNativeTouchPrioritySelector addTarget:self action:@selector(asyncNativeTouchPriorityChanged) forControlEvents:UIControlEventValueChanged]; + // async native touch event + // [self.asyncNativeTouchPrioritySelector setSelectedSegmentIndex:currentSettings.asyncNativeTouchPriority.intValue]; // load old setting of asyncNativeTouchPriority + // [self.asyncNativeTouchPrioritySelector addTarget:self action:@selector(asyncNativeTouchPriorityChanged) forControlEvents:UIControlEventValueChanged]; - // init relative touch mouse pointer veloc setting, will be enable/disabled by touchMode - [self.mousePointerVelocityFactorSlider setValue:[self map_SliderValue_fromVelocFactor: tempSettings.mousePointerVelocityFactor.floatValue] animated:YES]; // Load old setting. - [self.mousePointerVelocityFactorSlider addTarget:self action:@selector(mousePointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. - [self mousePointerVelocityFactorSliderMoved:self.mousePointerVelocityFactorSlider]; + // init relative touch mouse pointer veloc setting, will be enable/disabled by touchMode + [self.mousePointerVelocityFactorSlider setValue:[self map_SliderValue_fromVelocFactor: self->tempSettings.mousePointerVelocityFactor.floatValue] animated:YES]; // Load old setting. + [self.mousePointerVelocityFactorSlider addTarget:self action:@selector(mousePointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self mousePointerVelocityFactorSliderMoved:self.mousePointerVelocityFactorSlider]; - // these settings will be affected by onscreenControl & touchMode, must be loaded before them. - // NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue); - self->oswLayoutFingers = (uint16_t)tempSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers - uint8_t keyboardToggleFingers = tempSettings.keyboardToggleFingers.intValue; + // these settings will be affected by onscreenControl & touchMode, must be loaded before them. + // NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue); + self->oswLayoutFingers = (uint16_t)self->tempSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers + uint8_t keyboardToggleFingers = self->tempSettings.keyboardToggleFingers.intValue; - [self.softKeyboardGestureSelector setSelectedSegmentIndex:keyboardToggleFingers>=5 ? 3 : keyboardToggleFingers-3]; + [self.softKeyboardGestureSelector setSelectedSegmentIndex:keyboardToggleFingers>=5 ? 3 : keyboardToggleFingers-3]; - // this setting will be affected by touchMode, must be loaded before them. - NSInteger onscreenControlsLevel = [tempSettings.onscreenControls integerValue]; - [self.onScreenWidgetSelector setSelectedSegmentIndex:onscreenControlsLevel]; - [self.onScreenWidgetSelector addTarget:self action:@selector(onScreenWidgetChanged) forControlEvents:UIControlEventValueChanged]; - [self onScreenWidgetChanged]; + // this setting will be affected by touchMode, must be loaded before them. + NSInteger onscreenControlsLevel = [self->tempSettings.onscreenControls integerValue]; + [self.onScreenWidgetSelector setSelectedSegmentIndex:onscreenControlsLevel]; + [self.onScreenWidgetSelector addTarget:self action:@selector(onScreenWidgetChanged) forControlEvents:UIControlEventValueChanged]; + [self onScreenWidgetChanged]; - // touch move event interval for native-touch. - [self.touchMoveEventIntervalSlider setValue:tempSettings.touchMoveEventInterval.intValue animated:YES]; // Load old setting. - [self.touchMoveEventIntervalSlider addTarget:self action:@selector(touchMoveEventIntervalSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. - [self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider]; + // touch move event interval for native-touch. + [self.touchMoveEventIntervalSlider setValue:self->tempSettings.touchMoveEventInterval.intValue animated:YES]; // Load old setting. + [self.touchMoveEventIntervalSlider addTarget:self action:@selector(touchMoveEventIntervalSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider]; - // this part will enable/disable oscSelector & the asyncNativeTouchPriority selector - uint8_t touchModeSelectorIndex = tempSettings.touchMode.intValue == NativeTouchOnly ? NativeTouch : tempSettings.touchMode.intValue; - [self.touchModeSelector setSelectedSegmentIndex:touchModeSelectorIndex]; //Load old touchMode setting - [self.touchModeSelector addTarget:self action:@selector(touchModeChanged:) forControlEvents:UIControlEventValueChanged]; - [self touchModeChanged:self.touchModeSelector]; + // this part will enable/disable oscSelector & the asyncNativeTouchPriority selector + uint8_t touchModeSelectorIndex = self->tempSettings.touchMode.intValue == NativeTouchOnly ? NativeTouch : self->tempSettings.touchMode.intValue; + [self.touchModeSelector setSelectedSegmentIndex:touchModeSelectorIndex]; //Load old touchMode setting + [self.touchModeSelector addTarget:self action:@selector(touchModeChanged:) forControlEvents:UIControlEventValueChanged]; + [self touchModeChanged:self.touchModeSelector]; - self.enableOswSwitchStack.hidden = !(tempSettings.touchMode.intValue == NativeTouch || tempSettings.touchMode.intValue == NativeTouchOnly); // do not use setHidden to stack wrapped by a settingStack - - [self.enableOswForNativeTouchSwitch setOn:tempSettings.touchMode.intValue != NativeTouchOnly]; - [self.enableOswForNativeTouchSwitch addTarget:self action:@selector(enableOswForNativeTouchSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; - [self enableOswForNativeTouchSwitchFlipped:self.enableOswForNativeTouchSwitch]; + self.enableOswSwitchStack.hidden = !(self->tempSettings.touchMode.intValue == NativeTouch || self->tempSettings.touchMode.intValue == NativeTouchOnly); // do not use setHidden to stack wrapped by a settingStack + + [self.enableOswForNativeTouchSwitch setOn:self->tempSettings.touchMode.intValue != NativeTouchOnly]; + [self.enableOswForNativeTouchSwitch addTarget:self action:@selector(enableOswForNativeTouchSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + [self enableOswForNativeTouchSwitchFlipped:self.enableOswForNativeTouchSwitch]; + + [self.externalDisplayModeSelector setSelectedSegmentIndex:self->tempSettings.externalDisplayMode.integerValue]; + [self.localMousePointerModeSelector setSelectedSegmentIndex:self->tempSettings.localMousePointerMode.integerValue]; + + [self.sendDummyEventSwitch setOn:self->tempSettings.sendDummyEvent];// Load old setting + + + self->motionControlSection.expandable = [self isCustomOswEnabled]; + __weak typeof(self) weakSelf = self; + self->motionControlSection.lockedSectionHandler = ^{ + [CountdownAlertController showAlertIn:weakSelf + title:[LocalizationHelper localizedStringForKey:@"Tips"] + message:[LocalizationHelper localizedStringForKey:@"Tap 'OK' to set on-screen widget to 'Custom' and enable motion control."] + withCancel:YES + buttonTitle:[LocalizationHelper localizedStringForKey:@"OK"] + countdown:0 + completion:^{ + if(!CountdownAlertController.actionCancelled){ + [weakSelf.onScreenWidgetSelector setSelectedSegmentIndex:OnScreenControlsLevelCustom]; + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + strongSelf->motionControlSection.expandable = true; + [strongSelf->motionControlSection setExpanded:YES]; + } + }]; + }; + [self.mapGyroToSelector addTarget:self action:@selector(mapGyroToChanged:) forControlEvents:UIControlEventValueChanged]; + [self.yawPitchToRightStickSwitch addTarget:self action:@selector(yawPitchToRightStickSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + [self.rollToLeftStickSwitch addTarget:self action:@selector(rollToLeftStickSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + + [self.yawSensitivitySlider addTarget:self action:@selector(yawSensitivitySliderMoved:) forControlEvents:UIControlEventValueChanged]; + [self.pitchSensitivitySlider addTarget:self action:@selector(pitchSensitivitySliderMoved:) forControlEvents:UIControlEventValueChanged]; + [self.rollSensitivitySlider addTarget:self action:@selector(rollSensitivitySliderMoved:) forControlEvents:UIControlEventValueChanged]; + + //Motion control settings + + self->settingsViewJustLoaded = false; + }]; - [self.externalDisplayModeSelector setSelectedSegmentIndex:tempSettings.externalDisplayMode.integerValue]; - [self.localMousePointerModeSelector setSelectedSegmentIndex:tempSettings.localMousePointerMode.integerValue]; - - [self.sendDummyEventSwitch setOn:tempSettings.sendDummyEvent];// Load old setting - - settingsViewJustLoaded = false; } - (void)slideToSettingsScreenEdgeChanged{ @@ -1793,7 +1900,11 @@ BOOL isCustomResolution(int resolutionSelected) { } - (bool)isCustomOswEnabled{ - return [self isOswEnabled] && self.onScreenWidgetSelector.selectedSegmentIndex == OnScreenControlsLevelCustom; + bool customOswEnabled = [self isOswEnabled] && self.onScreenWidgetSelector.selectedSegmentIndex == OnScreenControlsLevelCustom; + motionControlSection.expandable = customOswEnabled; + if(!settingsViewJustLoaded) [motionControlSection setExpanded:customOswEnabled]; + NSLog(@"isCustomOswEnabled %d, %f",customOswEnabled, CACurrentMediaTime()); + return customOswEnabled; } - (bool)isNotNativeTouchOnly{ @@ -1926,6 +2037,76 @@ BOOL isCustomResolution(int resolutionSelected) { } } +- (void)mapGyroToChanged:(UISegmentedControl* )sender{ + [self setHidden:sender.selectedSegmentIndex != mapGyroToControllerStick forStack:_gyroToStickSwitchStack]; + bool mapGyroToMouseEnabled = sender.selectedSegmentIndex == mapGyroToMouse; + bool mapGyroToControllerStickEnabled = sender.selectedSegmentIndex == mapGyroToControllerStick; + if(mapGyroToMouseEnabled){ + [self setHidden:false forStack:_yawPitchSensitivityStack]; + [self setHidden:true forStack:_rollSensitivityStack]; + } + if(mapGyroToControllerStickEnabled){ + [self yawPitchToRightStickSwitchFlipped:self.yawPitchToRightStickSwitch]; + [self rollToLeftStickSwitchFlipped:self.rollToLeftStickSwitch]; + } + if(sender.selectedSegmentIndex == driftCorrection){ + + [CountdownAlertController showAlertIn:self + title:[LocalizationHelper localizedStringForKey:@"Drift Correction"] + message:[LocalizationHelper localizedStringForKey:@"Place the device flat on a surface and keep it still, then tap ‘Start’."] + withCancel:YES + buttonTitle:[LocalizationHelper localizedStringForKey:@"Start"] + countdown:0 + completion:^{ + if(CountdownAlertController.actionCancelled){ + [self.mapGyroToSelector setSelectedSegmentIndex:self->oscProfile.mapGyroTo]; + NSLog(@"actionCancelled"); + } + else{ + MotionHandler* motionHandler = [MotionHandler sharedInstance]; + [motionHandler calibrateGyroBiasWithDuration:5 completion:^{ + Settings* currentSettings = [self->dataMan retrieveSettings]; + currentSettings.gyroBiasX = [NSNumber numberWithDouble:motionHandler.gyroBiasX]; + currentSettings.gyroBiasY = [NSNumber numberWithDouble:motionHandler.gyroBiasY]; + currentSettings.gyroBiasZ = [NSNumber numberWithDouble:motionHandler.gyroBiasZ]; + [self->dataMan saveData]; + }]; + [CountdownAlertController showAlertIn:self + title:[LocalizationHelper localizedStringForKey:@"Drift Correction"] + message:[LocalizationHelper localizedStringForKey:@"Calibrating..."] + withCancel:NO + buttonTitle:[LocalizationHelper localizedStringForKey:@"Finished!"] + countdown:6 + completion:^{ + [self.mapGyroToSelector setSelectedSegmentIndex:self->oscProfile.mapGyroTo]; + }]; + } + }]; + } +} + +- (void)rollToLeftStickSwitchFlipped:(UISwitch* )sender{ + [self setHidden:!sender.isOn forStack:_rollSensitivityStack]; +} + +- (void)yawPitchToRightStickSwitchFlipped:(UISwitch* )sender{ + [self setHidden:!sender.isOn forStack:_yawPitchSensitivityStack]; +} + +- (void)yawSensitivitySliderMoved:(UISlider* )sender{ + [self findDynamicLabelFromStack:_yawSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (int16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; + [_pitchSensitivitySlider setValue:sender.value]; + [self pitchSensitivitySliderMoved:self.pitchSensitivitySlider]; +} + +- (void)pitchSensitivitySliderMoved:(UISlider* )sender{ + [self findDynamicLabelFromStack:_pitchSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (int16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; +} + +- (void)rollSensitivitySliderMoved:(UISlider* )sender{ + [self findDynamicLabelFromStack:_rollSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (int16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; +} + - (void)invokeOscLayout{ // init CustomOSC stuff /* sets a reference to the correct 'LayoutOnScreenControlsViewController' depending on whether the user is on an iPhone or iPad */ @@ -1950,7 +2131,7 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void) touchPointerVelocityFactorSliderMoved:(UISlider* )sender { - [self findDynamicLabelFromStack:self.pointerVelocityFactorStack].text = [NSString stringWithFormat:@" %d%% ", [self map_velocFactorDisplay_fromSliderValue:sender.value]]; // Update label display + [self findDynamicLabelFromStack:self.pointerVelocityFactorStack].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; // Update label display } - (void) gyroSensitivitySliderMoved:(UISlider* )sender { @@ -1977,10 +2158,10 @@ BOOL isCustomResolution(int resolutionSelected) { // veloc factor upto 700% -- (uint16_t) map_velocFactorDisplay_fromSliderValue:(CGFloat)sliderValue{ - uint16_t velocFactorDisplay = 0; +- (CGFloat) map_velocFactorDisplay_fromSliderValue:(CGFloat)sliderValue{ + CGFloat velocFactorDisplay = 0; if(sliderValue > 200) velocFactorDisplay = 200 + ((uint16_t)sliderValue % 200) * 5; - else velocFactorDisplay = (uint16_t) sliderValue; + else velocFactorDisplay = sliderValue; return velocFactorDisplay; } @@ -1994,7 +2175,7 @@ BOOL isCustomResolution(int resolutionSelected) { } - (void) mousePointerVelocityFactorSliderMoved:(UISlider* )sender { - [self findDynamicLabelFromStack:_mousePointerVelocityStack].text = [NSString stringWithFormat:@" %d%% ",[self map_velocFactorDisplay_fromSliderValue:sender.value]]; + [self findDynamicLabelFromStack:_mousePointerVelocityStack].text = [NSString stringWithFormat:@" %d%% ",(uint16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; } - (uint32_t) getScreenEdgeFromSelector { @@ -2516,6 +2697,8 @@ BOOL isCustomResolution(int resolutionSelected) { } if(![MicHandler permissionGranted]) [self.redirectMicSwitch setOn:false]; + + [self saveMotionControlConfigs]; } - (void) saveSettings { diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index c789bbc0..73441199 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -434,6 +434,10 @@ } _motionHandler = [MotionHandler sharedInstance]; + _motionHandler.gyroBiasX = _settings.gyroBiasX.doubleValue; + _motionHandler.gyroBiasY = _settings.gyroBiasY.doubleValue; + _motionHandler.gyroBiasZ = _settings.gyroBiasZ.doubleValue; + _streamView.onScreenControls.instanceReceiverDelegate = _motionHandler; [_streamView.onScreenControls sendInstance]; @@ -534,64 +538,22 @@ - (void)popFirstStreamingTip { // 初始化倒计时秒数 - __block NSInteger remainingSeconds = 16; - + NSString* settingsEdgeSide = _settings.slideToSettingsScreenEdge.intValue == UIRectEdgeLeft ? [LocalizationHelper localizedStringForKey:@"left"] : [LocalizationHelper localizedStringForKey:@"right"]; NSString* cmdToolEdgeSide = _settings.slideToSettingsScreenEdge.intValue == UIRectEdgeLeft ? [LocalizationHelper localizedStringForKey:@"right"] : [LocalizationHelper localizedStringForKey:@"left"]; uint8_t slideDist = (uint8_t)(_settings.slideToSettingsDistance.floatValue * 100); // 创建弹窗 - NSString* tipText = [LocalizationHelper localizedStringForKey:@"firstLaunchTip", settingsEdgeSide, slideDist, cmdToolEdgeSide, slideDist]; - UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"First Launch Tips"] message: [LocalizationHelper localizedStringForKey:@"%@", tipText] preferredStyle:UIAlertControllerStyleAlert]; - + [CountdownAlertController showAlertIn:self + title:[LocalizationHelper localizedStringForKey:@"First Launch Tips"] + message:tipText + withCancel:NO + buttonTitle:[LocalizationHelper localizedStringForKey:@"Got it!"] + countdown:16 + completion:^{}]; - NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; - paragraphStyle.alignment = NSTextAlignmentLeft; - - NSDictionary *attributes = @{ - NSParagraphStyleAttributeName: paragraphStyle, - NSFontAttributeName: [UIFont systemFontOfSize:14] - }; - - NSAttributedString *attributedMessage = [[NSAttributedString alloc] initWithString:tipText - attributes:attributes]; - - // 使用 KVC 设置 attributedMessage(注意审核风险) - [tipsAlertController setValue:attributedMessage forKey:@"attributedMessage"]; - - // 添加确认按钮(初始禁用) - UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Got it! (15)"] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - }]; - confirmAction.enabled = NO; - [tipsAlertController addAction:confirmAction]; - - // 显示弹窗 - [self presentViewController:tipsAlertController animated:YES completion:nil]; - - // 使用dispatch_source_t实现精确倒计时 - dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); - dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); - - dispatch_source_set_event_handler(timer, ^{ - remainingSeconds--; - - if (remainingSeconds <= 0) { - // 倒计时结束 - dispatch_source_cancel(timer); - // 启用确认按钮 - confirmAction.enabled = YES; - [confirmAction setValue:[LocalizationHelper localizedStringForKey:@"Got it!"] forKey:@"title"]; - } else { - // 更新按钮标题和消息 - [confirmAction setValue:[NSString stringWithFormat:[LocalizationHelper localizedStringForKey:@"Got it! (%ld)", remainingSeconds], (long)remainingSeconds] forKey:@"title"]; - } - }); - - dispatch_resume(timer); - + return; } - (void)updateTheme { @@ -1570,8 +1532,8 @@ [_motionHandler startAccelUpdate]; } -- (void)stopGyroUpdate{ - [_motionHandler stopGyroUpdate]; +- (void)stopGyroUpdateWithInterruption:(BOOL)interruption{ + [_motionHandler stopGyroUpdateWithInterruption:interruption]; } - (void)stopAccelUpdate{ diff --git a/VoidLink/VoidLink-Bridging-Header.h b/VoidLink/VoidLink-Bridging-Header.h index 6b6ed4c5..278a38e7 100644 --- a/VoidLink/VoidLink-Bridging-Header.h +++ b/VoidLink/VoidLink-Bridging-Header.h @@ -5,6 +5,8 @@ #include #import "OnScreenControls.h" #import "OnScreenButtonState.h" +#import "OSCProfile.h" +#import "OSCProfilesManager.h" #import "opus.h" #import "opus_defines.h" // #import "RelativeTouchHandler.h" diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index f0a1b64f..ab379863 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -313,6 +313,36 @@ } } }, + "6gY-8I-NWq.text" : { + "comment" : "Class = \"UILabel\"; text = \"Map Gyro to\"; ObjectID = \"6gY-8I-NWq\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Map Gyro to" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "陀螺仪映射为" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "陀螺儀映射為" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "陀螺儀映射為" + } + } + } + }, "6w5-Md-NpV.text" : { "comment" : "Class = \"UILabel\"; text = \"Preferred Codec\"; ObjectID = \"6w5-Md-NpV\";", "extractionState" : "extracted_with_value", @@ -865,6 +895,36 @@ } } }, + "aol-mw-Kn1.text" : { + "comment" : "Class = \"UILabel\"; text = \"Yaw Sensitivity\"; ObjectID = \"aol-mw-Kn1\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Yaw Sensitivity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航灵敏度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航靈敏度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航靈敏度" + } + } + } + }, "aup-5k-z6P.text" : { "comment" : "Class = \"UILabel\"; text = \"Show Local Keyboard Toolbar \"; ObjectID = \"aup-5k-z6P\";", "extractionState" : "extracted_with_value", @@ -1657,6 +1717,36 @@ } } }, + "EFR-Ee-KiK.text" : { + "comment" : "Class = \"UILabel\"; text = \"Roll → Left Stick\"; ObjectID = \"EFR-Ee-KiK\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Roll → Left Stick" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚转→左摇杆" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉→左搖桿" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉→左搖桿" + } + } + } + }, "egA-7n-Dpe.text" : { "comment" : "Class = \"UILabel\"; text = \"Touch Pointer Velocity\"; ObjectID = \"egA-7n-Dpe\";", "extractionState" : "extracted_with_value", @@ -1837,6 +1927,96 @@ } } }, + "gTZ-RS-VIl.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; gTZ-RS-VIl.segmentTitles[0] = \"Mouse\"; ObjectID = \"gTZ-RS-VIl\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mouse" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠标" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滑鼠" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "滑鼠" + } + } + } + }, + "gTZ-RS-VIl.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; gTZ-RS-VIl.segmentTitles[1] = \"Controller Stick\"; ObjectID = \"gTZ-RS-VIl\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Controller Stick" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "手柄摇杆" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "手把搖桿" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "手把搖桿" + } + } + } + }, + "gTZ-RS-VIl.segmentTitles[2]" : { + "comment" : "Class = \"UISegmentedControl\"; gTZ-RS-VIl.segmentTitles[2] = \"Drift Correction\"; ObjectID = \"gTZ-RS-VIl\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Drift Correction" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + } + } + }, "Gus-CK-wpc.text" : { "comment" : "Class = \"UILabel\"; text = \"Send Dummy Event\"; ObjectID = \"Gus-CK-wpc\";", "extractionState" : "extracted_with_value", @@ -2293,6 +2473,36 @@ } } }, + "jcj-D4-VnY.text" : { + "comment" : "Class = \"UILabel\"; text = \"Pitch Sensitivity\"; ObjectID = \"jcj-D4-VnY\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Pitch Sensitivity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "俯仰灵敏度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "俯仰靈敏度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "俯仰靈敏度" + } + } + } + }, "JTl-un-QJJ.text" : { "comment" : "Class = \"UILabel\"; text = \"Emulated Controller Type\"; ObjectID = \"JTl-un-QJJ\";", "extractionState" : "extracted_with_value", @@ -3709,6 +3919,36 @@ } } }, + "u9Q-wm-UBm.text" : { + "comment" : "Class = \"UILabel\"; text = \"Roll Sensitivity\"; ObjectID = \"u9Q-wm-UBm\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Roll Sensitivity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚转灵敏度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉靈敏度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉靈敏度" + } + } + } + }, "v8l-hf-kiR.text" : { "comment" : "Class = \"UILabel\"; text = \"Rendering Mode (Experimental)\"; ObjectID = \"v8l-hf-kiR\";", "extractionState" : "extracted_with_value", @@ -3841,6 +4081,36 @@ } } }, + "XU0-qu-2p4.text" : { + "comment" : "Class = \"UILabel\"; text = \"Yaw&Pitch → Right Stick\"; ObjectID = \"XU0-qu-2p4\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Yaw&Pitch → Right Stick" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航/俯仰→右摇杆" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航/俯仰→右搖桿" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "偏航/俯仰→右搖桿" + } + } + } + }, "Ylu-hT-Cuz.text" : { "comment" : "Class = \"UILabel\"; text = \"The recommended value is 2. 1 frame provides the lowest latency, but may require an Ethernet connection.\"; ObjectID = \"Ylu-hT-Cuz\";", "extractionState" : "extracted_with_value", diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index cae1b399..08ee2268 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -61,6 +61,96 @@ } } }, + "0OC-rX-CHK.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; 0OC-rX-CHK.segmentTitles[0] = \"Mouse\"; ObjectID = \"0OC-rX-CHK\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mouse" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠标" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滑鼠" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "滑鼠" + } + } + } + }, + "0OC-rX-CHK.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; 0OC-rX-CHK.segmentTitles[1] = \"Controller Stick\"; ObjectID = \"0OC-rX-CHK\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Controller Stick" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "手柄摇杆" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "手把搖桿" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "手把搖桿" + } + } + } + }, + "0OC-rX-CHK.segmentTitles[2]" : { + "comment" : "Class = \"UISegmentedControl\"; 0OC-rX-CHK.segmentTitles[2] = \"Drift Correction\"; ObjectID = \"0OC-rX-CHK\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Drift Correction" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "漂移修正" + } + } + } + }, "3av-lc-tcF.text" : { "comment" : "Class = \"UILabel\"; text = \"Enable On-Screen Widget & Peripherals\"; ObjectID = \"3av-lc-tcF\";", "extractionState" : "extracted_with_value", @@ -121,6 +211,36 @@ } } }, + "3LT-qb-ZEB.text" : { + "comment" : "Class = \"UILabel\"; text = \"Roll → Left Stick\"; ObjectID = \"3LT-qb-ZEB\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Roll → Left Stick" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚转→左摇杆" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉→左搖桿" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉→左搖桿" + } + } + } + }, "4Ul-P6-eUk.text" : { "comment" : "Class = \"UILabel\"; text = \"Enable PiP\"; ObjectID = \"4Ul-P6-eUk\";", "extractionState" : "extracted_with_value", @@ -907,6 +1027,66 @@ } } }, + "AN9-U4-nUG.text" : { + "comment" : "Class = \"UILabel\"; text = \"Yaw&Pitch → Right Stick\"; ObjectID = \"AN9-U4-nUG\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Yaw&Pitch → Right Stick" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航/俯仰→右摇杆" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航/俯仰→右搖桿" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航/俯仰→右搖桿" + } + } + } + }, + "aSq-2H-FPk.text" : { + "comment" : "Class = \"UILabel\"; text = \"Yaw Sensitivity\"; ObjectID = \"aSq-2H-FPk\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Yaw Sensitivity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航灵敏度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航靈敏度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "偏航靈敏度" + } + } + } + }, "awK-Ei-T9i.text" : { "comment" : "Class = \"UILabel\"; text = \"Open Toolbox in Streaming\"; ObjectID = \"awK-Ei-T9i\";", "extractionState" : "extracted_with_value", @@ -1435,6 +1615,36 @@ } } }, + "dxk-33-Nbx.text" : { + "comment" : "Class = \"UILabel\"; text = \"Roll Sensitivity\"; ObjectID = \"dxk-33-Nbx\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Roll Sensitivity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "滚转灵敏度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉靈敏度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "滾轉靈敏度" + } + } + } + }, "E7F-vx-pnM.text" : { "comment" : "Class = \"UILabel\"; text = \"Frame Rate\"; ObjectID = \"E7F-vx-pnM\";", "extractionState" : "extracted_with_value", @@ -1681,6 +1891,36 @@ } } }, + "GX4-zV-pQ8.text" : { + "comment" : "Class = \"UILabel\"; text = \"Map Gyro to\"; ObjectID = \"GX4-zV-pQ8\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Map Gyro to" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "陀螺仪映射为" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "陀螺儀映射為" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "陀螺儀映射為" + } + } + } + }, "hfR-K8-Ti4.segmentTitles[0]" : { "comment" : "Class = \"UISegmentedControl\"; hfR-K8-Ti4.segmentTitles[0] = \"Stereo\"; ObjectID = \"hfR-K8-Ti4\";", "extractionState" : "extracted_with_value", @@ -3931,6 +4171,36 @@ } } }, + "yJv-xb-5HL.text" : { + "comment" : "Class = \"UILabel\"; text = \"Pitch Sensitivity\"; ObjectID = \"yJv-xb-5HL\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Pitch Sensitivity" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "俯仰灵敏度" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "俯仰靈敏度" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "俯仰靈敏度" + } + } + } + }, "Z9M-yq-an2.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Import\"; ObjectID = \"Z9M-yq-an2\";", "extractionState" : "extracted_with_value",