From 8f8e86ab4f1819e849d8bbc1b8758f91c915944f Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Thu, 29 Jan 2026 10:06:04 +0800 Subject: [PATCH] =?UTF-8?q?-=20on=20screen=20button=20=E2=80=9Cfolder?= =?UTF-8?q?=E2=80=9D=20prototype=20-=20overlapped=20buttons=20triggered=20?= =?UTF-8?q?at=20touch=20began=20-=20memory=20management=20fixes=20-=20redu?= =?UTF-8?q?ce=20toolbox=20view=20&=20widget=20layout=20tool=20animation=20?= =?UTF-8?q?-=20fast=20widget=20profile=20loading=20-=20toolbox=20&=20widge?= =?UTF-8?q?t=20profile=20selector=20quick=20dismiss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VoidLink.xcodeproj/project.pbxproj | 4 +- VoidLink/Base.lproj/iPad.storyboard | 106 +++- VoidLink/Database/TemporaryHost.m | 2 +- VoidLink/Input/AbsoluteTouchHandler.m | 3 +- VoidLink/Input/CommandManager.swift | 36 +- VoidLink/Input/ControllerSupport.m | 8 +- .../Input/CustomOSC/LayoutOnScreenControls.h | 2 +- .../Input/CustomOSC/LayoutOnScreenControls.m | 70 ++- VoidLink/Input/CustomOSC/OSCProfile.h | 5 + VoidLink/Input/CustomOSC/OSCProfile.m | 36 +- VoidLink/Input/CustomOSC/OSCProfilesManager.m | 36 +- .../Input/CustomOSC/OnScreenButtonState.h | 10 +- .../Input/CustomOSC/OnScreenButtonState.m | 10 +- VoidLink/Input/MotionHandler.swift | 83 +-- VoidLink/Input/NativeTouchHandler.h | 2 +- VoidLink/Input/NativeTouchHandler.m | 25 +- VoidLink/Input/NativeTouchPointer.h | 1 + VoidLink/Input/NativeTouchPointer.m | 4 + VoidLink/Input/OnScreenControls.h | 35 +- VoidLink/Input/OnScreenControls.m | 513 ++++++++++-------- VoidLink/Input/OnScreenWidgetView.swift | 466 ++++++++++------ VoidLink/Input/PencilHandler.swift | 178 +++++- VoidLink/Input/PressureCurve.swift | 2 +- VoidLink/Input/PureNativeTouchHandler.m | 2 +- VoidLink/Input/RelativeTouchHandler.m | 2 +- VoidLink/Input/StreamView.h | 12 +- VoidLink/Input/StreamView.m | 357 ++++++------ VoidLink/Input/UITouchUtil.swift | 2 +- VoidLink/Localization/Localizable.xcstrings | 196 +++++++ VoidLink/Stream/StreamManager.m | 1 + VoidLink/Stream/VideoDecoderRenderer.m | 2 +- VoidLink/Utility/GraphicUtils.swift | 33 +- .../LayoutOnScreenControlsViewController.h | 5 +- .../LayoutOnScreenControlsViewController.m | 229 +++++--- .../OSCProfilesTableViewController.h | 3 +- .../OSCProfilesTableViewController.m | 30 +- .../ViewControllers/MainFrameViewController.m | 3 +- .../ViewControllers/SWRevealViewController.h | 2 +- .../ViewControllers/SWRevealViewController.m | 2 +- .../ViewControllers/SettingsViewController.h | 4 + .../ViewControllers/SettingsViewController.m | 47 +- .../StreamFrameViewController.h | 3 +- .../StreamFrameViewController.m | 67 ++- .../ToolboxViewController.swift | 132 +++-- VoidLink/mul.lproj/iPad.xcstrings | 132 +++++ 45 files changed, 2038 insertions(+), 865 deletions(-) diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index c10a1283..82a7fe8a 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -726,11 +726,11 @@ 9827E7A7251436EA00F25707 /* HapticContext.h */, 840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */, 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */, - 9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */, - 843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */, 844CD0E82C7F847E004861A7 /* PureNativeTouchHandler.h */, 844CD0EA2C7F8500004861A7 /* PureNativeTouchHandler.m */, + 843CC7F42C1E778000BBC475 /* NativeTouchHandler.h */, 843CC7F52C1E7A1700BBC475 /* NativeTouchHandler.m */, + 9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */, 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */, 9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */, 9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */, diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index c82ad121..1bad48d1 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -1950,6 +1950,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1987,6 +2027,8 @@ + + @@ -2049,6 +2091,8 @@ + + @@ -2423,7 +2467,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2852,6 +2945,8 @@ + + @@ -2870,6 +2965,7 @@ + @@ -3010,7 +3106,7 @@ - + @@ -3019,13 +3115,13 @@ - + - + - + diff --git a/VoidLink/Database/TemporaryHost.m b/VoidLink/Database/TemporaryHost.m index f49501eb..c9ba2fa3 100644 --- a/VoidLink/Database/TemporaryHost.m +++ b/VoidLink/Database/TemporaryHost.m @@ -29,7 +29,7 @@ self.address = host.address; - NSSet *classes = [NSSet setWithObjects: [NSMutableSet class], nil]; + NSSet *classes = [NSSet setWithObjects: [NSMutableSet class], [NSString class], nil]; NSError *error = nil; self.activeAddressPool = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:host.activeAddressPool error:&error]; if(self.activeAddressPool){ diff --git a/VoidLink/Input/AbsoluteTouchHandler.m b/VoidLink/Input/AbsoluteTouchHandler.m index 1efa1949..2df6295e 100644 --- a/VoidLink/Input/AbsoluteTouchHandler.m +++ b/VoidLink/Input/AbsoluteTouchHandler.m @@ -29,7 +29,7 @@ static int mouseButtonForCursorMove = BUTTON_LEFT; @implementation AbsoluteTouchHandler { - StreamView* streamView; + __weak StreamView* streamView; bool multiTouchesDetected; bool passthroughGestures; @@ -108,7 +108,6 @@ static int mouseButtonForCursorMove = BUTTON_LEFT; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { rightButtonClicked = false; - if([UITouchUtil touchesIn:streamView from:event].count>=2){ multiTouchesDetected = true; [longPressTimer invalidate]; diff --git a/VoidLink/Input/CommandManager.swift b/VoidLink/Input/CommandManager.swift index 320b3dfb..3923b22b 100644 --- a/VoidLink/Input/CommandManager.swift +++ b/VoidLink/Input/CommandManager.swift @@ -135,9 +135,22 @@ import UIKit "WIDGETTOOL", "WIDGETPROFILES", "PROFILES", + "PICKPROFILE", + "PICKPRFL", "SOFTKEYBOARD", "ABSTCHDRAG", - "PENCILHOVER" + "FOLDER", + "PRESSURECURVE", + "PENCILHOVER", + "BRUSH", + "ERASER", + "NOSINGLETOUCH" + ] + @objc public static let pencilProButtonCmds: [String] = [ + "PENCILHOVER", + "BRUSH", + "ERASER", + "NOSINGLETOUCH" ] @objc public static let motionControlButtonCmds: [String] = ["GYRO","GYROPAUSE","ACCEL","MOTION"] @@ -146,6 +159,8 @@ import UIKit @objc public static let keyboardButtonMappings: [String: Int16] = [ // Windows Key Codes "NULL": 0xFF, + "BRUSH": 0xFF, + "ERASER": 0xFF, "CTRL": 0x11, // VK_CONTROL "SHIFT": 0x10, // VK_SHIFT "ALT": 0x12, // VK_MENU @@ -431,7 +446,8 @@ import UIKit } // extractKeyStrings from keyboardCMDString - @objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? { + @objc public func extractKeyStrings(from keyboardCmd: String) -> [String]? { + let keyboardCmd = keyboardCmd.uppercased() let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|") let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$" @@ -439,14 +455,14 @@ import UIKit print("Failed to create regex") return nil } - let range = NSRange(location: 0, length: input.utf16.count) - guard let match = regex.firstMatch(in: input, options: [], range: range) else { - print("No match found for input: \(input)") + let range = NSRange(location: 0, length: keyboardCmd.utf16.count) + guard let match = regex.firstMatch(in: keyboardCmd, options: [], range: range) else { + print("No match found for input: \(keyboardCmd)") return nil } // print("Regex matched for input: \(input)") - let matchedString = (input as NSString).substring(with: match.range(at: 0)) + let matchedString = (keyboardCmd as NSString).substring(with: match.range(at: 0)) let keyStrings = matchedString.split(separator: "+").map { String($0) } guard !keyStrings.isEmpty else { @@ -479,7 +495,8 @@ import UIKit } //super combo key button strings - @objc public func extractSinglCmdStringsFromComboKeys(from input: String) -> [String]? { + @objc public func extractCmdStrings(from input: String) -> [String]? { + let input = input.uppercased() let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String }, CommandManager.oscButtonMappings.keys.map { $0 as String }, CommandManager.mouseButtonMappings.keys.map { $0 as String }, @@ -535,7 +552,7 @@ import UIKit @objc public func addCommand(_ command: RemoteCommand) -> Bool { command.cmdString = command.cmdString.uppercased() // convert all letters to upper case if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.cmdString} // copy cmd string as alias when alias is empty - let keyStrings = extractKeyStringsFromComboCommand(from: command.cmdString) + let keyStrings = extractKeyStrings(from: command.cmdString) if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false commands.append(command) saveCommands() @@ -581,8 +598,9 @@ import UIKit } } - @objc public func sendKeyComboCommand(keyboardCmdStrings: [String], delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming + @objc public func sendKeyComboCommand(keyboardCmdStrings: [String]?, delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming // 如果已处理完所有按键,则开始释放按键 + guard let keyboardCmdStrings = keyboardCmdStrings else { return } guard index < keyboardCmdStrings.count else { // 释放按键 for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键 diff --git a/VoidLink/Input/ControllerSupport.m b/VoidLink/Input/ControllerSupport.m index d00e3695..0f65679c 100644 --- a/VoidLink/Input/ControllerSupport.m +++ b/VoidLink/Input/ControllerSupport.m @@ -42,8 +42,8 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; NSLock *_controllerStreamLock; NSMutableDictionary *_voidControllers; - id _delegate; StreamConfiguration* _streamConfig; + __weak id _delegate; float accumulatedDeltaX; float accumulatedDeltaY; @@ -75,8 +75,8 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; VoidController *_oscController; NSMutableSet* _activeGCControllers; TemporarySettings* tempSettings; - OSCProfilesManager* oscProfileMan; OSCProfile* oscProfile; + OSCProfilesManager* oscProfileMan; #define EMULATING_SELECT 0x1 #define EMULATING_SPECIAL 0x2 @@ -98,7 +98,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25; bool _controllerGyroSwitchHoldPressed; ControllerGyroSwitchMode _gyroSwitchMode; - MotionHandler* motionHandler; + __weak MotionHandler* motionHandler; } // UPDATE_BUTTON_FLAG(controller, flag, pressed) @@ -1723,6 +1723,7 @@ double rc_expo(double x, double expo) { _oscController.playerIndex = 0; oscProfile = [oscProfileMan getSelectedProfile]; + motionHandler = [MotionHandler sharedWithProfile:oscProfile]; DataManager* dataMan = [[DataManager alloc] init]; tempSettings = [dataMan getSettings]; @@ -1965,7 +1966,6 @@ double rc_expo(double x, double expo) { _controllerMouseEnabledFlag = false; _gyroEnabledFlag = false; - motionHandler = [MotionHandler sharedInstance]; oscProfileMan = [OSCProfilesManager sharedManager:CGRectZero]; [self updateCommonConfig:streamConfig]; diff --git a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.h b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.h index 5aafa4f7..0df9dfb8 100644 --- a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.h +++ b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN @property UIView* _view; @property NSMutableArray *layoutChanges; @property CALayer *layerBeingDragged; -@property UIViewController *layoutToolVC; +@property (nonatomic, weak) UIViewController *layoutToolVC; - (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel; diff --git a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m index ac0e15ae..b327f22d 100644 --- a/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m +++ b/VoidLink/Input/CustomOSC/LayoutOnScreenControls.m @@ -44,7 +44,7 @@ layoutChanges = [[NSMutableArray alloc] init]; // will contain OSC button layout changes the user has made for this profile - [self drawButtons]; + // [self drawButtons]; [self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged return self; @@ -55,35 +55,45 @@ /** * This method overrides the superclass's drawButtons method. The purpose of this method is to create a dPad parent layer, and add the four dPad buttons to it so that the user can drag the entire dPad around, directional buttons and all, as one unit as is the expected behavior. Note that we do not want the four dPad buttons to be child layers of a CALayer parent layer on the game stream view since the touch logic implemented for the four dPad buttons on the game stream view is written assuming the dPad buttons are not children of another parent CALayer */ -- (void) drawButtons { - [super setDPadCenter]; // Set custom position for D-Pad here - [super setAnalogStickPositions]; // Set custom position for analog sticks here +- (void) drawButtons:(OSCProfile* )profile { + // [super regenLayerPool]; + [super setDPadCenter:profile]; // Set custom position for D-Pad here + [super setAnalogStickPositions:profile]; // Set custom position for analog sticks here [super drawBumpers]; - [super drawButtons]; + [super drawButtons:profile]; + /* + UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"]; + UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"]; + UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"]; + UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"]; + */ - UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"]; - UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"]; - UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"]; - UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"]; // create dPad background layer - self._dPadBackground = [CALayer layer]; - self._dPadBackground.name = @"dPad"; - self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X, - self.D_PAD_CENTER_Y, - self._leftButton.frame.size.width * 2 + BUTTON_DIST, - self._leftButton.frame.size.width * 2 + BUTTON_DIST); - self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here - [self.OSCButtonLayers addObject:self._dPadBackground]; - [_view.layer addSublayer:self._dPadBackground]; - - // add dPad buttons to parent layer - [self._dPadBackground addSublayer:self._downButton]; - [self._dPadBackground addSublayer:self._rightButton]; - [self._dPadBackground addSublayer:self._upButton]; - [self._dPadBackground addSublayer:self._leftButton]; - + if([self.OSCButtonLayerPool containsObject:self._upButton]){ + [self.OSCButtonLayerPool addObject:self._dPadBackground]; + self._dPadBackground.name = @"dPad"; + self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X, + self.D_PAD_CENTER_Y, + self._leftButton.frame.size.width * 2 + BUTTON_DIST, + self._leftButton.frame.size.width * 2 + BUTTON_DIST); + self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here + [self.OSCButtonLayerPool addObject:self._dPadBackground]; + [_view.layer addSublayer:self._dPadBackground]; + + // add dPad buttons to parent layer + [self._dPadBackground addSublayer:self._downButton]; + [self._dPadBackground addSublayer:self._rightButton]; + [self._dPadBackground addSublayer:self._upButton]; + [self._dPadBackground addSublayer:self._leftButton]; + } + + NSLog(@"layer name ttttttttttt============================"); + for(CALayer* buttonLayer in self.OSCButtonLayerPool){ + NSLog(@"layer name %@", buttonLayer.name); + } + [self resizeControllerLayerWith:self._dPadBackground and:_dPadSizeFactor]; /* @@ -128,7 +138,7 @@ */ horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons - for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check + for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check if ((layerBeingDragged != button) && !button.isHidden) { if ((horizontalGuideline.center.y < button.position.y + 1) && (horizontalGuideline.center.y > button.position.y - 1)) { @@ -147,7 +157,7 @@ } verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons - for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check + for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check if ((layerBeingDragged != button) && !button.isHidden) { if ((verticalGuideline.center.x < button.position.x + 1) && (verticalGuideline.center.x > button.position.x - 1)) { @@ -177,7 +187,7 @@ -Change vertical guideline color to yellow if its x-position is almost equal to that of one of the buttons on screen. */ horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons - for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check + for (CALayer *button in self.OSCButtonLayerPool) { // horizontal guideline position check if ((layerBeingDragged != button) && !button.isHidden) { if ((horizontalGuideline.center.y < button.position.y + 1) && (horizontalGuideline.center.y > button.position.y - 1)) { @@ -196,7 +206,7 @@ } } verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons - for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check + for (CALayer *button in self.OSCButtonLayerPool) { // vertical guideline position check if ((layerBeingDragged != button) && !button.isHidden) { if ((verticalGuideline.center.x < button.position.x + 1) && (verticalGuideline.center.x > button.position.x - 1)) { @@ -233,7 +243,7 @@ /* returns reference to button layer object given the button's name*/ - (CALayer*)controllerLayerFromName: (NSString*)name { - for (CALayer *buttonLayer in self.OSCButtonLayers) { + for (CALayer *buttonLayer in self.OSCButtonLayerPool) { if ([buttonLayer.name isEqualToString:name]) { return buttonLayer; diff --git a/VoidLink/Input/CustomOSC/OSCProfile.h b/VoidLink/Input/CustomOSC/OSCProfile.h index e5de1564..3edc2b26 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.h +++ b/VoidLink/Input/CustomOSC/OSCProfile.h @@ -50,6 +50,11 @@ typedef NS_ENUM(NSInteger, MapGyroTo) { @property (nonatomic, assign) int controllerGyroSwitchToggle; @property NSArray *pressureCurvePoints; @property (nonatomic, assign) bool pressureCurveEnabled; +@property (nonatomic, assign) bool doubleTapShorcutEnabled; +@property NSString *brushShortcut; +@property NSString *eraserShortcut; +@property (nonatomic, assign) bool pencilPausesNativeTouch; + - (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 e5683545..f402ab14 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.m +++ b/VoidLink/Input/CustomOSC/OSCProfile.m @@ -51,11 +51,20 @@ [encoder encodeInt:self.controllerGyroSwitchToggle forKey:@"controllerGyroSwitchToggle"]; [encoder encodeObject:self.pressureCurvePoints forKey:@"pressureCurvePoints"]; [encoder encodeBool:self.pressureCurveEnabled forKey:@"pressureCurveEnabled"]; + [encoder encodeBool:self.doubleTapShorcutEnabled forKey:@"doubleTapShorcutEnabled"]; + [encoder encodeObject:self.brushShortcut forKey:@"brushShortcut"]; + [encoder encodeObject:self.eraserShortcut forKey:@"eraserShortcut"]; + [encoder encodeBool:self.pencilPausesNativeTouch forKey:@"pencilPausesNativeTouch"]; } - (id) initWithCoder:(NSCoder*)decoder { if (self = [super init]) { - self.name = [decoder decodeObjectForKey:@"name"]; + self.name = + [decoder decodeObjectOfClasses: + [NSSet setWithObjects: + [NSString class], + nil] + forKey:@"name"]; self.buttonStatesEncoded = [decoder decodeObjectOfClasses: [NSSet setWithObjects: @@ -92,6 +101,27 @@ forKey:@"pressureCurvePoints"] : @[@0.0, @0.0, @1.0, @1.0]; self.pressureCurveEnabled = [decoder containsValueForKey:@"pressureCurveEnabled"] ? [decoder decodeBoolForKey:@"pressureCurveEnabled"] : false; + + self.doubleTapShorcutEnabled = [decoder containsValueForKey:@"doubleTapShorcutEnabled"] ? [decoder decodeBoolForKey:@"doubleTapShorcutEnabled"] : false; + self.brushShortcut = + [decoder containsValueForKey:@"brushShortcut"] + ? [decoder decodeObjectOfClasses: + [NSSet setWithObjects: + [NSString class], + nil] + forKey:@"brushShortcut"] + : @""; + + self.eraserShortcut = + [decoder containsValueForKey:@"eraserShortcut"] + ? [decoder decodeObjectOfClasses: + [NSSet setWithObjects: + [NSString class], + nil] + forKey:@"eraserShortcut"] + : @""; + + self.pencilPausesNativeTouch = [decoder containsValueForKey:@"pencilPausesNativeTouch"] ? [decoder decodeBoolForKey:@"pencilPausesNativeTouch"] : false; } return self; @@ -121,6 +151,10 @@ copy.controllerGyroSwitchToggle = self.controllerGyroSwitchToggle; copy.pressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.pressureCurvePoints copyItems:YES]; copy.pressureCurveEnabled = self.pressureCurveEnabled; + copy.doubleTapShorcutEnabled = self.doubleTapShorcutEnabled; + copy.brushShortcut = [self.brushShortcut mutableCopy]; // NSString → NSMutableString + copy.eraserShortcut = [self.eraserShortcut mutableCopy]; // NSString → NSMutableString + copy.pencilPausesNativeTouch = self.pencilPausesNativeTouch; return copy; } diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m index f3b77552..0c68ea27 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m @@ -207,7 +207,6 @@ static CGRect layoutViewBounds; #pragma mark - Getters - (NSMutableArray *) getAllProfiles { - NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil]; @@ -226,6 +225,7 @@ static CGRect layoutViewBounds; } - (OSCProfile *) getSelectedProfile { + NSLog(@"getAllProfiles test %f", CACurrentMediaTime()); NSMutableArray *profiles = [self getAllProfiles]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; @@ -344,25 +344,27 @@ static CGRect layoutViewBounds; - (NSMutableArray* ) convertOnScreenControllerAndWidgetsToButtonStates:(NSMutableArray *) oscButtonLayers { /* iterate through each OSC button the user sees on screen, create an 'OnScreenButtonState' object from each button, encode each object, and then add each object to an array */ /* - NSSet *validPositionButtonNames = [NSSet setWithObjects: - @"l2Button", - @"l1Button", - @"dPad", - @"selectButton", - @"leftStickBackground", - @"rightStickBackground", - @"r2Button", - @"r1Button", - @"aButton", - @"bButton", - @"xButton", - @"yButton", - @"startButton", - nil]; */ + NSSet *validPositionButtonNames = [NSSet setWithObjects: + @"l2Button", + @"l1Button", + @"dPad", + @"selectButton", + @"leftStickBackground", + @"rightStickBackground", + @"r2Button", + @"r1Button", + @"aButton", + @"bButton", + @"xButton", + @"yButton", + @"startButton", + nil]; */ NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init]; // save on-screen game controller buttons & sticks as buttonstate: for (CALayer *oscButtonLayer in oscButtonLayers) { + if(oscButtonLayer.isHidden) continue; + CGPoint normalizedPosition = [self normalizeWidgetPosition:oscButtonLayer.position]; OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOnScreenControls andPosition:normalizedPosition]; // add hidden attr here @@ -390,7 +392,7 @@ static CGRect layoutViewBounds; CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.center]; OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.cmdString buttonType:CustomOnScreenWidget andPosition:normalizedPosition]; buttonState.alias = widgetView.widgetLabel; - buttonState.identifier = widgetView.identifier; + buttonState.sequence = widgetView.sequence; buttonState.widthFactor = [self normalizeSizeWidthFactorWith:widgetView]; buttonState.heightFactor = [self normalizeSizeHeightFactorWith:widgetView]; buttonState.componentSizeFactor = [self normalizeComponentSizeFactorWith:widgetView]; diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.h b/VoidLink/Input/CustomOSC/OnScreenButtonState.h index b2cc7150..e2c2a3c0 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.h +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.h @@ -24,7 +24,10 @@ NS_ASSUME_NONNULL_BEGIN @property NSString *name; @property NSString *alias; -@property NSString *identifier; +@property (nonatomic, assign) BOOL folded; +@property (nonatomic, assign) uint8_t revealMode; +@property (nonatomic, assign) int16_t sequence; +@property NSMutableSet *sequenceSet; @property CGPoint position; @property (nonatomic, assign) BOOL isHidden; @property (nonatomic, assign) uint8_t buttonMode; @@ -86,6 +89,11 @@ typedef NS_ENUM(NSInteger, ButtonMode) { movable }; +typedef NS_ENUM(NSInteger, RevealMode) { + coexist, + exclusive +}; + - (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position; + (BOOL) supportsSecureCoding; diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.m b/VoidLink/Input/CustomOSC/OnScreenButtonState.m index 146b1b47..080d3762 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.m +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.m @@ -32,7 +32,10 @@ - (void) encodeWithCoder:(NSCoder*)encoder { [encoder encodeObject:self.name forKey:@"name"]; - [encoder encodeObject:([self.identifier isEqualToString:@""]||!self.identifier) ? [UUIDHelper newUUID] : self.identifier forKey:@"identifier"]; + [encoder encodeBool:self.folded forKey:@"folded"]; + [encoder encodeInt32:self.revealMode forKey:@"revealMode"]; + [encoder encodeInt32:self.sequence forKey:@"sequence"]; + [encoder encodeObject:self.sequenceSet forKey:@"sequenceSet"]; [encoder encodeObject:self.alias forKey:@"alias"]; [encoder encodeInt:self.widgetType forKey:@"buttonType"]; // keep original key [encoder encodeInt:self.sizeReference forKey:@"sizeReference"]; @@ -69,7 +72,10 @@ - (id) initWithCoder:(NSCoder*)decoder { if (self = [super init]) { self.name = [decoder decodeObjectForKey:@"name"]; - self.identifier = [decoder containsValueForKey:@"identifier"] ? [decoder decodeObjectForKey:@"identifier"] : [UUIDHelper newUUID]; + self.folded = [decoder containsValueForKey:@"folded"] ? [decoder decodeBoolForKey:@"folded"] : false; + self.revealMode = [decoder containsValueForKey:@"revealMode"] ? [decoder decodeInt32ForKey:@"revealMode"] : exclusive; + self.sequence = [decoder containsValueForKey:@"sequence"] ? [decoder decodeInt32ForKey:@"sequence"] : -1; + self.sequenceSet = [decoder containsValueForKey:@"sequenceSet"] ? [decoder decodeObjectForKey:@"sequenceSet"] : [NSSet set]; self.alias = [decoder decodeObjectForKey:@"alias"]; self.widgetType = [decoder decodeIntForKey:@"buttonType"]; self.sizeReference = [decoder containsValueForKey:@"sizeReference"] ? [decoder decodeIntForKey:@"sizeReference"] : longSide; diff --git a/VoidLink/Input/MotionHandler.swift b/VoidLink/Input/MotionHandler.swift index ca3f4040..fc585f09 100644 --- a/VoidLink/Input/MotionHandler.swift +++ b/VoidLink/Input/MotionHandler.swift @@ -15,7 +15,7 @@ import CoreMotion func gyroMixInputStarted() -> Bool } -@objc class MotionHandler: NSObject, OscInstanceReceiverDelegate, OnScreenWidgetStickMixedInputDelegate{ +@objc class MotionHandler: NSObject, OnScreenWidgetStickMixedInputDelegate{ public func mixOnScreenRightStickAndGyroInput(x: CGFloat, y: CGFloat) { rightStickTouchInputX = x rightStickTouchInputY = y @@ -28,48 +28,46 @@ import CoreMotion } @objc public func mixPhysicalRightStickAndGyroInput(x: CGFloat, y: CGFloat) { - rightStickPhysicalInputX = oscProfile.synthesizePhysicalStick ? x : 0 - rightStickPhysicalInputY = oscProfile.synthesizePhysicalStick ? y : 0 + rightStickPhysicalInputX = synthesizePhysicalStick ? x : 0 + rightStickPhysicalInputY = synthesizePhysicalStick ? y : 0 } @objc public func mixPhysicalLeftStickAndGyroInput(x: CGFloat, y: CGFloat) { // rollIntegral = 0 - leftStickPhysicalInputX = oscProfile.synthesizePhysicalStick ? x : 0 - leftStickPhysicalInputY = oscProfile.synthesizePhysicalStick ? y : 0 + leftStickPhysicalInputX = synthesizePhysicalStick ? x : 0 + leftStickPhysicalInputY = synthesizePhysicalStick ? y : 0 } public func gyroMixInputStarted() -> Bool { return gyroIsWorking } - func getOnScreenControlsInstance(_ sender: Any!) { - if let controls = sender as? OnScreenControls { - self.onScreenControls = controls - print("ClassA received OnScreenControls instance: \(controls)") - } else { - print("ClassA received an unknown sender") - } - } + private static let sharedInstance = MotionHandler() - static let shared = MotionHandler() - - @objc class func sharedInstance() -> MotionHandler { - let sharedInstance = MotionHandler.shared - sharedInstance.oscProfile = sharedInstance.oscProfileMan.getSelectedProfile() - sharedInstance.sensitvityYaw = sharedInstance.oscProfile.gyroSensitivityYaw - sharedInstance.sensitvityPitch = sharedInstance.oscProfile.gyroSensitivityPitch - sharedInstance.sensitvityRoll = sharedInstance.oscProfile.gyroSensitivityRoll - sharedInstance.gyroToStickMinOffset = sharedInstance.oscProfile.gyroToStickMinOffset + @objc class func shared(profile: OSCProfile?) -> MotionHandler { + let sharedInstance = MotionHandler.sharedInstance + guard let profile = profile else { return sharedInstance } + sharedInstance.mapGyroTo = profile.mapGyroTo + sharedInstance.synthesizePhysicalStick = profile.synthesizePhysicalStick + sharedInstance.rollToLeftStick = profile.rollToLeftStick + sharedInstance.yawPitchToRightStick = profile.yawPitchToRightStick + sharedInstance.sensitvityYaw = profile.gyroSensitivityYaw + sharedInstance.sensitvityPitch = profile.gyroSensitivityPitch + sharedInstance.sensitvityRoll = profile.gyroSensitivityRoll + sharedInstance.gyroToStickMinOffset = profile.gyroToStickMinOffset + sharedInstance.onScreenControls = OnScreenControls.shared() return sharedInstance } - private var oscProfileMan: OSCProfilesManager - private var oscProfile: OSCProfile - @objc public var gyroControlStarted: Bool = false private var gyroIsWorking: Bool = false private var accelControlStarted: Bool = false private let motionManager = CMMotionManager() + + private var synthesizePhysicalStick:Bool = false + private var mapGyroTo:MapGyroTo = .mapGyroToControllerStick + private var rollToLeftStick:Bool = false + private var yawPitchToRightStick:Bool = false public var sensitvityYaw:CGFloat = 1.0 public var sensitvityPitch:CGFloat = 1.0 public var sensitvityRoll:CGFloat = 1.0 @@ -82,7 +80,7 @@ import CoreMotion public var gyroStarter:Any? private var windowScene: Any? - @objc public var onScreenControls: OnScreenControls = OnScreenControls.init() + @objc public var onScreenControls: OnScreenControls? public let stickMaxOffset: CGFloat = 0x7FFE private var stickInputScale: CGFloat = 35 @@ -124,8 +122,7 @@ import CoreMotion } @objc public override init() { - self.oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) - self.oscProfile = oscProfileMan.getSelectedProfile() + let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) super.init() if #available(iOS 13.0, *) { let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene @@ -295,14 +292,16 @@ import CoreMotion gyroIsWorking = true - if oscProfile.mapGyroTo == MapGyroTo.mapGyroToMouse { + if mapGyroTo == MapGyroTo.mapGyroToMouse { yaw = yawSource*sensitvityYaw*widgetYawFactor*30 pitch = pitchSource*sensitvityPitch*widgetPitchFactor*30 LiSendMouseMoveEvent(Int16(yaw),Int16(pitch)) } - - if oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick { - if oscProfile.yawPitchToRightStick { + + //guard let onScreenControls = onScreenControls else { return } + + if mapGyroTo == MapGyroTo.mapGyroToControllerStick { + if yawPitchToRightStick { gyroToStickOffset.dx = gyroInputToStickInput(input:yawSource*sensitvityYaw*widgetYawFactor*10) yaw = rightStickTouchInputX + rightStickPhysicalInputX + gyroToStickOffset.dx yaw = self.clampStickInput(input: yaw) @@ -313,9 +312,9 @@ import CoreMotion let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: yaw, dy: pitch), minOffset: gyroToStickMinOffset) - self.onScreenControls.sendRightStickTouchPadEvent(offsetVector.dx, offsetVector.dy) + onScreenControls?.sendRightStickTouchPadEvent(offsetVector.dx, offsetVector.dy) } - if oscProfile.rollToLeftStick { + if rollToLeftStick { roll = gyroInputToStickInput(input:rollSource*sensitvityRoll*widgetRollFactor*0.2) rollIntegral = rollIntegral + roll @@ -324,24 +323,26 @@ import CoreMotion let offsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: mixedLeftStickOffsetX, dy: mixedLeftStickOffsetY), minOffset: gyroToStickMinOffset) - self.onScreenControls.sendLeftStickTouchPadEvent(offsetVector.dx, offsetVector.dy) + onScreenControls?.sendLeftStickTouchPadEvent(offsetVector.dx, offsetVector.dy) } } } private func clearGyroInput(interruptNonGyroInput:Bool, resetLeftStick:Bool=false){ - if oscProfile.yawPitchToRightStick{ - self.onScreenControls.sendRightStickTouchPadEvent(rightStickPhysicalInputX+rightStickTouchInputX-yawBias, rightStickPhysicalInputY+rightStickTouchInputY-pitchBias) + // guard let onScreenControls = onScreenControls else { return } + + if yawPitchToRightStick{ + onScreenControls?.sendRightStickTouchPadEvent(rightStickPhysicalInputX+rightStickTouchInputX-yawBias, rightStickPhysicalInputY+rightStickTouchInputY-pitchBias) } - if oscProfile.rollToLeftStick{ + if rollToLeftStick{ if resetLeftStick { rollIntegral = 0 - self.onScreenControls.sendLeftStickTouchPadEvent(leftStickPhysicalInputX+leftStickTouchInputX-rollBias,leftStickPhysicalInputY+leftStickTouchInputY) + onScreenControls?.sendLeftStickTouchPadEvent(leftStickPhysicalInputX+leftStickTouchInputX-rollBias,leftStickPhysicalInputY+leftStickTouchInputY) } } if(interruptNonGyroInput){ - self.onScreenControls.clearLeftStickTouchPadFlag() - self.onScreenControls.clearRightStickTouchPadFlag() + onScreenControls?.clearLeftStickTouchPadFlag() + onScreenControls?.clearRightStickTouchPadFlag() } } diff --git a/VoidLink/Input/NativeTouchHandler.h b/VoidLink/Input/NativeTouchHandler.h index 1df681e1..a59d9fc6 100644 --- a/VoidLink/Input/NativeTouchHandler.h +++ b/VoidLink/Input/NativeTouchHandler.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) NSMutableSet* touchesCapturedByOnScreenButtons; - (id)initWithView:(StreamView* )view andSettings:(TemporarySettings*)settings; - +- (void)setAllowSingleTouchEnabled:(BOOL)enabled; @end diff --git a/VoidLink/Input/NativeTouchHandler.m b/VoidLink/Input/NativeTouchHandler.m index 5548a1ae..69f70322 100644 --- a/VoidLink/Input/NativeTouchHandler.m +++ b/VoidLink/Input/NativeTouchHandler.m @@ -15,9 +15,10 @@ #include @implementation NativeTouchHandler { - StreamView* streamView; TemporarySettings* currentSettings; + __weak StreamView* streamView; bool activateCoordSelector; + bool singleTouchDisabled; CGFloat pointerVelocityDividerLocationByPoints; bool asyncNativeTouch; @@ -187,10 +188,18 @@ } -- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{ +- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType withEvent:(UIEvent *)event{ //if(touchPointSpawnedAtUpperScreenEdge && touchType != LI_TOUCH_EVENT_UP) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash if([blacklistedTouches containsObject:@((uintptr_t)touch)]) return; + if(singleTouchDisabled + && [UITouchUtil touchesIn:streamView from:event].count != 2 + && touchType != LI_TOUCH_EVENT_UP) return; + + if(PencilHandler.pencilPausesNativeTouch + && PencilHandler.isDrawing + && touchType != LI_TOUCH_EVENT_UP) return; + CGPoint targetCoords; //NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved); // NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]); @@ -216,7 +225,7 @@ if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue; [self handleTouchDown:touch]; //generate & populate pointerId if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch]; - [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN]; + [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN withEvent:event]; } }); else{ @@ -225,7 +234,7 @@ if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue; [self handleTouchDown:touch]; //generate & populate pointerId if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch]; - [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN]; + [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN withEvent:event]; } } } @@ -235,7 +244,7 @@ for (UITouch* touch in touches){ if([OnScreenControls.touchesCapturedByOnScreenControls containsObject:touch]) continue; if(self->activateCoordSelector) [self updatePointerObjInDict:touch]; - [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE]; + [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE withEvent:event]; [[self getPointerObjFromDict:touch] doesNeedResetCoords]; } }); @@ -248,7 +257,7 @@ [OnScreenControls.touchesCapturedByOnScreenControls removeObject:touch]; continue; } - [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId + [self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP withEvent:event]; //send touch event before remove pointerId [self removePointerId:touch]; //then remove pointerId if(self->activateCoordSelector) [self removePointerObjFromDict:touch]; [self->blacklistedTouches removeObject:@((uintptr_t)touch)]; @@ -289,4 +298,8 @@ return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint; } +- (void)setAllowSingleTouchEnabled:(BOOL)enabled{ + singleTouchDisabled = !enabled; +} + @end diff --git a/VoidLink/Input/NativeTouchPointer.h b/VoidLink/Input/NativeTouchPointer.h index 6cd73ea9..c8d0cfce 100644 --- a/VoidLink/Input/NativeTouchPointer.h +++ b/VoidLink/Input/NativeTouchPointer.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings; ++ (void)cleanUpContext; - (bool)doesNeedResetCoords; - (void)updatePointerCoords:(UITouch *)touch; diff --git a/VoidLink/Input/NativeTouchPointer.m b/VoidLink/Input/NativeTouchPointer.m index 2ad67d1a..bc5e16f4 100644 --- a/VoidLink/Input/NativeTouchPointer.m +++ b/VoidLink/Input/NativeTouchPointer.m @@ -63,4 +63,8 @@ static StreamView* streamView; NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight); } ++ (void)cleanUpContext { + streamView = nil; +} + @end diff --git a/VoidLink/Input/OnScreenControls.h b/VoidLink/Input/OnScreenControls.h index 8fdcc2be..3c882bf1 100644 --- a/VoidLink/Input/OnScreenControls.h +++ b/VoidLink/Input/OnScreenControls.h @@ -17,12 +17,7 @@ @class ControllerSupport; @class StreamConfiguration; - -// sending self as an instance to OnScreenWidgetView or other related classes -@protocol OscInstanceReceiverDelegate -- (void)getOnScreenControlsInstance:(id)sender; // Renamed delegate method -@end - +@class LayoutOnScreenControls; static const float D_PAD_DIST = 10; static const float BUTTON_DIST = 20; @@ -52,11 +47,6 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) { }; - - -@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 @property (nonatomic, assign) bool isLayingOut; //@property (nonatomic) NSMutableSet* touchesCapturedByOnScreenButtons; @@ -105,7 +95,7 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) { @property OnScreenControlsLevel _level; -@property NSMutableArray *OSCButtonLayers; +@property NSMutableSet *OSCButtonLayerPool; @property NSMutableDictionary* layerVibrationStyleDic; @@ -128,18 +118,19 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) { - (BOOL) handleTouchUpEvent:(NSSet*)touches; - (BOOL) handleTouchMovedEvent:(NSSet*)touches; - (void) setLevel:(OnScreenControlsLevel)level; -- (void) show; +- (void) showLegacyWidgetsWith:(OSCProfile* )profile; +- (void) regenLayerPool; - (void) setupComplexControls; -- (void) drawButtons; +- (void) drawButtons:(OSCProfile* _Nullable)profile; - (void) drawBumpers; -- (void) updateControls; +- (void) updateLegacyWidgetsWith:(OSCProfile* _Nullable )profile; - (OnScreenControlsLevel) getLevel; -- (void) setDPadCenter; -- (void) setAnalogStickPositions; -- (void) positionAndResizeSingleControllerLayers; -- (void) resizeControllerLayerWith:(CALayer*)layer and:(CGFloat)sizeFactor; -- (void) adjustControllerLayerOpacityWith:(CALayer*)layer and:(CGFloat)alpha; -+ (CGFloat) getControllerLayerSizeFactor:(CALayer*)layer; -- (CGFloat) getControllerLayerOpacity:(CALayer*)layer; +- (void) setDPadCenter:(OSCProfile *_Nonnull)profile;; +- (void) setAnalogStickPositions:(OSCProfile *_Nonnull)profile; +- (void) positionAndResizeSingleControllerLayers:(OSCProfile *_Nonnull)profile; +- (void) resizeControllerLayerWith:(CALayer*_Nonnull)layer and:(CGFloat)sizeFactor; +- (void) adjustControllerLayerOpacityWith:(CALayer*_Nonnull)layer and:(CGFloat)alpha; ++ (CGFloat) getControllerLayerSizeFactor:(CALayer*_Nonnull)layer; +- (CGFloat) getControllerLayerOpacity:(CALayer*_Nonnull)layer; @end diff --git a/VoidLink/Input/OnScreenControls.m b/VoidLink/Input/OnScreenControls.m index b770466f..04adb6fd 100644 --- a/VoidLink/Input/OnScreenControls.m +++ b/VoidLink/Input/OnScreenControls.m @@ -10,6 +10,7 @@ // #import "OnScreenControls.h" +#import "LayoutOnScreenControls.h" #import "CustomTapGestureRecognizer.h" #import "VoidController.h" #include "Limelight.h" @@ -105,7 +106,7 @@ static NSSet *validPositionButtonNames; @synthesize _l2Button; @synthesize _l3Button; @synthesize _level; -@synthesize OSCButtonLayers; +@synthesize OSCButtonLayerPool; @synthesize _dPadBackground; static const float EDGE_WIDTH = .05; @@ -203,16 +204,6 @@ static float L3_Y; [_controllerSupport updateFinished:_controller]; } -// sending self as an instance to OnScreenWidgetView -- (void)sendInstance{ - NSLog(@"OnScreenControls is sending its instance..."); - if ([self.instanceReceiverDelegate respondsToSelector:@selector(getOnScreenControlsInstance:)]) { - [self.instanceReceiverDelegate getOnScreenControlsInstance:self]; - } else { - NSLog(@"Delegate not set or does not respond to getOnScreenControlsInstance:"); - } -} - - (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig { self = [self init]; self.isLayingOut = false; // set false by default (play mode instead of layout mode) @@ -220,7 +211,7 @@ static float L3_Y; profilesManager = [OSCProfilesManager sharedManager:view.bounds]; - self.OSCButtonLayers = [[NSMutableArray alloc] init]; + self.OSCButtonLayerPool = [[NSMutableSet alloc] init]; if (controllerSupport) { _controllerSupport = controllerSupport; @@ -272,28 +263,10 @@ static float L3_Y; _rightStickBackground = [CALayer layer]; _leftStick = [CALayer layer]; _rightStick = [CALayer layer]; - - [self.OSCButtonLayers addObject:_aButton]; - [self.OSCButtonLayers addObject:_bButton]; - [self.OSCButtonLayers addObject:_xButton]; - [self.OSCButtonLayers addObject:_yButton]; - [self.OSCButtonLayers addObject:_startButton]; - [self.OSCButtonLayers addObject:_selectButton]; - [self.OSCButtonLayers addObject:_r1Button]; - [self.OSCButtonLayers addObject:_r2Button]; - [self.OSCButtonLayers addObject:_r3Button]; - [self.OSCButtonLayers addObject:_l1Button]; - [self.OSCButtonLayers addObject:_l2Button]; - [self.OSCButtonLayers addObject:_l3Button]; - [self.OSCButtonLayers addObject:_upButton]; - [self.OSCButtonLayers addObject:_downButton]; - [self.OSCButtonLayers addObject:_leftButton]; - [self.OSCButtonLayers addObject:_rightButton]; - [self.OSCButtonLayers addObject:_leftStickBackground]; - [self.OSCButtonLayers addObject:_rightStickBackground]; - [self.OSCButtonLayers addObject:_leftStick]; - [self.OSCButtonLayers addObject:_rightStick]; + _dPadBackground = [CALayer layer]; + [self regenLayerPool]; + /* Name button layers to allow us to more easily associate them with 'OnScreenButtonState' objects by comparing their name properties */ _leftStickBackground.name = @"leftStickBackground"; _rightStickBackground.name = @"rightStickBackground"; @@ -315,6 +288,7 @@ static float L3_Y; _rightButton.name = @"rightButton"; _downButton.name = @"downButton"; _leftButton.name = @"leftButton"; + _dPadBackground.name = @"dPad"; _standardRoundButtonBounds = standardRoundButtonBounds = CGRectMake(0, 0, [UIImage imageNamed:@"AButton"].size.width, [UIImage imageNamed:@"AButton"].size.height); @@ -326,14 +300,43 @@ static float L3_Y; _activeCustomOscButtonPositionDict = [[NSMutableDictionary alloc] init]; touchesCapturedByOnScreenControls = [[NSMutableSet alloc] init]; - + + if(![self isKindOfClass:[LayoutOnScreenControls class]]) OnScreenControls.shared = self; + return self; } -- (void) show { +- (void)regenLayerPool{ + for(CALayer* layer in self.OSCButtonLayerPool){ + [layer removeFromSuperlayer]; + } + [self.OSCButtonLayerPool removeAllObjects]; + [self.OSCButtonLayerPool addObject:_aButton]; + [self.OSCButtonLayerPool addObject:_bButton]; + [self.OSCButtonLayerPool addObject:_xButton]; + [self.OSCButtonLayerPool addObject:_yButton]; + [self.OSCButtonLayerPool addObject:_startButton]; + [self.OSCButtonLayerPool addObject:_selectButton]; + [self.OSCButtonLayerPool addObject:_r1Button]; + [self.OSCButtonLayerPool addObject:_r2Button]; + // [self.OSCButtonLayerPool addObject:_r3Button]; + [self.OSCButtonLayerPool addObject:_l1Button]; + [self.OSCButtonLayerPool addObject:_l2Button]; + // [self.OSCButtonLayerPool addObject:_l3Button]; + [self.OSCButtonLayerPool addObject:_upButton]; + [self.OSCButtonLayerPool addObject:_downButton]; + [self.OSCButtonLayerPool addObject:_leftButton]; + [self.OSCButtonLayerPool addObject:_rightButton]; + [self.OSCButtonLayerPool addObject:_leftStickBackground]; + [self.OSCButtonLayerPool addObject:_rightStickBackground]; + [self.OSCButtonLayerPool addObject:_leftStick]; + [self.OSCButtonLayerPool addObject:_rightStick]; +} + +- (void) showLegacyWidgetsWith:(OSCProfile* )profile { _visible = YES; - - [self updateControls]; + NSLog(@"show updateControls %f", CACurrentMediaTime()); + [self updateLegacyWidgetsWith:profile]; } - (void) setLevel:(OnScreenControlsLevel)level { @@ -342,7 +345,7 @@ static float L3_Y; // Only update controls if we're showing, otherwise // show will do it for us. if (_visible) { - [self updateControls]; + [self updateLegacyWidgetsWith:nil]; } } @@ -359,13 +362,20 @@ static float L3_Y; return position; } -- (void) updateControls { - if(self._level == OnScreenControlsLevelCustom){ +- (void) updateLegacyWidgetsWith:(OSCProfile* _Nullable)profile { + if(self._level == OnScreenControlsLevelCustom && profile){ // mark all OSC buttons that has valid coords of positions - validPositionButtonNames = [NSSet setWithObjects: + [self regenLayerPool]; + [_activeCustomOscButtonPositionDict removeAllObjects]; //reset the Dict. + + NSSet* persistedButtonNames = [NSSet setWithObjects: @"l2Button", @"l1Button", @"dPad", + @"upButton", + @"downButton", + @"leftButton", + @"rightButton", @"selectButton", @"leftStickBackground", @"rightStickBackground", @@ -378,15 +388,29 @@ static float L3_Y; @"startButton", nil]; + NSSet* singleLayerButtons = [NSSet setWithObjects: + @"l2Button", + @"l1Button", + @"selectButton", + @"r2Button", + @"r1Button", + @"aButton", + @"bButton", + @"xButton", + @"yButton", + @"startButton", + nil]; + // _activeCustomOscButtonPositionDict will be updated every time when the osc profile is reloaded - OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile - [_activeCustomOscButtonPositionDict removeAllObjects]; //reset the Dict. + // if(!profile) profile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile + // NSLog(@"_activeCustomOscButtonPositionDict update: STARTOVER"); - for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { + for (NSData *buttonStateEncoded in profile.buttonStatesEncoded) { OnScreenButtonState* buttonState = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; + if(!buttonState) continue; buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; [OnScreenControls.layerVibrationStyleDic setObject:@(buttonState.vibrationStyle) forKey:buttonState.name]; - if(!buttonState.isHidden && [validPositionButtonNames containsObject:buttonState.name] && (buttonState.widgetType == LegacyOnScreenControls || [profilesManager getIndexOfSelectedProfile] == 0 ) ){ + if(!buttonState.isHidden && [persistedButtonNames containsObject:buttonState.name] && buttonState.widgetType == LegacyOnScreenControls){ [_activeCustomOscButtonPositionDict setObject:[NSValue valueWithCGPoint:buttonState.position] forKey:buttonState.name]; // we got a buttonname -> position dict here // NSLog(@"_activeCustomOscButtonPositionDict update, button name:%@, position: %f, %f", buttonState.name, buttonState.position.x, buttonState.position.y); } @@ -410,6 +434,33 @@ static float L3_Y; } } //NSLog(@"_activeCustomOscButtonPositionDict update, active button number: %lu", (unsigned long)[_activeCustomOscButtonPositionDict count]); + NSSet* tempLayerPool = [self.OSCButtonLayerPool copy]; + + for (CALayer* button in tempLayerPool){ + if([singleLayerButtons containsObject:button.name] + && ![_activeCustomOscButtonPositionDict.allKeys containsObject:button.name]) [OSCButtonLayerPool removeObject:button]; + if(![_activeCustomOscButtonPositionDict.allKeys containsObject:@"upButton"]){ + [OSCButtonLayerPool removeObject:_dPadBackground]; + [OSCButtonLayerPool removeObject:_upButton]; + [OSCButtonLayerPool removeObject:_downButton]; + [OSCButtonLayerPool removeObject:_leftButton]; + [OSCButtonLayerPool removeObject:_rightButton]; + } + if(![_activeCustomOscButtonPositionDict.allKeys containsObject:@"leftStickBackground"]){ + [OSCButtonLayerPool removeObject:_leftStick]; + [OSCButtonLayerPool removeObject:_leftStickBackground]; + } + if(![_activeCustomOscButtonPositionDict.allKeys containsObject:@"rightStickBackground"]){ + [OSCButtonLayerPool removeObject:_rightStick]; + [OSCButtonLayerPool removeObject:_rightStickBackground]; + } + } + + NSLog(@"OSCButtonLayerPool count: %ld", OSCButtonLayerPool.count); + for(CALayer* layer in OSCButtonLayerPool){ + NSLog(@"OSCButtonLayerPool name: %@", layer.name); + } + } else{ // deal with simple & full mode configurations @@ -472,13 +523,13 @@ static float L3_Y; [self hideBumpers]; [self hideSticks]; [self drawStartSelect]; - [self drawButtons]; + [self drawButtons:profile]; [self setOpacityForStandardControllerLayers]; break; case OnScreenControlsLevelFull: [self setupComplexControls]; - [self drawButtons]; + [self drawButtons:profile]; [self drawStartSelect]; [self drawBumpers]; [self drawTriggers]; @@ -489,15 +540,15 @@ static float L3_Y; case OnScreenControlsLevelCustom: [self setupComplexControls]; // Default postion for D-Pad set here - [self setDPadCenter]; // Custom position for D-Pad set here - [self setAnalogStickPositions]; // Custom position for analog sticks set here - [self drawButtons]; + [self setDPadCenter:profile]; // Custom position for D-Pad set here + [self setAnalogStickPositions:profile]; // Custom position for analog sticks set here + [self drawButtons:profile]; [self drawStartSelect]; [self drawBumpers]; [self drawTriggers]; [self drawSticks]; - [self positionAndResizeSingleControllerLayers]; - [self setOpacityForCutsomControllerLayers]; + [self positionAndResizeSingleControllerLayers:profile]; + [self setOpacityForCutsomControllerLayers:profile]; break; default: @@ -506,9 +557,11 @@ static float L3_Y; } // populate the controllerLayer.name -> Opacity dictionary... have to do this in order to make touchdown visual effect consistent - for (CALayer* controllerLayer in self.OSCButtonLayers){ + for (CALayer* controllerLayer in self.OSCButtonLayerPool){ [_originalControllerLayerOpacityDict setObject:@(controllerLayer.opacity) forKey:controllerLayer.name]; } + + [self removeHiddenlayers]; } // For GCExtendedGamepad controls we move start, select, L3, and R3 to the button @@ -635,7 +688,7 @@ static float L3_Y; } } -- (void) drawButtons { +- (void) drawButtons:(OSCProfile* )profile { UIImage* aButtonImage = [UIImage imageNamed:@"AButton"]; UIImage* aButtonHdImage = [UIImage imageNamed:@"AButtonHD"]; UIImage* bButtonImage = [UIImage imageNamed:@"BButton"]; @@ -644,7 +697,7 @@ static float L3_Y; UIImage* xButtonHdImage = [UIImage imageNamed:@"XButtonHD"]; UIImage* yButtonImage = [UIImage imageNamed:@"YButton"]; UIImage* yButtonHdImage = [UIImage imageNamed:@"YButtonHD"]; - + CGRect aButtonFrame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height); @@ -653,33 +706,41 @@ static float L3_Y; CGRect yButtonFrame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height); // create A button - _aButton.contents = (id) aButtonHdImage.CGImage; - // Set the filtering mode for smooth downscaling - _aButton.minificationFilter = kCAFilterLinear; // Smooth scaling down - _aButton.magnificationFilter = kCAFilterLinear; // Smooth scaling up (if needed) - _aButton.minificationFilterBias = 0.0f; // Trilinear-like effect - - // Enable anti-aliasing for smoother edges - _aButton.allowsEdgeAntialiasing = YES; - _aButton.edgeAntialiasingMask = kCALayerLeftEdge | kCALayerRightEdge | kCALayerBottomEdge | kCALayerTopEdge; - - _aButton.frame = _swapABXY ? bButtonFrame : aButtonFrame; - [_view.layer addSublayer:_aButton]; // rendering OSC Button here + if([OSCButtonLayerPool containsObject:_aButton]){ + _aButton.contents = (id) aButtonHdImage.CGImage; + // Set the filtering mode for smooth downscaling + _aButton.minificationFilter = kCAFilterLinear; // Smooth scaling down + _aButton.magnificationFilter = kCAFilterLinear; // Smooth scaling up (if needed) + _aButton.minificationFilterBias = 0.0f; // Trilinear-like effect + + // Enable anti-aliasing for smoother edges + _aButton.allowsEdgeAntialiasing = YES; + _aButton.edgeAntialiasingMask = kCALayerLeftEdge | kCALayerRightEdge | kCALayerBottomEdge | kCALayerTopEdge; + + _aButton.frame = _swapABXY ? bButtonFrame : aButtonFrame; + [_view.layer addSublayer:_aButton]; // rendering OSC Button here + } // create B button - _bButton.frame = _swapABXY ? aButtonFrame : bButtonFrame; - _bButton.contents = (id) bButtonHdImage.CGImage; - [_view.layer addSublayer:_bButton]; + if([OSCButtonLayerPool containsObject:_bButton]){ + _bButton.frame = _swapABXY ? aButtonFrame : bButtonFrame; + _bButton.contents = (id) bButtonHdImage.CGImage; + [_view.layer addSublayer:_bButton]; + } // create X Button - _xButton.frame = _swapABXY ? yButtonFrame : xButtonFrame; - _xButton.contents = (id) xButtonHdImage.CGImage; - [_view.layer addSublayer:_xButton]; + if([OSCButtonLayerPool containsObject:_xButton]){ + _xButton.frame = _swapABXY ? yButtonFrame : xButtonFrame; + _xButton.contents = (id) xButtonHdImage.CGImage; + [_view.layer addSublayer:_xButton]; + } // create Y Button - _yButton.frame = _swapABXY ? xButtonFrame : yButtonFrame; - _yButton.contents = (id) yButtonHdImage.CGImage; - [_view.layer addSublayer:_yButton]; + if([OSCButtonLayerPool containsObject:_yButton]){ + _yButton.frame = _swapABXY ? xButtonFrame : yButtonFrame; + _yButton.contents = (id) yButtonHdImage.CGImage; + [_view.layer addSublayer:_yButton]; + } if(self._level == OnScreenControlsLevelFull || @@ -687,59 +748,58 @@ static float L3_Y; _dPadSizeFactor = 1.0; } - // Calculate the distances of each button from the shared center based on their transformed positions - CGFloat newDPadDistFactor = 0.2/_dPadSizeFactor; - CGFloat newLongSideLength = standardLeftRightButtonBounds.size.width * _dPadSizeFactor; - CGFloat newShortSideLength = standardLeftRightButtonBounds.size.height * _dPadSizeFactor; - CGPoint sharedCenter = CGPointMake(D_PAD_CENTER_X, D_PAD_CENTER_Y); // this will anchor the center point of the dPad - - // dPad buttons are resized HERE // up button // UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"]; - UIImage* upButtonHdImage = [UIImage imageNamed:@"UpButtonHD"]; - _upButton.contents = (id) upButtonHdImage.CGImage; - [_view.layer addSublayer:_upButton]; - _upButton.anchorPoint = CGPointMake(0.5, 1 + newDPadDistFactor); - _upButton.bounds = CGRectMake(0, 0, newShortSideLength, newLongSideLength); - _upButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); - - // down button - // UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"]; - UIImage* downButtonHdImage = [UIImage imageNamed:@"DownButtonHD"]; - _downButton.contents = (id) downButtonHdImage.CGImage; - [_view.layer addSublayer:_downButton]; - // Resize and reposition the button - _downButton.anchorPoint = CGPointMake(0.5, 0 - newDPadDistFactor); - _downButton.bounds = CGRectMake(0, 0, newShortSideLength, newLongSideLength); - _downButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); - - // left button - // UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"]; - UIImage* leftButtonHdImage = [UIImage imageNamed:@"LeftButtonHD"]; - _leftButton.contents = (id) leftButtonHdImage.CGImage; - [_view.layer addSublayer:_leftButton]; - _leftButton.anchorPoint = CGPointMake(1 + newDPadDistFactor, 0.5); - _leftButton.bounds = CGRectMake(0, 0, newLongSideLength, newShortSideLength); - _leftButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); - - // right button - // UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"]; - UIImage* rightButtonHdImage = [UIImage imageNamed:@"RightButtonHD"]; - _rightButton.contents = (id) rightButtonHdImage.CGImage; - [_view.layer addSublayer:_rightButton]; - _rightButton.anchorPoint = CGPointMake(0 - newDPadDistFactor, 0.5); - _rightButton.bounds = CGRectMake(0, 0, newLongSideLength, newShortSideLength); - _rightButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); + if([_activeCustomOscButtonPositionDict.allKeys containsObject:@"dPad"]){ + // Calculate the distances of each button from the shared center based on their transformed positions + CGFloat newDPadDistFactor = 0.2/_dPadSizeFactor; + CGFloat newLongSideLength = standardLeftRightButtonBounds.size.width * _dPadSizeFactor; + CGFloat newShortSideLength = standardLeftRightButtonBounds.size.height * _dPadSizeFactor; + CGPoint sharedCenter = CGPointMake(D_PAD_CENTER_X, D_PAD_CENTER_Y); // this will anchor the center point of the dPad + + UIImage* upButtonHdImage = [UIImage imageNamed:@"UpButtonHD"]; + _upButton.contents = (id) upButtonHdImage.CGImage; + [_view.layer addSublayer:_upButton]; + _upButton.anchorPoint = CGPointMake(0.5, 1 + newDPadDistFactor); + _upButton.bounds = CGRectMake(0, 0, newShortSideLength, newLongSideLength); + _upButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); + + // down button + // UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"]; + UIImage* downButtonHdImage = [UIImage imageNamed:@"DownButtonHD"]; + _downButton.contents = (id) downButtonHdImage.CGImage; + [_view.layer addSublayer:_downButton]; + // Resize and reposition the button + _downButton.anchorPoint = CGPointMake(0.5, 0 - newDPadDistFactor); + _downButton.bounds = CGRectMake(0, 0, newShortSideLength, newLongSideLength); + _downButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); + + // left button + // UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"]; + UIImage* leftButtonHdImage = [UIImage imageNamed:@"LeftButtonHD"]; + _leftButton.contents = (id) leftButtonHdImage.CGImage; + [_view.layer addSublayer:_leftButton]; + _leftButton.anchorPoint = CGPointMake(1 + newDPadDistFactor, 0.5); + _leftButton.bounds = CGRectMake(0, 0, newLongSideLength, newShortSideLength); + _leftButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); + + // right button + // UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"]; + UIImage* rightButtonHdImage = [UIImage imageNamed:@"RightButtonHD"]; + _rightButton.contents = (id) rightButtonHdImage.CGImage; + [_view.layer addSublayer:_rightButton]; + _rightButton.anchorPoint = CGPointMake(0 - newDPadDistFactor, 0.5); + _rightButton.bounds = CGRectMake(0, 0, newLongSideLength, newShortSideLength); + _rightButton.position = CGPointMake(sharedCenter.x, sharedCenter.y); + } } /** * Sets D-Pad position for class const var */ -- (void) setDPadCenter { - OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile - - for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { +- (void) setDPadCenter:(OSCProfile *)profile { + for (NSData *buttonStateEncoded in profile.buttonStatesEncoded) { OnScreenButtonState* buttonState = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; if ([buttonState.name isEqualToString:@"dPad"]) { buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; @@ -752,10 +812,8 @@ static float L3_Y; /** * Sets analog stick positions for class const var */ -- (void) setAnalogStickPositions { - OSCProfile *oscProfile = [profilesManager getSelectedProfile]; // returns the currently selected OSCProfile - - for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { +- (void) setAnalogStickPositions:(OSCProfile *)profile { + for (NSData *buttonStateEncoded in profile.buttonStatesEncoded) { OnScreenButtonState* buttonState = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; if ([buttonState.name isEqualToString:@"leftStickBackground"]) { @@ -775,16 +833,10 @@ static float L3_Y; # define LR2_Y_UP_OFFSET 8 # define LR1_Y_DOWN_OFFSET 3.5 -- (void) positionAndResizeSingleControllerLayers { - OSCProfile *oscProfile = [profilesManager getSelectedProfile]; - bool defaultProfileSelected = [profilesManager getIndexOfSelectedProfile] == 0; - - for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { - +- (void) positionAndResizeSingleControllerLayers:(OSCProfile *)profile { + for (NSData *buttonStateEncoded in profile.buttonStatesEncoded) { OnScreenButtonState *buttonStateDecoded = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; - - - for (CALayer *buttonLayer in self.OSCButtonLayers) { // iterate through each button layer on screen and position and hide/unhide each according to the instructions of its associated 'buttonState' + for (CALayer *buttonLayer in self.OSCButtonLayerPool) { // iterate through each button layer on screen and position and hide/unhide each according to the instructions of its associated 'buttonState' if ([buttonLayer.name isEqualToString:buttonStateDecoded.name]) { if ([buttonLayer.name isEqualToString:@"upButton"] == NO && [buttonLayer.name isEqualToString:@"rightButton"] == NO && @@ -795,10 +847,15 @@ static float L3_Y; buttonStateDecoded.position = [self denormalizeWidgetPosition:buttonStateDecoded.position]; buttonLayer.position = buttonStateDecoded.position; } - buttonLayer.hidden = buttonStateDecoded.isHidden; + buttonLayer.hidden = buttonStateDecoded.isHidden; + if(buttonLayer.hidden){ + [buttonLayer removeFromSuperlayer]; + continue; + } + // adjust default layout for largerStickLR1 - if(defaultProfileSelected && _largerStickLR1){ + if(_largerStickLR1){ if([buttonLayer.name isEqualToString:@"l2Button"] || [buttonLayer.name isEqualToString:@"r2Button"]) buttonLayer.position = CGPointMake(buttonLayer.position.x, buttonLayer.position.y - LR2_Y_UP_OFFSET); if([buttonLayer.name isEqualToString:@"l1Button"] || [buttonLayer.name isEqualToString:@"r1Button"]) buttonLayer.position = CGPointMake(buttonLayer.position.x, buttonLayer.position.y + LR1_Y_DOWN_OFFSET); } @@ -823,7 +880,7 @@ static float L3_Y; } - (void) setOpacityForStandardControllerLayers { - for(CALayer* oscLayer in self.OSCButtonLayers){ + for(CALayer* oscLayer in self.OSCButtonLayerPool){ if(oscLayer == self._leftStick || oscLayer == self._rightStick){ oscLayer.opacity = DEFAULT_STICK_OPACITY; @@ -836,15 +893,10 @@ static float L3_Y; } } - -- (void) setOpacityForCutsomControllerLayers { - OSCProfile *oscProfile = [profilesManager getSelectedProfile]; - // bool defaultProfileSelected = [profilesManager getIndexOfSelectedProfile] == 0; - - for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { +- (void) setOpacityForCutsomControllerLayers:(OSCProfile *)profile { + for (NSData *buttonStateEncoded in profile.buttonStatesEncoded) { OnScreenButtonState* buttonStateDecoded = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; - - for (CALayer *buttonLayer in self.OSCButtonLayers) { // iterate through each button layer on screen + for (CALayer *buttonLayer in self.OSCButtonLayerPool) { // iterate through each button layer on screen // Here we deal with resizing single layer controllers only if ([buttonLayer.name isEqualToString:buttonStateDecoded.name]) { if ([buttonLayer.name isEqualToString:@"l1Button"] || @@ -875,22 +927,30 @@ static float L3_Y; } } - +- (void)removeHiddenlayers{ + for (CALayer *buttonLayer in self.OSCButtonLayerPool) { // iterate through each button layer on screen + if(buttonLayer.isHidden || buttonLayer.opacity == 0) [buttonLayer removeFromSuperlayer]; + } +} - (void) drawStartSelect { // create Start button - UIImage* startButtonImage = [UIImage imageNamed:@"StartButton"]; - UIImage* startButtonHdImage = [UIImage imageNamed:@"StartButtonHD"]; - _startButton.frame = CGRectMake(START_X - startButtonImage.size.width / 2, START_Y - startButtonImage.size.height / 2, startButtonImage.size.width, startButtonImage.size.height); - _startButton.contents = (id) startButtonHdImage.CGImage; - [_view.layer addSublayer:_startButton]; + if([OSCButtonLayerPool containsObject:_startButton]){ + UIImage* startButtonImage = [UIImage imageNamed:@"StartButton"]; + UIImage* startButtonHdImage = [UIImage imageNamed:@"StartButtonHD"]; + _startButton.frame = CGRectMake(START_X - startButtonImage.size.width / 2, START_Y - startButtonImage.size.height / 2, startButtonImage.size.width, startButtonImage.size.height); + _startButton.contents = (id) startButtonHdImage.CGImage; + [_view.layer addSublayer:_startButton]; + } // create Select button - UIImage* selectButtonImage = [UIImage imageNamed:@"SelectButton"]; - UIImage* selectHdButtonImage = [UIImage imageNamed:@"SelectButtonHD"]; - _selectButton.frame = CGRectMake(SELECT_X - selectButtonImage.size.width / 2, SELECT_Y - selectButtonImage.size.height / 2, selectButtonImage.size.width, selectButtonImage.size.height); - _selectButton.contents = (id) selectHdButtonImage.CGImage; - [_view.layer addSublayer:_selectButton]; + if([OSCButtonLayerPool containsObject:_selectButton]){ + UIImage* selectButtonImage = [UIImage imageNamed:@"SelectButton"]; + UIImage* selectHdButtonImage = [UIImage imageNamed:@"SelectButtonHD"]; + _selectButton.frame = CGRectMake(SELECT_X - selectButtonImage.size.width / 2, SELECT_Y - selectButtonImage.size.height / 2, selectButtonImage.size.width, selectButtonImage.size.height); + _selectButton.contents = (id) selectHdButtonImage.CGImage; + [_view.layer addSublayer:_selectButton]; + } } - (void) drawBumpers { @@ -898,26 +958,31 @@ static float L3_Y; if(_largerStickLR1) L1_Y = R1_Y = L1_Y + LR1_Y_DOWN_OFFSET; // create L1 button - UIImage* l1ButtonImage = [UIImage imageNamed:@"L1"]; - UIImage* l1ButtonHdImage = [UIImage imageNamed:@"L1HD"]; - _l1Button.frame = CGRectMake(L1_X - l1ButtonImage.size.width / 2, L1_Y - l1ButtonImage.size.height / 2, l1ButtonImage.size.width, l1ButtonImage.size.height); - _l1Button.contents = (id) l1ButtonHdImage.CGImage; - [_view.layer addSublayer:_l1Button]; - + if([OSCButtonLayerPool containsObject:_l1Button]){ + UIImage* l1ButtonImage = [UIImage imageNamed:@"L1"]; + UIImage* l1ButtonHdImage = [UIImage imageNamed:@"L1HD"]; + _l1Button.frame = CGRectMake(L1_X - l1ButtonImage.size.width / 2, L1_Y - l1ButtonImage.size.height / 2, l1ButtonImage.size.width, l1ButtonImage.size.height); + _l1Button.contents = (id) l1ButtonHdImage.CGImage; + if(_largerStickLR1){ + UIImage* l2ButtonImage = [UIImage imageNamed:@"L2"]; + _l1Button.bounds = CGRectMake(0, 0, l2ButtonImage.size.width, l2ButtonImage.size.height); + } + [_view.layer addSublayer:_l1Button]; + } + // create R1 button - UIImage* r1ButtonImage = [UIImage imageNamed:@"R1"]; - UIImage* r1ButtonHdImage = [UIImage imageNamed:@"R1HD"]; - _r1Button.frame = CGRectMake(R1_X - r1ButtonImage.size.width / 2, R1_Y - r1ButtonImage.size.height / 2, r1ButtonImage.size.width, r1ButtonImage.size.height); - _r1Button.contents = (id) r1ButtonHdImage.CGImage; - - // make l1 r1 the same size as l2 r2 - if(_largerStickLR1){ - UIImage* l2ButtonImage = [UIImage imageNamed:@"L2"]; - UIImage* l2ButtonHdImage = [UIImage imageNamed:@"L2HD"]; - _l1Button.bounds = _r1Button.bounds = CGRectMake(0, 0, l2ButtonImage.size.width, l2ButtonImage.size.height); + if([OSCButtonLayerPool containsObject:_r1Button]){ + UIImage* r1ButtonImage = [UIImage imageNamed:@"R1"]; + UIImage* r1ButtonHdImage = [UIImage imageNamed:@"R1HD"]; + _r1Button.frame = CGRectMake(R1_X - r1ButtonImage.size.width / 2, R1_Y - r1ButtonImage.size.height / 2, r1ButtonImage.size.width, r1ButtonImage.size.height); + _r1Button.contents = (id) r1ButtonHdImage.CGImage; + // make l1 r1 the same size as l2 r2 + if(_largerStickLR1){ + UIImage* l2ButtonImage = [UIImage imageNamed:@"L2"]; + _r1Button.bounds = CGRectMake(0, 0, l2ButtonImage.size.width, l2ButtonImage.size.height); + } + [_view.layer addSublayer:_r1Button]; } - - [_view.layer addSublayer:_r1Button]; } - (void) drawTriggers { @@ -925,73 +990,84 @@ static float L3_Y; if(_largerStickLR1) L2_Y = R2_Y = L2_Y - LR2_Y_UP_OFFSET; // create L2 button - UIImage* l2ButtonImage = [UIImage imageNamed:@"L2"]; - UIImage* l2ButtonHdImage = [UIImage imageNamed:@"L2HD"]; - _l2Button.frame = CGRectMake(L2_X - l2ButtonImage.size.width / 2, L2_Y - l2ButtonImage.size.height / 2, l2ButtonImage.size.width, l2ButtonImage.size.height); - _l2Button.contents = (id) l2ButtonHdImage.CGImage; - [_view.layer addSublayer:_l2Button]; + if([OSCButtonLayerPool containsObject:_l2Button]){ + UIImage* l2ButtonImage = [UIImage imageNamed:@"L2"]; + UIImage* l2ButtonHdImage = [UIImage imageNamed:@"L2HD"]; + _l2Button.frame = CGRectMake(L2_X - l2ButtonImage.size.width / 2, L2_Y - l2ButtonImage.size.height / 2, l2ButtonImage.size.width, l2ButtonImage.size.height); + _l2Button.contents = (id) l2ButtonHdImage.CGImage; + [_view.layer addSublayer:_l2Button]; + } // create R2 button - UIImage* r2ButtonImage = [UIImage imageNamed:@"R2"]; - UIImage* r2ButtonHdImage = [UIImage imageNamed:@"R2HD"]; - _r2Button.frame = CGRectMake(R2_X - r2ButtonImage.size.width / 2, R2_Y - r2ButtonImage.size.height / 2, r2ButtonImage.size.width, r2ButtonImage.size.height); - _r2Button.contents = (id) r2ButtonHdImage.CGImage; - [_view.layer addSublayer:_r2Button]; + if([OSCButtonLayerPool containsObject:_r2Button]){ + UIImage* r2ButtonImage = [UIImage imageNamed:@"R2"]; + UIImage* r2ButtonHdImage = [UIImage imageNamed:@"R2HD"]; + _r2Button.frame = CGRectMake(R2_X - r2ButtonImage.size.width / 2, R2_Y - r2ButtonImage.size.height / 2, r2ButtonImage.size.width, r2ButtonImage.size.height); + _r2Button.contents = (id) r2ButtonHdImage.CGImage; + [_view.layer addSublayer:_r2Button]; + } } - (void) drawSticks { // create left analog stick - UIImage* leftStickBgImage = [UIImage imageNamed:@"StickOuter"]; - UIImage* stickBgHdImage = [UIImage imageNamed:@"StickOuterHD"]; + bool hasLeftStick = [_activeCustomOscButtonPositionDict.allKeys containsObject:@"leftStickBackground"]; + bool hasRightStick = [_activeCustomOscButtonPositionDict.allKeys containsObject:@"rightStickBackground"]; - _leftStickBackground.frame = CGRectMake(LS_CENTER_X - leftStickBgImage.size.width / 2, LS_CENTER_Y - leftStickBgImage.size.height / 2, leftStickBgImage.size.width, leftStickBgImage.size.height); - _leftStickBackground.contents = (id) stickBgHdImage.CGImage; - [_view.layer addSublayer:_leftStickBackground]; + if(!hasLeftStick && !hasRightStick) return; - UIImage* leftStickImage = [UIImage imageNamed:@"StickInner"]; + UIImage* stickBgImage = [UIImage imageNamed:@"StickOuter"]; + UIImage* stickBgHdImage = [UIImage imageNamed:@"StickOuterHD"]; + UIImage* stickImage = [UIImage imageNamed:@"StickInner"]; UIImage* stickHdImage = [UIImage imageNamed:@"StickInnerHD"]; - _leftStick.frame = CGRectMake(LS_CENTER_X - leftStickImage.size.width / 2, LS_CENTER_Y - leftStickImage.size.height / 2, leftStickImage.size.width, leftStickImage.size.height); - _leftStick.contents = (id) stickHdImage.CGImage; - _leftStick.opacity = _leftStickOpacity; // make stick half transparent when it's idle - [_view.layer addSublayer:_leftStick]; + + if(hasLeftStick){ + _leftStickBackground.frame = CGRectMake(LS_CENTER_X - stickBgImage.size.width / 2, LS_CENTER_Y - stickBgImage.size.height / 2, stickBgImage.size.width, stickBgImage.size.height); + _leftStickBackground.contents = (id) stickBgHdImage.CGImage; + [_view.layer addSublayer:_leftStickBackground]; + + _leftStick.frame = CGRectMake(LS_CENTER_X - stickImage.size.width / 2, LS_CENTER_Y - stickImage.size.height / 2, stickImage.size.width, stickImage.size.height); + _leftStick.contents = (id) stickHdImage.CGImage; + _leftStick.opacity = _leftStickOpacity; // make stick half transparent when it's idle + [_view.layer addSublayer:_leftStick]; + } // create right analog stick - UIImage* rightStickBgImage = [UIImage imageNamed:@"StickOuter"]; - _rightStickBackground.frame = CGRectMake(RS_CENTER_X - rightStickBgImage.size.width / 2, RS_CENTER_Y - rightStickBgImage.size.height / 2, rightStickBgImage.size.width, rightStickBgImage.size.height); - _rightStickBackground.contents = (id) stickBgHdImage.CGImage; - [_view.layer addSublayer:_rightStickBackground]; - - - - UIImage* rightStickImage = [UIImage imageNamed:@"StickInner"]; - _rightStick.frame = CGRectMake(RS_CENTER_X - rightStickImage.size.width / 2, RS_CENTER_Y - rightStickImage.size.height / 2, rightStickImage.size.width, rightStickImage.size.height); - - _rightStick.contents = (id) stickHdImage.CGImage; - _rightStick.opacity = _rightStickOpacity; // make stick half transparent when it's idle - [_view.layer addSublayer:_rightStick]; + if(hasRightStick){ + _rightStickBackground.frame = CGRectMake(RS_CENTER_X - stickBgImage.size.width / 2, RS_CENTER_Y - stickBgImage.size.height / 2, stickBgImage.size.width, stickBgImage.size.height); + _rightStickBackground.contents = (id) stickBgHdImage.CGImage; + [_view.layer addSublayer:_rightStickBackground]; + + + + _rightStick.frame = CGRectMake(RS_CENTER_X - stickImage.size.width / 2, RS_CENTER_Y - stickImage.size.height / 2, stickImage.size.width, stickImage.size.height); + + _rightStick.contents = (id) stickHdImage.CGImage; + _rightStick.opacity = _rightStickOpacity; // make stick half transparent when it's idle + [_view.layer addSublayer:_rightStick]; + } // make stick larger if(_largerStickLR1){ // sticks are resized here by the sizefactor persisted in the osc profile. - RIGHT_STICK_INNER_SIZE = rightStickImage.size.width *1.33 * _rightStickSizeFactor; - RIGHT_STICK_OUTER_SIZE = rightStickBgImage.size.width *1.10 * _rightStickSizeFactor; + RIGHT_STICK_INNER_SIZE = stickImage.size.width *1.33 * _rightStickSizeFactor; + RIGHT_STICK_OUTER_SIZE = stickBgImage.size.width *1.10 * _rightStickSizeFactor; _rightStick.bounds = CGRectMake(0, 0, RIGHT_STICK_INNER_SIZE, RIGHT_STICK_INNER_SIZE); _rightStickBackground.bounds = CGRectMake(0, 0, RIGHT_STICK_OUTER_SIZE, RIGHT_STICK_OUTER_SIZE); - LEFT_STICK_INNER_SIZE = rightStickImage.size.width *1.33 * _leftStickSizeFactor; - LEFT_STICK_OUTER_SIZE = rightStickBgImage.size.width *1.10 * _leftStickSizeFactor; + LEFT_STICK_INNER_SIZE = stickImage.size.width *1.33 * _leftStickSizeFactor; + LEFT_STICK_OUTER_SIZE = stickBgImage.size.width *1.10 * _leftStickSizeFactor; _leftStick.bounds = CGRectMake(0, 0, LEFT_STICK_INNER_SIZE, LEFT_STICK_INNER_SIZE); _leftStickBackground.bounds = CGRectMake(0, 0, LEFT_STICK_OUTER_SIZE, LEFT_STICK_OUTER_SIZE); } else{ // sticks are resized here by the sizefactor persisted in the osc profile. - RIGHT_STICK_INNER_SIZE = rightStickImage.size.width * _rightStickSizeFactor; - RIGHT_STICK_OUTER_SIZE = rightStickBgImage.size.width * _rightStickSizeFactor; + RIGHT_STICK_INNER_SIZE = stickImage.size.width * _rightStickSizeFactor; + RIGHT_STICK_OUTER_SIZE = stickBgImage.size.width * _rightStickSizeFactor; _rightStick.bounds = CGRectMake(0, 0, RIGHT_STICK_INNER_SIZE, RIGHT_STICK_INNER_SIZE); _rightStickBackground.bounds = CGRectMake(0, 0, RIGHT_STICK_OUTER_SIZE, RIGHT_STICK_OUTER_SIZE); - LEFT_STICK_INNER_SIZE = rightStickImage.size.width * _leftStickSizeFactor; - LEFT_STICK_OUTER_SIZE = rightStickBgImage.size.width * _leftStickSizeFactor; + LEFT_STICK_INNER_SIZE = stickImage.size.width * _leftStickSizeFactor; + LEFT_STICK_OUTER_SIZE = stickBgImage.size.width * _leftStickSizeFactor; _leftStick.bounds = CGRectMake(0, 0, LEFT_STICK_INNER_SIZE, LEFT_STICK_INNER_SIZE); _leftStickBackground.bounds = CGRectMake(0, 0, LEFT_STICK_OUTER_SIZE, LEFT_STICK_OUTER_SIZE); } @@ -1992,5 +2068,12 @@ static float L3_Y; return dict; } +- (void)dealloc{ + for(CALayer* layer in self.OSCButtonLayerPool){ + [layer removeFromSuperlayer]; + } + [self.OSCButtonLayerPool removeAllObjects]; +} + @end diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index e2db3ae1..08f43606 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -9,35 +9,30 @@ import UIKit import SVGKit -@objc class OnScreenWidgetView: UIView, OscInstanceReceiverDelegate { - // receiving the OnScreenControls instance from delegate - @objc func getOnScreenControlsInstance(_ sender: Any) { - if let controls = sender as? OnScreenControls { - self.onScreenControls = controls - print("ClassA received OnScreenControls instance: \(controls)") - } else { - print("ClassA received an unknown sender") - } - } - +@objc class OnScreenWidgetView: UIView { + @objc public weak var guidelineDelegate: OnScreenWidgetGuidelineUpdateDelegate? @objc protocol OnScreenWidgetGuidelineUpdateDelegate: AnyObject { func updateGuidelinesForOnScreenWidget(_ sender: Any) } - @objc public var motionHandler: MotionHandler = MotionHandler.shared + @objc public var motionHandler: MotionHandler? @objc public weak var functionalButtonDelegate: OnScreenFunctionalButtonDelegate? @objc protocol OnScreenFunctionalButtonDelegate: AnyObject { func expandSettingsView() func bringUpToolboxMenu() func openWidgetLayoutTool() - func switchWidgetProfile() + func openWidgetProfileTable(pickProfile: Bool) func bringUpSoftKeyboard() func alterAbsTouchDragWith(mouseButton:Int32) func switchPencilHover() func enablePencilHover() func disablePencilHover() + func setAllowSingleTouchEnabled(_ enabled:Bool) + func replaceBrush(shortcut:String) + func replaceEraser(shortcut:String) + func presentPressureCurveVC() } @objc enum WidgetTypeEnum: UInt8 { @@ -48,11 +43,8 @@ import SVGKit private static let MinAutotapInterval:Int = 50 - private let oscProfileMan: OSCProfilesManager = OSCProfilesManager.sharedManager(CGRectZero) - private var oscProfile: OSCProfile - - private let dataMan: DataManager = DataManager() - private let tempSettings: TemporarySettings + // private let oscProfileMan: OSCProfilesManager = OSCProfilesManager.sharedManager(CGRectZero) + @objc public var oscProfile: OSCProfile @objc public var widgetType: WidgetTypeEnum = WidgetTypeEnum.uninitialized @@ -60,9 +52,9 @@ import SVGKit @objc static public var buttonVisualFeedbackEnabled: Bool = true @objc public var widgetLabel: String @objc public var cmdString: String - @objc public var identifier: String = "" + @objc public var sequence: Int16 = -1 private var buttonString: String = "" - private var functionalButtonString: String = "" + @objc private(set) var functionalButtonString: String = "" public var motionControlButtonString: String = "" @objc public var touchPadString: String = "" // super combo key string set @@ -74,7 +66,7 @@ import SVGKit @objc public var heightFactor: CGFloat = 1.0 @objc public var componentSizeFactor: CGFloat = 2.88 - @objc public var buttonMode: Int = 0 + @objc public var buttonMode: ButtonMode = .slideToToggle @objc private var tapToToggleFlag: Bool = true @objc public var sizeReference: Int = WidgetSizeReference.longSide.rawValue @@ -150,6 +142,7 @@ import SVGKit @objc public var hasL3R3Indicator: Bool = false @objc public var isStickWheel: Bool = false + @objc public var isFolder: Bool = false // for all stick pads @objc public var minStickOffset: CGFloat = 0 @@ -220,7 +213,7 @@ import SVGKit private var previousButtonMask = Direction.initialStatus.rawValue // OnScreenControls instance - @objc public var onScreenControls: OnScreenControls + @objc public var onScreenControls: OnScreenControls? // key / button label private let label: UILabel @@ -229,7 +222,6 @@ import SVGKit @objc public var touchBeganLocation: CGPoint = .zero // for mousePad - private var touchLockedForMoveEvent: UITouch private var touchBegan: Bool = false private var directionPadTouchBegan: Bool = false private var firstTouchMoved: Bool = false @@ -253,7 +245,6 @@ import SVGKit //slide buttons private var capturedTouches: NSMutableSet - private let noTouch: UITouch = UITouch() let setLock = NSLock() //controller touch pad @@ -271,15 +262,19 @@ import SVGKit private var tickCycle: UInt8 = UIScreen.main.maximumFramesPerSecond > 110 ? 20 : 10 private var tickFlag: UInt8 = 0 + @objc public var folded: Bool = false + @objc public var revealMode: RevealMode = .exclusive + @objc public var sequenceSet: Set = Set() + private weak var capturer: OnScreenWidgetView? - @objc init(cmdString: String, buttonLabel: String, shape:String) { + @objc init(cmdString: String, buttonLabel: String, shape:String, profile:OSCProfile) { self.cmdString = cmdString self.touchPadString = "" if !self.cmdString.contains("+"){ // 安全解包并处理 `comboKeyStrings` - if var comboStrings = CommandManager.shared.extractSinglCmdStringsFromComboKeys(from: self.cmdString) { + if var comboStrings = CommandManager.shared.extractCmdStrings(from: self.cmdString) { // extract timeInterval if let lastString = comboStrings.last, lastString.contains("MS") { @@ -346,7 +341,6 @@ import SVGKit self.touchTapTimeStamp = 100 self.buttonDownVisualEffectStandardWidth = 0 self.mousePointerMoved = false - self.touchLockedForMoveEvent = UITouch() self.twoTouchesDetected = false self.stickIndicatorOffset = 95 self.sensitivityFactorX = 1.0 @@ -358,10 +352,10 @@ import SVGKit self.pointerIdPool.insert(UInt32(i)) } self.activePointerIds = [] - self.oscProfile = oscProfileMan.getSelectedProfile() - self.tempSettings = dataMan.getSettings() + self.oscProfile = profile self.inertialScroller = InertialScroller() dWheelWalkModeThreshold = stickMaxOffset*0.5 + // self.motionHandler = MotionHandler.shared(profile: profile) super.init(frame: .zero) // helps widget panel to hide/show stacks @@ -370,14 +364,20 @@ import SVGKit if self.widgetType == WidgetTypeEnum.button { if !self.touchPadString.isEmpty { self.mouseButtonAction = MouseButtonAction.noClick - self.buttonMode = ButtonMode.regular.rawValue + self.buttonMode = .regular } if !self.motionControlButtonString.isEmpty { - self.buttonMode = ButtonMode.tapToToggle.rawValue + self.buttonMode = .tapToToggle } if !self.functionalButtonString.isEmpty { - self.buttonMode = ButtonMode.movable.rawValue - if self.functionalButtonString == "PENCILHOVER" {self.buttonMode = ButtonMode.regular.rawValue} + self.buttonMode = .movable + if self.functionalButtonString == "PENCILHOVER" {self.buttonMode = .regular} + } + if self.cmdString.contains("BRUSH"){ + self.functionalButtonString = "BRUSH" + } + if self.cmdString.contains("ERASER"){ + self.functionalButtonString = "ERASER" } } @@ -401,6 +401,8 @@ import SVGKit } self.tweakBorderAlpha(alpha: self.borderAlpha) // fix default borderAlpha offset + + self.onScreenControls = OnScreenControls.shared() setupView() @@ -439,6 +441,7 @@ import SVGKit self.hasHapticFeedback = !self.comboButtonStrings.isEmpty || CommandManager.directionPads.contains(self.touchPadString) self.isDirectionPad = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.directionPads.contains(self.touchPadString) self.isStickWheel = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.stickWheels.contains(self.touchPadString) + self.isFolder = self.cmdString == "FOLDER" self.hasComponent = self.isStickWheel self.hasL3R3Indicator = !self.isStickWheel && !self.isDirectionPad && self.widgetType == WidgetTypeEnum.touchPad @@ -457,9 +460,9 @@ import SVGKit } } - @objc public func setupInertialScroller() { + @objc public func setupInertialScroller(fps: Int) { if self.hasInertia { - self.inertialScroller = InertialScroller(decelerationRate: self.decelerationRateX, displayLinkRate: CGFloat(self.tempSettings.framerate.intValue)) + self.inertialScroller = InertialScroller(decelerationRate: self.decelerationRateX, displayLinkRate: CGFloat(fps)) self.inertialScroller.decelerationRateY = self.decelerationRateY } } @@ -736,11 +739,11 @@ import SVGKit } return false } - + private func setupAtrributedText(){ let text = self.widgetLabel let attr = NSAttributedString( - string: text, + string: self.folded ? "[\(self.widgetLabel)]" : self.widgetLabel, attributes: [ .foregroundColor: UIColor(white:labelAlpha>0 ? 1.0 : 0, alpha: abs(labelAlpha)), // 填充色 .strokeColor: (labelAlpha>0 ? UIColor.black : UIColor.white).withAlphaComponent(abs(labelAlpha)*0.43), // 描边色 @@ -1081,15 +1084,14 @@ import SVGKit let axisdiameter = self.getDiameter(lengthFactor: UIDevice.current.userInterfaceIdiom == .phone ? 0.35 : 0.5) let axisSize = CGSize(width: axisdiameter, height: axisdiameter) - self.stickWheelAxis = GraphicUtils.makeCenteredSVGLayer(from: "StickWheelAxis.svg", in: self.layer, targetSize: axisSize) self.stickWheelAxis.removeFromSuperlayer() + self.stickWheelAxis = GraphicUtils.makeSVGLayer(from: "StickWheelAxis", in: self.layer, targetSize: axisSize) self.layer.insertSublayer(self.stickWheelAxis, at: 0) // GraphicUtils.changeColor(layer: self.stickWheelAxis, color: .white.withAlphaComponent(1)) self.stickWheelAxis.isHidden = false - - self.stickWheelLayer = GraphicUtils.makeCenteredSVGLayer(from: "StickWheel.svg", in: self.layer, targetSize: CGSize(width: diameter, height: diameter)) self.stickWheelLayer.removeFromSuperlayer() + self.stickWheelLayer = GraphicUtils.makeSVGLayer(from: "StickWheel", in: self.layer, targetSize: CGSize(width: diameter, height: diameter)) self.layer.insertSublayer(self.stickWheelLayer, at: 0) GraphicUtils.changeColor(layer: self.stickWheelLayer, color: tintColor) self.stickWheelLayer.setAffineTransform(.identity) @@ -1097,8 +1099,8 @@ import SVGKit let smallWheelSize = CGSize(width: diameter*0.56766, height: diameter*0.56766) - self.stickWheelLayerSmall = GraphicUtils.makeCenteredSVGLayer(from: "StickWheelSmall.svg", in: self.layer, targetSize: smallWheelSize) self.stickWheelLayerSmall.removeFromSuperlayer() + self.stickWheelLayerSmall = GraphicUtils.makeSVGLayer(from: "StickWheelSmall", in: self.layer, targetSize: smallWheelSize) self.layer.insertSublayer(self.stickWheelLayerSmall, below: stickWheelLayer) GraphicUtils.changeColor(layer: self.stickWheelLayerSmall, color: tintColor) self.stickWheelLayerSmall.setAffineTransform(.identity) @@ -1266,7 +1268,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["W"]!,Int8(KEY_ACTION_DOWN), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["UP_ARROW"]!,Int8(KEY_ACTION_DOWN), 0) - case "DPAD": self.onScreenControls.pressDownControllerButton(UP_FLAG) + case "DPAD": self.onScreenControls?.pressDownControllerButton(UP_FLAG) default: break } } @@ -1275,7 +1277,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["W"]!,Int8(KEY_ACTION_UP), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["UP_ARROW"]!,Int8(KEY_ACTION_UP), 0) - case "DPAD": self.onScreenControls.releaseControllerButton(UP_FLAG) + case "DPAD": self.onScreenControls?.releaseControllerButton(UP_FLAG) default: break } } @@ -1284,7 +1286,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["S"]!,Int8(KEY_ACTION_DOWN), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["DOWN_ARROW"]!,Int8(KEY_ACTION_DOWN), 0) - case "DPAD": self.onScreenControls.pressDownControllerButton(DOWN_FLAG) + case "DPAD": self.onScreenControls?.pressDownControllerButton(DOWN_FLAG) default: break } } @@ -1293,7 +1295,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["S"]!,Int8(KEY_ACTION_UP), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["DOWN_ARROW"]!,Int8(KEY_ACTION_UP), 0) - case "DPAD": self.onScreenControls.releaseControllerButton(DOWN_FLAG) + case "DPAD": self.onScreenControls?.releaseControllerButton(DOWN_FLAG) default: break } } @@ -1302,7 +1304,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["A"]!,Int8(KEY_ACTION_DOWN), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["LEFT_ARROW"]!,Int8(KEY_ACTION_DOWN), 0) - case "DPAD": self.onScreenControls.pressDownControllerButton(LEFT_FLAG) + case "DPAD": self.onScreenControls?.pressDownControllerButton(LEFT_FLAG) default: break } } @@ -1311,7 +1313,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["A"]!,Int8(KEY_ACTION_UP), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["LEFT_ARROW"]!,Int8(KEY_ACTION_UP), 0) - case "DPAD": self.onScreenControls.releaseControllerButton(LEFT_FLAG) + case "DPAD": self.onScreenControls?.releaseControllerButton(LEFT_FLAG) default: break } } @@ -1320,7 +1322,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["D"]!,Int8(KEY_ACTION_DOWN), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["RIGHT_ARROW"]!,Int8(KEY_ACTION_DOWN), 0) - case "DPAD": self.onScreenControls.pressDownControllerButton(RIGHT_FLAG) + case "DPAD": self.onScreenControls?.pressDownControllerButton(RIGHT_FLAG) default: break } } @@ -1329,7 +1331,7 @@ import SVGKit switch touchPadString { case "WASDPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["D"]!,Int8(KEY_ACTION_UP), 0) case "ARROWPAD": LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["RIGHT_ARROW"]!,Int8(KEY_ACTION_UP), 0) - case "DPAD": self.onScreenControls.releaseControllerButton(RIGHT_FLAG) + case "DPAD": self.onScreenControls?.releaseControllerButton(RIGHT_FLAG) default: break } } @@ -1424,7 +1426,6 @@ import SVGKit } private func handleButtonDown() { - if !OnScreenWidgetView.editMode, !self.functionalButtonString.isEmpty {self.handleFunctionalButtonDown()} if !OnScreenWidgetView.editMode {self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} @@ -1471,6 +1472,20 @@ import SVGKit if !OnScreenWidgetView.editMode && !self.functionalButtonString.isEmpty{ self.handleFunctionalButtonUp() } + + // legacy keyboard button combo connected by "+" + if !OnScreenWidgetView.editMode && self.cmdString.contains("+") && !self.cmdString.contains("-"){ + if buttonMode == .movable, moveableButtonLongPressed() {return} + self.buttonDownVisualEffect() + var keyboardCmdStrings = CommandManager.shared.extractKeyStrings(from: self.cmdString)! + keyboardCmdStrings.removeAll{ + Set(CommandManager.pencilProButtonCmds).contains($0) + } + CommandManager.shared.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings) // send multi-key command + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.buttonUpVisualEffect() + } + } } @objc public func setupLrudDirectionIndicatorlayers() { @@ -1499,16 +1514,16 @@ import SVGKit if !touchCenteredOffset { let axisdiameter = self.getDiameter(lengthFactor: UIDevice.current.userInterfaceIdiom == .phone ? 0.27 : 0.35) let axisSize = CGSize(width: axisdiameter, height: axisdiameter) - self.stickWheelAxis = GraphicUtils.makeCenteredSVGLayer(from: "StickWheelAxis-0.75.svg", in: self.layer, targetSize: axisSize) - GraphicUtils.changeColor(layer: self.stickWheelAxis, color: UIColor(white: 1, alpha: 0.5)) self.stickWheelAxis.removeFromSuperlayer() + self.stickWheelAxis = GraphicUtils.makeSVGLayer(from: "StickWheelAxis-0.75", in: self.layer, targetSize: axisSize) + GraphicUtils.changeColor(layer: self.stickWheelAxis, color: UIColor(white: 1, alpha: 0.5)) self.layer.addSublayer(self.stickWheelAxis) self.stickWheelAxis.isHidden = false } CATransaction.commit() } - + @objc public func hideAllHighlightLayersOfAllWidgets(selfIncluded:Bool) { self.forEachWidget(){ widget in if !selfIncluded && widget == self {return} @@ -1534,7 +1549,7 @@ import SVGKit if self.motionControlButtonString != "" {self.buttonDownVisualEffectStandardWidth = 3} // Set the frame to be larger than the view to expand outward - buttonDownVisualEffectLayer.borderWidth = self.buttonDownVisualEffectStandardWidth * self.highlightSizeFactor // set this 0 to hide the visual effect first + buttonDownVisualEffectLayer.borderWidth = CGFloat(Int(self.buttonDownVisualEffectStandardWidth * self.highlightSizeFactor / 2) * 2) // set this 0 to hide the visual effect first buttonDownVisualEffectLayer.borderColor = voidlinkPurple buttonDownVisualEffectLayer.frame = self.bounds.insetBy(dx: -buttonDownVisualEffectLayer.borderWidth, dy: -buttonDownVisualEffectLayer.borderWidth) // Adjust the inset as needed buttonDownVisualEffectLayer.cornerRadius = self.layer.cornerRadius + buttonDownVisualEffectLayer.borderWidth @@ -1584,12 +1599,12 @@ import SVGKit let mixRightStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick && oscProfile.yawPitchToRightStick) - if !mixRightStickInputToGyro || (self.motionHandler.gyroMixInputStarted() != true) { + if !mixRightStickInputToGyro || (self.motionHandler?.gyroMixInputStarted() != true) { stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate) - self.onScreenControls.sendRightStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy) + self.onScreenControls?.sendRightStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy) } - self.motionHandler.mixOnScreenRightStickAndGyroInput(x: targetX, y: targetY) + self.motionHandler?.mixOnScreenRightStickAndGyroInput(x: targetX, y: targetY) } private func sendLeftStickTouchPadEvent(weightedTouchX:CGFloat, weightedTouchY:CGFloat, circulate:Bool=false){ @@ -1598,44 +1613,44 @@ import SVGKit let mixLeftStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick && oscProfile.rollToLeftStick) - if !mixLeftStickInputToGyro || (self.motionHandler.gyroMixInputStarted() != true) { + if !mixLeftStickInputToGyro || (self.motionHandler?.gyroMixInputStarted() != true) { stickOffsetVector = ControllerUtil.compensated(offsetVector: CGVector(dx: targetX, dy: targetY), minOffset: minStickOffset, circulate: circulate) - self.onScreenControls.sendLeftStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy) + self.onScreenControls?.sendLeftStickTouchPadEvent(stickOffsetVector.dx, stickOffsetVector.dy) } - self.motionHandler.mixOnScreenLeftStickAndGyroInput(x: targetX, y: targetY) + self.motionHandler?.mixOnScreenLeftStickAndGyroInput(x: targetX, y: targetY) } private func sendLeftTriggerTouchPadEvent(inputY: CGFloat){ - self.onScreenControls.updateLeftTrigger(UInt8(max(min(inputY,255),0))) + self.onScreenControls?.updateLeftTrigger(UInt8(max(min(inputY,255),0))) } private func sendRightTriggerTouchPadEvent(inputY: CGFloat){ - self.onScreenControls.updateRightTrigger(UInt8(max(min(inputY,255),0))) + self.onScreenControls?.updateRightTrigger(UInt8(max(min(inputY,255),0))) } //========================================================================================================== private func sendOscButtonDownEvent(oscString: String){ let buttonFlag = CommandManager.oscButtonMappings[oscString] - if buttonFlag != 0 {self.onScreenControls.pressDownControllerButton(buttonFlag!)} + if buttonFlag != 0 {self.onScreenControls?.pressDownControllerButton(buttonFlag!)} else {switch oscString { case "OSCL2", "L2", "LT": - self.onScreenControls.updateLeftTrigger(0xFF) + self.onScreenControls?.updateLeftTrigger(0xFF) case "OSCR2", "R2", "RT": - self.onScreenControls.updateRightTrigger(0xFF) + self.onScreenControls?.updateRightTrigger(0xFF) default:break }} } private func sendOscButtonUpEvent(oscString: String){ let buttonFlag = CommandManager.oscButtonMappings[oscString] - if buttonFlag != 0 {self.onScreenControls.releaseControllerButton(buttonFlag!)} + if buttonFlag != 0 {self.onScreenControls?.releaseControllerButton(buttonFlag!)} else {switch oscString { case "OSCL2", "L2", "LT": - self.onScreenControls.updateLeftTrigger(0x00) + self.onScreenControls?.updateLeftTrigger(0x00) case "OSCR2", "R2", "RT": - self.onScreenControls.updateRightTrigger(0x00) + self.onScreenControls?.updateRightTrigger(0x00) default:break }} } @@ -1703,22 +1718,23 @@ import SVGKit stopTrackballMomentum() } + guard let touch = touches.first else {return} + // get touchBeganLocation + if touches.count == 1 { // to make sure touchBegan location captured properly, don't use event.alltouches.count here let currentTime = CACurrentMediaTime() touchTapTimeInterval = currentTime - touchTapTimeStamp touchTapTimeStamp = currentTime quickDoubleTapDetected = touchTapTimeInterval < QUICK_TAP_TIME_INTERVAL - let touch = touches.first - // get touchBeganLocation if OnScreenWidgetView.editMode { - self.touchBeganLocation = touch!.location(in: superview) + self.touchBeganLocation = touch.location(in: superview) self.highlightBorder(highlighted: true) } else { - if widgetType == WidgetTypeEnum.button, self.buttonMode == ButtonMode.movable.rawValue {self.touchBeganLocation = touch!.location(in: superview)} - else {self.touchBeganLocation = touch!.location(in: self)} + if widgetType == WidgetTypeEnum.button, self.buttonMode == .movable {self.touchBeganLocation = touch.location(in: superview)} + else {self.touchBeganLocation = touch.location(in: self)} } self.latestTouchLocation = touchBeganLocation } @@ -1732,8 +1748,8 @@ import SVGKit if self.widgetType == WidgetTypeEnum.touchPad && touches.count == 1{ // don't use event?.allTouches?.count here, it will counts all touches including the ones captured by other UIViews switch self.touchPadString { case "LSWHEEL","RSWHEEL": - self.getVector(touch: touches.first!) - self.handleStickWheelMove(touch: touches.first!) + self.getVector(touch: touch) + self.handleStickWheelMove(touch: touch) if quickDoubleTapDetected { self.showl3r3Indicator() self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} @@ -1766,7 +1782,7 @@ import SVGKit self.stickWheelAxis.position = touchCenteredOffset ? touchBeganLocation : CGPoint(x: self.bounds.midX, y: self.bounds.midY) self.stickWheelAxis.isHidden = false CATransaction.commit() - self.getVector(touch: touches.first!) + self.getVector(touch: touch) self.handleLrudTouchMove() } if quickDoubleTapDetected { @@ -1796,26 +1812,21 @@ import SVGKit // this will also deal with button events if self.widgetType == WidgetTypeEnum.button && !self.comboButtonStrings.isEmpty { - if self.buttonMode != ButtonMode.tapToToggle.rawValue { + switch self.buttonMode { + case .tapToToggle: + if(self.tapToToggleFlag) {self.handleTapDownOrSlidein()} + else {self.handleFingerUpOrSlideout()} + self.tapToToggleFlag = !self.tapToToggleFlag + case .slideToToggle: + self.handleButtonSliding(touches: touches) + case .slideAndHold: + self.handleButtonSliding(touches: touches) + default: self.handleTapDownOrSlidein() setLock.lock() self.capturedTouches.union(touches) setLock.unlock() } - else{ - if(self.tapToToggleFlag) {self.handleTapDownOrSlidein()} - else {self.handleFingerUpOrSlideout()} - self.tapToToggleFlag = !self.tapToToggleFlag - } - } - - // legacy keyboard button combo connected by "+" - if self.cmdString.contains("+") && !self.cmdString.contains("-"){ - let keyboardCmdStrings = CommandManager.shared.extractKeyStringsFromComboCommand(from: self.cmdString)! - CommandManager.shared.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings) // send multi-key command - DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { // reset shadow color immediately 50ms later - self.handleButtonUp() - } } } // here is in edit mode: @@ -1827,9 +1838,25 @@ import SVGKit private func moveByTouch(touch: UITouch){ let currentLocation: CGPoint - if OnScreenWidgetView.editMode {currentLocation = touch.location(in: superview)} + if OnScreenWidgetView.editMode { + // self.layer.borderColor = voidlinkPurple + capturer = nil + currentLocation = touch.location(in: superview) + forEachWidget { (widget) in + if widget.isFolder { + if self.frame.intersects(widget.frame), !self.sequenceSet.contains(widget.sequence) { + // self.layer.borderColor = UIColor.systemYellow.cgColor + widget.highlightBorder(highlighted: true) + if widget == self {return} + capturer = widget + return + } + else {widget.highlightBorder(highlighted: false)} + } + } + } else { - if self.buttonMode == ButtonMode.movable.rawValue {currentLocation = touch.location(in: superview)} + if self.buttonMode == .movable {currentLocation = touch.location(in: superview)} else {return} } @@ -1898,7 +1925,7 @@ import SVGKit setLock.lock() let captured = widget.capturedTouches.contains(touch) setLock.unlock() - if !captured || widget.buttonMode == ButtonMode.regular.rawValue {return} + if !captured || widget.buttonMode == .regular {return} widget.handleFingerUpOrSlideout() setLock.lock() widget.capturedTouches.remove(touch) @@ -1915,15 +1942,19 @@ import SVGKit } } } + + private func isLocation(_ location:CGPoint, in widget:OnScreenWidgetView) -> Bool{ + let locationInWidget = widget.convert(location, from: self.superview) + return widget.bounds.contains(locationInWidget) + } private func handleButtonSliding(touches: Set) { for touch in touches { let locationInSuperView = touch.location(in: self.superview) self.forEachWidget{ widget in if widget.widgetType != WidgetTypeEnum.button {return} - let isSlidableButton = widget.buttonMode == ButtonMode.slideToToggle.rawValue || widget.buttonMode == ButtonMode.slideAndHold.rawValue - let pointInSubview = widget.convert(locationInSuperView, from: self.superview) - if widget.bounds.contains(pointInSubview){ + let isSlidableButton = widget.buttonMode == .slideToToggle || widget.buttonMode == .slideAndHold + if isLocation(locationInSuperView, in: widget){ setLock.lock() let captured = widget.capturedTouches.contains(touch) setLock.unlock() @@ -1941,13 +1972,13 @@ import SVGKit setLock.unlock() if !captured || !isSlidableButton {return} // print("UIButton: \(widget.buttonLabel) out test, \(widget.touchPadString), \(CACurrentMediaTime())") - if(widget.buttonMode == ButtonMode.slideToToggle.rawValue){ + if(widget.buttonMode == .slideToToggle){ widget.handleFingerUpOrSlideout() setLock.lock() widget.capturedTouches.remove(touch) setLock.unlock() } - if(widget.buttonMode == ButtonMode.slideAndHold.rawValue){ + if(widget.buttonMode == .slideAndHold){ // do nothing here } } @@ -1965,13 +1996,12 @@ import SVGKit } if self.widgetType == WidgetTypeEnum.button { - if self.buttonMode == ButtonMode.slideToToggle.rawValue || self.buttonMode == ButtonMode.slideAndHold.rawValue {self.handleButtonSliding(touches: touches)} + if self.buttonMode == .slideToToggle || self.buttonMode == .slideAndHold {self.handleButtonSliding(touches: touches)} } - if self.buttonMode == ButtonMode.movable.rawValue{ + if self.buttonMode == .movable{ if let touch = touches.first { - NSLog("touchTapTimeStamp %f", self.touchTapTimeStamp) - if CACurrentMediaTime() - self.touchTapTimeStamp > 0.3 { // temporarily relocate special buttons + if self.moveableButtonLongPressed() { // temporarily relocate special buttons self.moveByTouch(touch: touch) } } @@ -2028,15 +2058,16 @@ import SVGKit } private func handleTouchPadMoveEvent (_ touches: Set, with event: UIEvent?){ + guard let touch = touches.first else { return } if touches.count == 1{ // don't use event.alltouches.count here, it will counts all touches - self.getVector(touch: touches.first!) + self.getVector(touch: touch) switch self.touchPadString{ case "MOUSEPAD": DispatchQueue.global(qos: .userInteractive).async { self.weightedDeltaX = Int(self.deltaX * 1.7 * self.sensitivityFactorX) self.weightedDeltaY = Int(self.deltaY * 1.7 * self.sensitivityFactorY) if self.firstTouchMoved {LiSendMouseMoveEvent(Int16(self.weightedDeltaX), Int16(self.weightedDeltaY))} - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) } break case "TRACKBALL": @@ -2048,7 +2079,7 @@ import SVGKit self.trackballVelocity = CGPoint(x: self.weightedDeltaX, y: self.weightedDeltaY) self.stopTrackballMomentum() } - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) } break case "ABSMOUSE": @@ -2068,24 +2099,24 @@ import SVGKit } } } - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) break case "LSWHEEL", "RSWHEEL": - self.handleStickWheelMove(touch: touches.first!) + self.handleStickWheelMove(touch: touch) case "LSPAD": DispatchQueue.global(qos: .userInteractive).async { self.weightedDeltaX = 1 self.sendLeftStickTouchPadEvent(weightedTouchX: self.offSetX * self.sensitivityFactorX, weightedTouchY: self.offSetY * self.sensitivityFactorY) } if widgetType == WidgetTypeEnum.touchPad {updateStickIndicator()} - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) case "RSPAD": DispatchQueue.global(qos: .userInteractive).async { self.weightedDeltaX = 1 self.sendRightStickTouchPadEvent(weightedTouchX: self.offSetX * self.sensitivityFactorX, weightedTouchY: self.offSetY * self.sensitivityFactorY) } if widgetType == WidgetTypeEnum.touchPad {updateStickIndicator()} - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) case "LSVPAD": DispatchQueue.global(qos: .userInteractive).async { self.weightedDeltaX = Int(self.deltaX*self.VectorStickFactor*self.sensitivityFactorX) @@ -2093,7 +2124,7 @@ import SVGKit if self.firstTouchMoved { self.sendLeftStickTouchPadEvent(weightedTouchX: CGFloat(self.weightedDeltaX), weightedTouchY: CGFloat(self.weightedDeltaY)) } - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) } case "RSVPAD": DispatchQueue.global(qos: .userInteractive).async { @@ -2102,30 +2133,30 @@ import SVGKit if self.firstTouchMoved { self.sendRightStickTouchPadEvent(weightedTouchX: CGFloat(self.weightedDeltaX), weightedTouchY: CGFloat(self.weightedDeltaY)) } - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) } case "LTPAD": DispatchQueue.global(qos: .userInteractive).async { self.weightedDeltaY = 1 self.sendLeftTriggerTouchPadEvent(inputY: -self.offSetY*4.5*self.sensitivityFactorY) - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) } case "RTPAD": DispatchQueue.global(qos: .userInteractive).async { self.weightedDeltaY = 1 self.sendRightTriggerTouchPadEvent(inputY: -self.offSetY*4.5*self.sensitivityFactorY) - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) } case "DPAD", "WASDPAD", "ARROWPAD": self.weightedDeltaX = 1 handleLrudTouchMove() - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) case "MOUSEWHEEL","WHEEL": self.weightedDeltaY = Int(self.deltaY*7.5*self.sensitivityFactorY) if firstTouchMoved {LiSendHighResScrollEvent(Int16(self.weightedDeltaY))} - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) case "DISCRETEWHEEL", "DSWHEEL": - let currentLocation = touches.first!.location(in: self) + let currentLocation = touch.location(in: self) tickFlag = (tickFlag+1)%UInt8(CGFloat(tickCycle)/abs(sensitivityFactorY)) var delta = self.deltaY self.weightedDeltaY = 1 @@ -2134,7 +2165,7 @@ import SVGKit delta = delta * CGFloat(copysign(1.0, sensitivityFactorY)) if tickFlag == 1, delta != 0 {LiSendScrollEvent(delta > 0 ? -3 : 3)} // self.latestTouchLocation = currentLocation - self.updateTouchLocation(touch: touches.first!) + self.updateTouchLocation(touch: touch) default: break } @@ -2191,9 +2222,9 @@ import SVGKit private func handleMotionControlButtonDown(){ switch self.motionControlButtonString { case "GYRO": - self.motionHandler.startGyroByOnScreenButton(self, yawFactor: yawFactor, pitchFactor: pitchFactor, rollFactor: rollFactor) + self.motionHandler?.startGyroByOnScreenButton(self, yawFactor: yawFactor, pitchFactor: pitchFactor, rollFactor: rollFactor) case "GYROPAUSE": - self.motionHandler.stopGyroUpdate(interruptNoneGyroInput:false) + self.motionHandler?.stopGyroUpdate(interruptNoneGyroInput:false) break case "ACCEL": break @@ -2207,27 +2238,27 @@ import SVGKit private func handleMotionControlButtonUp(){ switch self.motionControlButtonString { case "GYRO": - if let gyroStarter = motionHandler.gyroStarter as? OnScreenWidgetView, self === gyroStarter { + if let gyroStarter = motionHandler?.gyroStarter as? OnScreenWidgetView, self === gyroStarter { self.forEachWidget{ widget in if widget.motionControlButtonString != "GYRO" || widget === self {return} - if(widget.buttonMode == ButtonMode.tapToToggle.rawValue && widget.logicallyDown) { + if(widget.buttonMode == .tapToToggle && widget.logicallyDown) { widget.buttonUpVisualEffect() widget.tapToToggleFlag = !widget.tapToToggleFlag } } - self.motionHandler.stopGyroUpdate(interruptNoneGyroInput: false, resetLeftStick: true) - self.motionHandler.gyroStarter = nil + self.motionHandler?.stopGyroUpdate(interruptNoneGyroInput: false, resetLeftStick: true) + self.motionHandler?.gyroStarter = nil } else { - if self.motionHandler.gyroStarter != nil { - if let gyroStarter = motionHandler.gyroStarter as? OnScreenWidgetView { - self.motionHandler.startGyroByOnScreenButton(self, yawFactor: gyroStarter.yawFactor, pitchFactor: gyroStarter.pitchFactor, rollFactor: gyroStarter.rollFactor) + if self.motionHandler?.gyroStarter != nil { + if let gyroStarter = motionHandler?.gyroStarter as? OnScreenWidgetView { + self.motionHandler?.startGyroByOnScreenButton(self, yawFactor: gyroStarter.yawFactor, pitchFactor: gyroStarter.pitchFactor, rollFactor: gyroStarter.rollFactor) } } } case "GYROPAUSE": - if self.motionHandler.gyroStarter != nil { - self.motionHandler.startGyroByOnScreenButton(self, yawFactor: motionHandler.widgetYawFactor, pitchFactor: motionHandler.widgetPitchFactor, rollFactor: motionHandler.widgetRollFactor) + if self.motionHandler?.gyroStarter != nil { + self.motionHandler?.startGyroByOnScreenButton(self, yawFactor: motionHandler?.widgetYawFactor ?? 0, pitchFactor: motionHandler?.widgetPitchFactor ?? 0, rollFactor: motionHandler?.widgetRollFactor ?? 0) } case "ACCEL": break @@ -2245,15 +2276,35 @@ import SVGKit print("mouseButton \(mouseButton)"); self.functionalButtonDelegate?.alterAbsTouchDragWith(mouseButton:mouseButton) case "PENCILHOVER": + if !self.isPencilProEnabled() {break} self.functionalButtonDelegate?.enablePencilHover() + case "NOSINGLETOUCH": + if !self.isPencilProEnabled() {break} + self.functionalButtonDelegate?.setAllowSingleTouchEnabled(false) default: break } } + private func moveableButtonLongPressed() -> Bool{ + return CACurrentMediaTime() - self.touchTapTimeStamp > 0.3 + } + + private var singleTouchEnabled:Bool = true + private func handleFunctionalButtonUp(){ - let longPressed = CACurrentMediaTime() - self.touchTapTimeStamp > 0.3 - if longPressed, buttonMode == ButtonMode.movable.rawValue {return} + if buttonMode == .movable { + if moveableButtonLongPressed() {return} + switch self.functionalButtonString { + case "NOSINGLETOUCH": + if !self.isPencilProEnabled() {break} + singleTouchEnabled = !singleTouchEnabled + self.functionalButtonDelegate?.setAllowSingleTouchEnabled(singleTouchEnabled) + return + default: + break + } + } switch self.functionalButtonString { case "SETTINGS": self.functionalButtonDelegate?.expandSettingsView() @@ -2262,13 +2313,31 @@ import SVGKit case "WIDGETTOOL": self.functionalButtonDelegate?.openWidgetLayoutTool() case "PROFILES","WIDGETPROFILES": - self.functionalButtonDelegate?.switchWidgetProfile() + self.functionalButtonDelegate?.openWidgetProfileTable(pickProfile: false) + case "PICKPROFILE","PICKPRFL": + self.functionalButtonDelegate?.openWidgetProfileTable(pickProfile: true) case "SOFTKEYBOARD": self.functionalButtonDelegate?.bringUpSoftKeyboard() case "ABSTCHDRAG": self.functionalButtonDelegate?.alterAbsTouchDragWith(mouseButton:BUTTON_LEFT) case "PENCILHOVER": + if !self.isPencilProEnabled() {break} self.functionalButtonDelegate?.disablePencilHover() + case "NOSINGLETOUCH": + if !self.isPencilProEnabled() {break} + self.functionalButtonDelegate?.setAllowSingleTouchEnabled(true) + case "BRUSH": + if !self.isPencilProEnabled() {break} + var brushShortcut = self.cmdString.replacingOccurrences(of: "BRUSH+", with: "") + brushShortcut = brushShortcut.replacingOccurrences(of: "BRUSH", with: "") + self.functionalButtonDelegate?.replaceBrush(shortcut: brushShortcut) + case "ERASER": + if !self.isPencilProEnabled() {break} + var eraserShortcut = self.cmdString.replacingOccurrences(of: "ERASER+", with: "") + eraserShortcut = eraserShortcut.replacingOccurrences(of: "ERASER", with: "") + self.functionalButtonDelegate?.replaceEraser(shortcut: eraserShortcut) + case "PRESSURECURVE": + self.functionalButtonDelegate?.presentPressureCurveVC() default: break } @@ -2278,19 +2347,19 @@ import SVGKit private func clearRightStickTouchPadFlag(){ let mixRightStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick && oscProfile.yawPitchToRightStick) - if !mixRightStickInputToGyro || self.motionHandler.gyroMixInputStarted() != true { - self.onScreenControls.clearRightStickTouchPadFlag() + if !mixRightStickInputToGyro || self.motionHandler?.gyroMixInputStarted() != true { + self.onScreenControls?.clearRightStickTouchPadFlag() } - self.motionHandler.mixOnScreenRightStickAndGyroInput(x: 0, y: 0) + self.motionHandler?.mixOnScreenRightStickAndGyroInput(x: 0, y: 0) } private func clearLeftStickTouchPadFlag(){ let mixLeftStickInputToGyro = (oscProfile.mapGyroTo == MapGyroTo.mapGyroToControllerStick && oscProfile.rollToLeftStick) - if !mixLeftStickInputToGyro || self.motionHandler.gyroMixInputStarted() != true { - self.onScreenControls.clearLeftStickTouchPadFlag() + if !mixLeftStickInputToGyro || self.motionHandler?.gyroMixInputStarted() != true { + self.onScreenControls?.clearLeftStickTouchPadFlag() } - self.motionHandler.mixOnScreenLeftStickAndGyroInput(x: 0, y: 0) + self.motionHandler?.mixOnScreenLeftStickAndGyroInput(x: 0, y: 0) } override func touchesCancelled(_ touches: Set, with event: UIEvent?) { @@ -2363,8 +2432,8 @@ import SVGKit self.inertialScroller.handler = { [self] in let weightedScrollerVector = CGVector(dx: self.inertialScroller.vector.dx*self.VectorStickFactor*self.sensitivityFactorX, dy: self.inertialScroller.vector.dy*self.VectorStickFactor*self.sensitivityFactorY) - if self.motionHandler.gyroMixInputStarted() { - let normailizedGyroVector = CGVector(dx: self.stickOffsetToWeightedTouchInput(offset: self.motionHandler.gyroToStickOffset.dx), dy: -self.stickOffsetToWeightedTouchInput(offset: self.motionHandler.gyroToStickOffset.dy)) + if ((self.motionHandler?.gyroMixInputStarted()) == true) { + let normailizedGyroVector = CGVector(dx: self.stickOffsetToWeightedTouchInput(offset: self.motionHandler?.gyroToStickOffset.dx ?? 0), dy: -self.stickOffsetToWeightedTouchInput(offset: self.motionHandler?.gyroToStickOffset.dy ?? 0)) let xConvergent = normailizedGyroVector.dx.sign != weightedScrollerVector.dx.sign && abs(normailizedGyroVector.dx) <= abs(weightedScrollerVector.dx) let yConvergent = normailizedGyroVector.dy.sign != weightedScrollerVector.dy.sign && abs(normailizedGyroVector.dy) <= abs(weightedScrollerVector.dy) @@ -2392,9 +2461,9 @@ import SVGKit } } case "LTPAD": - self.onScreenControls.updateLeftTrigger(0x00) + self.onScreenControls?.updateLeftTrigger(0x00) case "RTPAD": - self.onScreenControls.updateRightTrigger(0x00) + self.onScreenControls?.updateRightTrigger(0x00) case "WASDPAD": self.stickWheelAxis.isHidden = touchCenteredOffset LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["W"]!,Int8(KEY_ACTION_UP), 0) @@ -2409,10 +2478,10 @@ import SVGKit LiSendKeyboardEvent(CommandManager.keyboardButtonMappings["DOWN_ARROW"]!,Int8(KEY_ACTION_UP), 0) case "DPAD": self.stickWheelAxis.isHidden = touchCenteredOffset - self.onScreenControls.releaseControllerButton(LEFT_FLAG) - self.onScreenControls.releaseControllerButton(RIGHT_FLAG) - self.onScreenControls.releaseControllerButton(UP_FLAG) - self.onScreenControls.releaseControllerButton(DOWN_FLAG) + self.onScreenControls?.releaseControllerButton(LEFT_FLAG) + self.onScreenControls?.releaseControllerButton(RIGHT_FLAG) + self.onScreenControls?.releaseControllerButton(UP_FLAG) + self.onScreenControls?.releaseControllerButton(DOWN_FLAG) case "DS4TOUCH": self.handleControllerTouchesUp(touches: touches) default: @@ -2443,7 +2512,7 @@ import SVGKit } if !OnScreenWidgetView.editMode && !self.cmdString.contains("+") && !self.comboButtonStrings.isEmpty { // if the command(keystring contains "+", it's a legacy multi-key command - if self.buttonMode == ButtonMode.slideToToggle.rawValue || self.buttonMode == ButtonMode.slideAndHold.rawValue { + if self.buttonMode == .slideToToggle || self.buttonMode == .slideAndHold { self.handleFingerUpAfterSliding(touches: touches) setLock.lock() self.capturedTouches.minus(touches) @@ -2458,6 +2527,16 @@ import SVGKit if center != layoutChanges.last { layoutChanges.append(center) } + + if capturer != nil { + if firstTouchMoved {undoRelocation()} + if let capturer = capturer { + capturer.highlightBorder(highlighted: false) + guard !self.sequenceSet.contains(capturer.sequence) else { return } + OnScreenWidgetView.put(widget: self, into: capturer) + self.isHidden = capturer.folded + } + } guard let superview = superview else { return } @@ -2491,7 +2570,7 @@ import SVGKit } } - if self.buttonMode != ButtonMode.tapToToggle.rawValue {self.handleFingerUpOrSlideout()} + if self.buttonMode != .tapToToggle {self.handleFingerUpOrSlideout()} } @objc public func setAutoTapIntervalByText(str: String){ @@ -2565,23 +2644,93 @@ import SVGKit } } - private func highlightBorder(highlighted:Bool) { - if highlighted { - self.layer.borderWidth = 3 - self.layer.borderColor = voidlinkPurple + static private func setBorder(hilighted:Bool, in color:CGColor, for widget:OnScreenWidgetView){ + if hilighted { + widget.layer.borderWidth = 3 + widget.layer.borderColor = color } else { - self.layer.borderWidth = self.borderWidth - self.layer.borderColor = self.defaultBorderColor + widget.layer.borderWidth = widget.borderWidth + widget.layer.borderColor = widget.defaultBorderColor } } + private func highlightBorder(highlighted:Bool) { + if self.isFolder { + OnScreenWidgetView.setBorder(hilighted: highlighted, in: UIColor.systemYellow.cgColor, for: self) + self.forEachWidget{ widget in + if self.sequenceSet.contains(widget.sequence) { + OnScreenWidgetView.setBorder(hilighted: highlighted, in: UIColor.systemYellow.cgColor, for: widget) + } + } + return + } + + OnScreenWidgetView.setBorder(hilighted: highlighted, in: voidlinkPurple, for: self) + } + + private func isPencilProEnabled() -> Bool { + if !(PencilHandler.shared?.pencilProEnabled ?? false) { + IAPManager.shared.purchase(AddOnProduct.PencilProPack) + return false + } + return true + } + + @objc func getAvailableSequence() -> Int16 { + var sequence:Int16 = 0 + self.forEachWidget(){ widget in + sequence = max(sequence, widget.sequence) + } + return sequence+1 + } + + @objc static func setCollection(hidden:Bool, for folder:OnScreenWidgetView) { + guard folder.isFolder else {return} + folder.folded = hidden + folder.setupAtrributedText() + if hidden { + folder.forEachWidget{ widget in + if widget == folder {return} + if folder.sequenceSet.contains(widget.sequence){ + DispatchQueue.main.async {widget.isHidden = hidden} + if widget.isFolder { + // OnScreenWidgetView.setCollection(hidden: hidden, for: widget) + } + } + } + } + else{ + folder.forEachWidget{ widget in + if widget == folder {return} + if folder.sequenceSet.contains(widget.sequence){ + DispatchQueue.main.async {widget.isHidden = hidden} + } + } + if folder.revealMode == .exclusive { + folder.forEachWidget{ widget in + if widget == folder {return} + if widget.isFolder, !widget.sequenceSet.contains(folder.sequence) { + OnScreenWidgetView.setCollection(hidden: true, for: widget) + } + } + } + } + } + + private static func put(widget:OnScreenWidgetView, into folder:OnScreenWidgetView){ + widget.forEachWidget{ otherWidget in + otherWidget.sequenceSet.remove(widget.sequence) + } + folder.sequenceSet.insert(widget.sequence) + } + override func didMoveToSuperview() { super.didMoveToSuperview() if superview == nil { if self.motionControlButtonString == "GYRO" { - self.motionHandler.stopGyroUpdate(interruptNoneGyroInput: true) - self.motionHandler.gyroStarter = nil + self.motionHandler?.stopGyroUpdate(interruptNoneGyroInput: true) + self.motionHandler?.gyroStarter = nil } if self.motionControlButtonString == "ACCEL" {} if self.motionControlButtonString == "MOTION" {} @@ -2606,11 +2755,16 @@ import SVGKit self.clearLeftStickTouchPadFlag() self.clearRightStickTouchPadFlag() self.autoTapTimer?.clean() + self.onScreenControls = nil } else{ self.superViewWidth = (superview?.bounds.size.width)! self.superViewHeight = (superview?.bounds.size.height)! } } + + deinit { + print("onScreenWidgetView deinit \(CACurrentMediaTime())") + } } diff --git a/VoidLink/Input/PencilHandler.swift b/VoidLink/Input/PencilHandler.swift index b1657b62..896da6c6 100644 --- a/VoidLink/Input/PencilHandler.swift +++ b/VoidLink/Input/PencilHandler.swift @@ -9,12 +9,13 @@ import UIKit -@objc class PencilHandler: UIResponder { - @objc static var sharedInstance: PencilHandler? +@objc class PencilHandler: UIResponder, UIPencilInteractionDelegate { + @objc static var shared: PencilHandler? /// 用来处理所有 touch 事件的宿主视图 weak var streamView: UIView? // private var tickTimer: SafeTimer + private var pencilInteraction: Any? private var streamAspectRatio: Float private var tickInterval: TimeInterval private var manualTick: Bool @@ -22,6 +23,12 @@ import UIKit private var pressureCurveEnabled: Bool = false private var manualHoverFlag: Bool = false private var autoHoverFlag: Bool = false + private(set) var pencilProEnabled: Bool = false + @objc static private(set) var isDrawing: Bool = false + @objc static private(set) var pencilPausesNativeTouch: Bool = false + + // static private let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) + static private var selectedProfile:OSCProfile? private var pressureLUT = PressureCurveLUT(curve: PressureCurve()) @@ -33,22 +40,36 @@ import UIKit pencilTickEnabled = settings.pencilTickMode.intValue != PencilTickMode.PencilTickDisabled.rawValue super.init() setupPressureLUT() - PencilHandler.sharedInstance = self + PencilHandler.shared = self } public func setupPressureLUT(){ let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) - let oscProfile = oscProfileMan.getSelectedProfile() - pressureCurveEnabled = oscProfile.pressureCurveEnabled + PencilHandler.selectedProfile = oscProfileMan.getSelectedProfile() + guard let selectedProfile = PencilHandler.selectedProfile else {return} + + if selectedProfile.eraserShortcut != "" { + doubleTapShorcuts.append(selectedProfile.eraserShortcut) + } + if selectedProfile.brushShortcut != "" { + doubleTapShorcuts.append(selectedProfile.brushShortcut) + } + + pressureCurveEnabled = selectedProfile.pressureCurveEnabled if #available(iOS 15.0, *) { IAPManager.checkPurchaseInfo(.PencilProPack) { info in self.pressureCurveEnabled = self.pressureCurveEnabled && info.valid self.pencilTickEnabled = self.pencilTickEnabled && info.valid + self.pencilProEnabled = info.valid + PencilHandler.pencilPausesNativeTouch = selectedProfile.pencilPausesNativeTouch && info.valid + if info.valid { + self.setupPencilInteraction(view: self.streamView) + } } } - let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfile.pressureCurvePoints) + let persistedCurvePoints = PressureCurve.importCurvePoints(selectedProfile.pressureCurvePoints) let curve = PressureCurve() curve.polylinePoints = persistedCurvePoints curve.buildCurveSegments() @@ -61,6 +82,7 @@ import UIKit override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard let event = event else { return } + PencilHandler.isDrawing = true for touch in touches { let coalesced = event.coalescedTouches(for: touch) ?? [] _ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch]) @@ -189,13 +211,16 @@ import UIKit DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) { LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), targetForce, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude))) + if eventType == UInt8(LI_TOUCH_EVENT_UP) { + PencilHandler.isDrawing = false + } } } return dispatchMoment + tickMoment + delay } - @objc public func switchPencilHoever(){ + @objc public func switchPencilHover(){ manualHoverFlag = !manualHoverFlag } @@ -206,7 +231,146 @@ import UIKit @objc public func disablePencilHover(){ manualHoverFlag = false } + + @available(iOS 12.1, *) + private func setupPencilInteraction(view:UIView?) { + let interaction = UIPencilInteraction() + guard let view else { return } + interaction.delegate = self + view.addInteraction(interaction) + pencilInteraction = interaction + } + @available(iOS 12.1, *) + func pencilInteractionDidTap(_ interaction: UIPencilInteraction) { + if !pencilProEnabled {return} + if doubleTapShorcuts.isEmpty {return} + let keyStrings = CommandManager.shared.extractKeyStrings(from: doubleTapShorcuts[shorcutIndex]) + CommandManager.shared.sendKeyComboCommand(keyboardCmdStrings: keyStrings, delay: 0.1) + shorcutIndex = (shorcutIndex + 1) % doubleTapShorcuts.count + } + + @available(iOS 17.5, *) + func pencilInteraction( + _ interaction: UIPencilInteraction, + didReceiveSqueeze squeeze: UIPencilInteraction.Squeeze + ) { + switch squeeze.phase { + case .began: + break + case .ended, .cancelled: + break + default: + break + } + } + + @objc static private(set) var eraserShortcut:String = "" + @objc static private(set) var brushShortcut:String = "" + private var doubleTapShorcuts: Array = [] + private var shorcutIndex: Int = 0 + + @objc func replaceBrush(with shortcut:String){ + if let i = doubleTapShorcuts.indices.last { + doubleTapShorcuts[i] = shortcut + } + } + + @objc func replaceEraser(with shortcut:String){ + if let i = doubleTapShorcuts.indices.first { + doubleTapShorcuts[i] = shortcut + } + } + + @objc static public func enterDoubleTapShortcuts(in viewController: UIViewController){ + let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) + selectedProfile = oscProfileMan.getSelectedProfile() + guard let selectedProfile = selectedProfile else {return} + + let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Eraser Shortcut"), + message: SwiftLocalizationHelper.localizedString(forKey: "Enter eraser keyboard shortcut:"), + preferredStyle: .alert) + + alert.addTextField { textField in + textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: e, ctrl+e, alt+e ...") + textField.keyboardType = .asciiCapable + textField.autocorrectionType = .no + textField.spellCheckingType = .no + textField.text = selectedProfile.eraserShortcut + } + + let okAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in + let keyboardShortcut = alert.textFields?[0].text ?? "" + let keyStrings = CommandManager.shared.extractKeyStrings(from: keyboardShortcut) + if keyStrings?.count ?? 0 > 0 || keyboardShortcut == "" { + eraserShortcut = keyboardShortcut + } + enterBrushShortcut(in: viewController) + } + + let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in + if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + alert.addAction(learnMoreAction) + alert.addAction(okAction) + + viewController.present(alert, animated: true, completion: { + }) + + } + + @objc static public func enterBrushShortcut(in viewController: UIViewController){ + + let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Brush Shortcut"), + message: SwiftLocalizationHelper.localizedString(forKey: "Enter brush keyboard shortcut:"), + preferredStyle: .alert) + + alert.addTextField { textField in + textField.placeholder = SwiftLocalizationHelper.localizedString(forKey:"Example: b, ctrl+b, alt+b ...") + textField.keyboardType = .asciiCapable + textField.autocorrectionType = .no + textField.spellCheckingType = .no + textField.text = selectedProfile?.brushShortcut + } + + let okAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "OK"), style: .default) { _ in + let keyboardShortcut = alert.textFields?[0].text ?? "" + let keyStrings = CommandManager.shared.extractKeyStrings(from: keyboardShortcut) + if keyStrings?.count ?? 0 > 0 || keyboardShortcut == "" { + brushShortcut = keyboardShortcut + } + + if selectedProfile?.brushShortcut != brushShortcut + || selectedProfile?.eraserShortcut != eraserShortcut { + guard let selectedProfile = selectedProfile else {return} + let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) + selectedProfile.brushShortcut = brushShortcut + selectedProfile.eraserShortcut = eraserShortcut + oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true) + } + } + + let learnMoreAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Learn More"), style: .default) { _ in + if let url = URL(string: SwiftLocalizationHelper.localizedString(forKey: "pencilKeyboardCmdURL")) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + alert.addAction(learnMoreAction) + alert.addAction(okAction) + + viewController.present(alert, animated: true, completion: { + + }) + + } + + + + private func attachHoverLeave(normalizedLocation:CGPoint){ DispatchQueue.global().asyncAfter(deadline: .now()+0.0086) { LiSendPenEvent(UInt8(LI_TOUCH_EVENT_HOVER), UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), 0, 0, 0, 0, 0) diff --git a/VoidLink/Input/PressureCurve.swift b/VoidLink/Input/PressureCurve.swift index 8014f628..f64b048e 100644 --- a/VoidLink/Input/PressureCurve.swift +++ b/VoidLink/Input/PressureCurve.swift @@ -675,7 +675,7 @@ class PressureCurveViewController: UIViewController { buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"), countdown: 0, completion: { - PencilHandler.sharedInstance?.setupPressureLUT() + PencilHandler.shared?.setupPressureLUT() }) } } diff --git a/VoidLink/Input/PureNativeTouchHandler.m b/VoidLink/Input/PureNativeTouchHandler.m index 36bbc2ae..19663af0 100644 --- a/VoidLink/Input/PureNativeTouchHandler.m +++ b/VoidLink/Input/PureNativeTouchHandler.m @@ -15,8 +15,8 @@ #include @implementation PureNativeTouchHandler { - StreamView* streamView; TemporarySettings* currentSettings; + __weak StreamView* streamView; bool activateCoordSelector; CGFloat pointerVelocityDividerLocationByPoints; diff --git a/VoidLink/Input/RelativeTouchHandler.m b/VoidLink/Input/RelativeTouchHandler.m index 14f8e8d2..b5a78e3b 100644 --- a/VoidLink/Input/RelativeTouchHandler.m +++ b/VoidLink/Input/RelativeTouchHandler.m @@ -42,7 +42,7 @@ static const float QUICK_TAP_TIME_INTERVAL = 0.2; UIGestureRecognizer* remoteLongPressRecognizer; #endif - StreamView* streamView; + __weak StreamView* streamView; } - (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings { diff --git a/VoidLink/Input/StreamView.h b/VoidLink/Input/StreamView.h index ed16c839..2d96add0 100644 --- a/VoidLink/Input/StreamView.h +++ b/VoidLink/Input/StreamView.h @@ -34,13 +34,14 @@ @interface StreamView : UIView #endif -@property (assign, nonatomic) UIView* streamFrameTopLayerView; +@property (weak, nonatomic) UIView* streamFrameTopLayerView; +@property (weak, nonatomic) id interactionDelegate; @property (assign, nonatomic) CGFloat streamAspectRatio; @property (assign, nonatomic) CGRect originalFrame; @property (assign, nonatomic) bool widgetToolOpened; @property (strong, nonatomic) OnScreenControls* onScreenControls; @property (weak, nonatomic) PencilHandler* pencilHandler; - +@property (weak, nonatomic) UIViewController* streamFrameVC; - (void) setupStreamView:(ControllerSupport*)controllerSupport interactionDelegate:(id)interactionDelegate @@ -48,7 +49,9 @@ streamFrameTopLayerView:(UIView* )topLayerView ; -- (void) showOnScreenControls; +- (void)cleanUp; + +- (void) reloadLegacyWidgets:(OSCProfile* )profile; - (void) setOnScreenControls; - (void) disableOnScreenControls; - (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport @@ -56,7 +59,7 @@ - (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport andConfig:(StreamConfiguration*)streamConfig; - (void) clearOnScreenWidgets; -- (void) reloadOnScreenWidgetViews; +- (void) reloadOnScreenWidgetViews:(bool)reload; - (void) saveRelocatedWidgetViews; - (bool) isOnScreenWidgetEnabled; @@ -75,6 +78,7 @@ - (void)enablePencilHover; - (void)disablePencilHover; +- (void)setAllowSingleTouchEnabled:(BOOL)enabled; #if !TARGET_OS_TV - (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse; diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 263f2f00..bb897c18 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -26,6 +26,7 @@ #import "KeyboardInputField.h" #import "CustomTapGestureRecognizer.h" #import "LocalizationHelper.h" +#import "StreamFrameViewController.h" static const double X1_MOUSE_SPEED_DIVISOR = 2.5; @@ -34,8 +35,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; Stream Video has been moved out of this class to _renderView in StreamFrameViewController. */ @implementation StreamView { - UIView* streamFrameTopLayerView; - UIViewController* streamFrameVC; TemporarySettings* settings; @@ -59,7 +58,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; UIResponder* touchHandler; - id interactionDelegate; NSTimer* interactionTimer; BOOL hasUserInteracted; @@ -85,9 +83,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; streamFrameTopLayerView:(UIView* )topLayerView{ self->comboKeyModifierFlags = (UIKeyModifierControl|UIKeyModifierAlternate|UIKeyModifierShift); - self->streamFrameTopLayerView = topLayerView; - self.streamFrameTopLayerView = topLayerView; // this will be used as a read-only pointer for other class - self->interactionDelegate = interactionDelegate; + self->_streamFrameTopLayerView = topLayerView; + self->_interactionDelegate = interactionDelegate; self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height; self.streamAspectRatio = self->streamAspectRatio; _widgetSizeTransition = keepWidgetSize; @@ -128,8 +125,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self]; #else - PencilHandler.sharedInstance = [[PencilHandler alloc] initWithStreamView:self settings:settings]; - _pencilHandler = PencilHandler.sharedInstance; + PencilHandler.shared = [[PencilHandler alloc] initWithStreamView:self settings:settings]; + _pencilHandler = PencilHandler.shared; // iOS uses touch Mode depending on user preference @@ -160,8 +157,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } // we'll render on-screen controls on the toplayer too: - _onScreenControls = [[OnScreenControls alloc] initWithView:self->streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; // don't delete, this is mandatory - OnScreenControls.shared = _onScreenControls; + _onScreenControls = [[OnScreenControls alloc] initWithView:self->_streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; // don't delete, this is mandatory + // OnScreenControls.shared = _onScreenControls; /* // here we pass the tap recognizer to the onscreencontrols obj if (settings.touchMode.intValue == RelativeTouch){ @@ -400,7 +397,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; // Notify the delegate if this was a new user interaction if (!timerAlreadyRunning) { - [interactionDelegate userInteractionBegan]; + [_interactionDelegate userInteractionBegan]; } } @@ -408,7 +405,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; if (!hasUserInteracted) { // User has finished touching the screen interactionTimer = nil; - [interactionDelegate userInteractionEnded]; + [_interactionDelegate userInteractionEnded]; } else { // User is still touching the screen. Restart the timer. @@ -416,9 +413,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } } -- (void) showOnScreenControls { +- (void) reloadLegacyWidgets:(OSCProfile* )profile { #if !TARGET_OS_TV - if(!_widgetToolOpened) [_onScreenControls show]; + if(!_widgetToolOpened) [_onScreenControls showLegacyWidgetsWith:profile]; #endif } @@ -446,16 +443,17 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; - (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport andConfig:(StreamConfiguration*)streamConfig { [self reloadOnScreenControlsWith:controllerSupport andConfig:streamConfig]; - if([self isOscEnabled]) [self showOnScreenControls]; - else [self disableOnScreenControls]; + /* + StreamFrameViewController* vc = (StreamFrameViewController* )_streamFrameVC; + if([self isOscEnabled]) [self reloadLegacyWidgets:nil]; + else [self disableOnScreenControls];*/ } - - (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport andConfig:(StreamConfiguration*)streamConfig { // we'll render on-screen controllers on the toplayer too. - _onScreenControls = [[OnScreenControls alloc] initWithView:self->streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; + _onScreenControls = [[OnScreenControls alloc] initWithView:self->_streamFrameTopLayerView controllerSup:controllerSupport streamConfig:streamConfig]; /* // pass mouseRightClickTapRecognizer to onScreenControls obj here: if([self isOscEnabled]){ @@ -469,7 +467,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; - (void) clearOnScreenWidgets{ OnScreenWidgetView.isTweakingHighlight = false; - for (UIView *subview in self->streamFrameTopLayerView.subviews) { + for (UIView *subview in self->_streamFrameTopLayerView.subviews) { // 检查子视图是否是特定类型的实例 if ([subview isKindOfClass:[OnScreenWidgetView class]]) { // 如果是,就添加到将要被移除的数组中 @@ -480,8 +478,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; - (CGPoint)denormalizeWidgetPosition:(CGPoint)position { if(position.x < 1.0 && position.y < 1.0){ - position.x = position.x * streamFrameTopLayerView.bounds.size.width; - position.y = position.y * streamFrameTopLayerView.bounds.size.height; + position.x = position.x * _streamFrameTopLayerView.bounds.size.width; + position.y = position.y * _streamFrameTopLayerView.bounds.size.height; } else{ NSLog(@"invalid coords"); @@ -498,23 +496,25 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; - (void)saveRelocatedWidgetViews{ NSMutableDictionary* relocatedWidgetDict = [NSMutableDictionary dictionary]; - for(UIView* subview in self->streamFrameTopLayerView.subviews){ + for(UIView* subview in self->_streamFrameTopLayerView.subviews){ if([subview isKindOfClass:[OnScreenWidgetView class]]){ OnScreenWidgetView* widget = (OnScreenWidgetView* )subview; - if(widget.relocatedDuringStreaming) [relocatedWidgetDict setObject:widget forKey:widget.identifier]; + if(widget.relocatedDuringStreaming) [relocatedWidgetDict setObject:widget forKey:@(widget.sequence)]; } } + NSLog(@"relocatedWidgetDict.allKeys.count %d, %f", relocatedWidgetDict.allKeys.count, CACurrentMediaTime()); + if(relocatedWidgetDict.allKeys.count == 0) return; - oscProfileMan = [OSCProfilesManager sharedManager:self->streamFrameTopLayerView.bounds]; + 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 = [oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded]; - if([relocatedWidgetDict.allKeys containsObject:newButtonState.identifier]){ - OnScreenWidgetView* widget = [relocatedWidgetDict objectForKey:newButtonState.identifier]; + if([relocatedWidgetDict.allKeys containsObject:@(newButtonState.sequence)]){ + OnScreenWidgetView* widget = [relocatedWidgetDict objectForKey:@(newButtonState.sequence)]; newButtonState.position = [oscProfileMan normalizeWidgetPosition:widget.center]; NSData *newButtonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:newButtonState requiringSecureCoding:YES error:nil]; newProfile.buttonStatesEncoded[i] = newButtonStateEncoded; @@ -524,105 +524,125 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [oscProfileMan replaceSelectedProfileWith:newProfile overwriteDefault:NO]; } -- (void) reloadOnScreenWidgetViews{ - - // NSLog(@"reload on screen keyboard buttons here"); +- (void) reloadOnScreenWidgetViews:(bool)reload{ + NSLog(@"reloadOnScreenWidgets in streamview %f", CACurrentMediaTime()); - // remove all keyboard widget views first - [self clearOnScreenWidgets]; - - // get streamFrameVC - if(!streamFrameVC) streamFrameVC = [self parentViewController]; - - // bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom; - - if(![self isOnScreenWidgetEnabled]) return; - - OnScreenWidgetView.buttonVisualFeedbackEnabled = settings.buttonVisualFeedback; - - 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 = [oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded]; - if(buttonState.widgetType == CustomOnScreenWidget){ - OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape]; //reconstruct widgetView - //-------------------------------------------------- - _onScreenControls.instanceReceiverDelegate = widgetView; // connecting onScreenControls to OnScreenWidgetView, sending the active instance for touchPad stick control - [_onScreenControls sendInstance]; - //-------------------------------------------------- - - widgetView.functionalButtonDelegate = (id)streamFrameVC; - widgetView.motionHandler = 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 - widgetView.widthFactor = buttonState.widthFactor; - widgetView.heightFactor = buttonState.heightFactor; - widgetView.componentSizeFactor = buttonState.componentSizeFactor; - widgetView.borderWidth = buttonState.borderWidth; - widgetView.highlightSizeFactor = buttonState.highlightSizeFactor; - widgetView.autoTapInterval = buttonState.autoTapInterval; - [widgetView setVibrationWithStyle:buttonState.vibrationStyle]; - widgetView.mouseButtonAction = buttonState.mouseButtonAction; - widgetView.sensitivityFactorX = buttonState.sensitivityFactorX; - widgetView.sensitivityFactorY = buttonState.sensitivityFactorY; - widgetView.slideThreshold = buttonState.slideThreshold; - widgetView.yawFactor = buttonState.yawFactor; - widgetView.pitchFactor = buttonState.pitchFactor; - widgetView.rollFactor = buttonState.rollFactor; - widgetView.decelerationRateX = buttonState.decelerationRateX; - widgetView.decelerationRateY = buttonState.decelerationRateY; - widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; - widgetView.minStickOffset = buttonState.minStickOffset; - widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold; - widgetView.buttonMode = buttonState.buttonMode; - // Add the widgetView to the view controller's view - [self->streamFrameTopLayerView addSubview:widgetView]; // add keyboard button to the stream frame view. must add it to the target view before setting location. - buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; - [widgetView setLocationWithPosition:buttonState.position]; - widgetView.sizeReference = [self getCurrentWidgetSizeReference]; - // widgetView.sizeReference = buttonState.sizeReference; - //portrait markmarkmark - [widgetView resizeWidgetView]; // resize must be called after relocation - [widgetView adjustTransparencyWithAlpha:buttonState.backgroundAlpha tweakBorderAlpha:NO]; - [widgetView adjustBorderWithWidth:buttonState.borderWidth]; - [widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha]; - [widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha]; - [widgetView tweakHighlightAlphaWithAlpha:buttonState.highlightAlpha]; - [widgetView setupAutoTapTimer]; - [widgetView setupInertialScroller]; - } - } + dispatch_async(dispatch_get_main_queue(), ^{ + if(!self->oscProfileMan) self->oscProfileMan = [OSCProfilesManager sharedManager:self->_streamFrameTopLayerView.bounds]; + OSCProfile *oscProfile = [self->oscProfileMan getSelectedProfile]; //returns the currently selected OSCProfile + MotionHandler* motionHandler = [MotionHandler sharedWithProfile:oscProfile]; - uint64_t buttonIndex = 9999999; - UIView* deepestButton; - for (UIView *subview in self->streamFrameTopLayerView.subviews) { - if ([subview isKindOfClass:[OnScreenWidgetView class]]) { - OnScreenWidgetView* widget = (OnScreenWidgetView* ) subview; - if(widget.widgetType == WidgetTypeEnumButton){ - uint64_t index = [self->streamFrameTopLayerView.subviews indexOfObject:subview]; - if (index_streamFrameVC) self->_streamFrameVC = [self parentViewController]; + + // bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom; + + if(![self isOnScreenWidgetEnabled]) return; + + OnScreenWidgetView.buttonVisualFeedbackEnabled = self->settings.buttonVisualFeedback; + + bool hasLegacyWidget = false; + if(reload && !OnScreenWidgetView.editMode){ + // remove all keyboard widget views first + [self clearOnScreenWidgets]; + + bool sequenceGenerated = false; + bool hasMovableWidget = false; + for (NSInteger i = 0; i < oscProfile.buttonStatesEncoded.count; i++) { + NSData* buttonStateEncoded = oscProfile.buttonStatesEncoded[i]; + OnScreenButtonState* buttonState = [self->oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded]; + if(buttonState.widgetType == CustomOnScreenWidget){ + OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView + + widgetView.functionalButtonDelegate = (id)self->_streamFrameVC; + widgetView.motionHandler = motionHandler; + + widgetView.sequence = buttonState.sequence == -1 ? i : buttonState.sequence; + if(!sequenceGenerated) sequenceGenerated = buttonState.sequence == -1; + if(!hasMovableWidget) hasMovableWidget = buttonState.buttonMode == movable; + if(sequenceGenerated){ + NSLog(@"widgetView.sequence %d %f", widgetView.sequence, CACurrentMediaTime()); + buttonState.sequence = widgetView.sequence; + oscProfile.buttonStatesEncoded[i] = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil]; + } + + widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place + widgetView.widthFactor = buttonState.widthFactor; + widgetView.heightFactor = buttonState.heightFactor; + widgetView.componentSizeFactor = buttonState.componentSizeFactor; + widgetView.borderWidth = buttonState.borderWidth; + widgetView.highlightSizeFactor = buttonState.highlightSizeFactor; + widgetView.autoTapInterval = buttonState.autoTapInterval; + [widgetView setVibrationWithStyle:buttonState.vibrationStyle]; + widgetView.mouseButtonAction = buttonState.mouseButtonAction; + widgetView.sensitivityFactorX = buttonState.sensitivityFactorX; + widgetView.sensitivityFactorY = buttonState.sensitivityFactorY; + widgetView.slideThreshold = buttonState.slideThreshold; + widgetView.yawFactor = buttonState.yawFactor; + widgetView.pitchFactor = buttonState.pitchFactor; + widgetView.rollFactor = buttonState.rollFactor; + widgetView.decelerationRateX = buttonState.decelerationRateX; + widgetView.decelerationRateY = buttonState.decelerationRateY; + widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; + widgetView.minStickOffset = buttonState.minStickOffset; + widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold; + widgetView.buttonMode = buttonState.buttonMode; + // Add the widgetView to the view controller's view + [self->_streamFrameTopLayerView addSubview:widgetView]; // add keyboard button to the stream frame view. must add it to the target view before setting location. + buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; + [widgetView setLocationWithPosition:buttonState.position]; + widgetView.sizeReference = [self getCurrentWidgetSizeReference]; + // widgetView.sizeReference = buttonState.sizeReference; + //portrait markmarkmark + [widgetView resizeWidgetView]; // resize must be called after relocation + [widgetView adjustTransparencyWithAlpha:buttonState.backgroundAlpha tweakBorderAlpha:NO]; + [widgetView adjustBorderWithWidth:buttonState.borderWidth]; + [widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha]; + [widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha]; + [widgetView tweakHighlightAlphaWithAlpha:buttonState.highlightAlpha]; + [widgetView setupAutoTapTimer]; + [widgetView setupInertialScrollerWithFps:self->settings.framerate.intValue]; + } + else if(buttonState.widgetType == LegacyOnScreenControls) hasLegacyWidget = true; + } + + if(sequenceGenerated && hasMovableWidget){ + [self->oscProfileMan replaceSelectedProfileWith:oscProfile overwriteDefault:YES]; + } + + uint64_t buttonIndex = 9999999; + UIView* deepestButton; + for (UIView *subview in self->_streamFrameTopLayerView.subviews) { + if ([subview isKindOfClass:[OnScreenWidgetView class]]) { + OnScreenWidgetView* widget = (OnScreenWidgetView* ) subview; + if(widget.widgetType == WidgetTypeEnumButton){ + uint64_t index = [self->_streamFrameTopLayerView.subviews indexOfObject:subview]; + if (index_streamFrameTopLayerView.subviews) { + if ([subview isKindOfClass:[OnScreenWidgetView class]]) { + OnScreenWidgetView* widget = (OnScreenWidgetView* ) subview; + if(widget.widgetType == WidgetTypeEnumTouchPad){ + [self->_streamFrameTopLayerView insertSubview:subview belowSubview:deepestButton]; + } } } } } - if(!deepestButton) return; - for (UIView *subview in self->streamFrameTopLayerView.subviews) { - if ([subview isKindOfClass:[OnScreenWidgetView class]]) { - OnScreenWidgetView* widget = (OnScreenWidgetView* ) subview; - if(widget.widgetType == WidgetTypeEnumTouchPad){ - [self->streamFrameTopLayerView insertSubview:subview belowSubview:deepestButton]; - } - } + NSLog(@"hasLegacyWidget %d %f", hasLegacyWidget, CACurrentMediaTime()); + // legacy widgets + if(!OnScreenWidgetView.editMode && (hasLegacyWidget || !reload)){ + if([self isOscEnabled]) [self reloadLegacyWidgets:oscProfile]; + else [self disableOnScreenControls]; } - } + }); } - (OnScreenControlsLevel) getCurrentOscState { @@ -787,14 +807,18 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [touchHandler touchesBegan:touches withEvent:event]; return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only. } - if (@available(iOS 13.4, *)) { // now only pencil events are restricted - for (UITouch* touch in touches) { - if (touch.type == UITouchTypePencil) { - [_pencilHandler touchesBegan:touches withEvent:event]; - return; - } + NSMutableSet* pencilTouches = [NSMutableSet set]; + for (UITouch* touch in touches) { + if (touch.type == UITouchTypePencil) { + [pencilTouches addObject:touch]; } } + NSMutableSet* nonPencilTouches; + if(pencilTouches.count>0){ + [_pencilHandler touchesBegan:pencilTouches withEvent:event]; + nonPencilTouches = [touches mutableCopy]; + [nonPencilTouches minusSet:pencilTouches]; + } #endif if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS @@ -809,11 +833,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; // Notify of user interaction and start expiration timer [self startInteractionTimer]; + NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches; if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){ - [self->_onScreenControls handleTouchDownEvent:touches]; - [self->touchHandler touchesBegan:touches withEvent:event]; + [self->_onScreenControls handleTouchDownEvent:targetTouches]; + [self->touchHandler touchesBegan:targetTouches withEvent:event]; } - else if(![_onScreenControls handleTouchDownEvent:touches]) [touchHandler touchesBegan:touches withEvent:event]; + else if(![_onScreenControls handleTouchDownEvent:targetTouches]) [touchHandler touchesBegan:targetTouches withEvent:event]; } - (UIBarButtonItem *)createButtonWithImageNamed:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor target:(id)target action:(SEL)action keyCode:(NSInteger)keyCode isToggleable:(BOOL)isToggleable { @@ -944,10 +969,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only. } + NSMutableSet* pencilTouches = [NSMutableSet set]; for (UITouch* touch in touches) { if (touch.type == UITouchTypePencil) { - [_pencilHandler touchesMoved:touches withEvent:event]; - return; + [pencilTouches addObject:touch]; } if (@available(iOS 13.4, *)) { UITouch *touch = [touches anyObject]; @@ -970,14 +995,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } #endif } - + NSMutableSet* nonPencilTouches; + if(pencilTouches.count>0){ + [_pencilHandler touchesMoved:pencilTouches withEvent:event]; + nonPencilTouches = [touches mutableCopy]; + [nonPencilTouches minusSet:pencilTouches]; + } + hasUserInteracted = YES; + NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches; if(self->settings.touchMode.intValue == NativeTouch || self->settings.touchMode.intValue == RelativeTouch){ - [self->touchHandler touchesMoved:touches withEvent:event]; - [self->_onScreenControls handleTouchMovedEvent:touches]; + [self->touchHandler touchesMoved:targetTouches withEvent:event]; + [self->_onScreenControls handleTouchMovedEvent:targetTouches]; } - else if(![self->_onScreenControls handleTouchMovedEvent:touches]) [self->touchHandler touchesMoved:touches withEvent:event]; + else if(![self->_onScreenControls handleTouchMovedEvent:targetTouches]) [self->touchHandler touchesMoved:targetTouches withEvent:event]; } - (void) handleKeyCombos:(UIPress*) press{ @@ -986,19 +1018,19 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } switch (press.key.keyCode) { case UIKeyboardHIDUsageKeyboardQ: - [interactionDelegate streamExitRequested]; + [_interactionDelegate streamExitRequested]; break; case UIKeyboardHIDUsageKeyboardS: - [interactionDelegate toggleStatsOverlay]; + [_interactionDelegate toggleStatsOverlay]; break; case UIKeyboardHIDUsageKeyboardM: - [interactionDelegate toggleMouseCapture]; + [_interactionDelegate toggleMouseCapture]; break; case UIKeyboardHIDUsageKeyboardC: - [interactionDelegate toggleMouseVisible]; + [_interactionDelegate toggleMouseVisible]; break; case UIKeyboardHIDUsageKeyboardD: - [interactionDelegate disconnectAndQuitApp]; + [_interactionDelegate disconnectAndQuitApp]; break; default: break; @@ -1052,18 +1084,22 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only. } - if (@available(iOS 13.4, *)){ //now only pencil events are restricted - for (UITouch* touch in touches) { - if (touch.type == UITouchTypePencil) { - [_pencilHandler touchesEnded:touches withEvent:event]; - return; - } + NSMutableSet* pencilTouches = [NSMutableSet set]; + for (UITouch* touch in touches) { + if (touch.type == UITouchTypePencil) { + [pencilTouches addObject:touch]; } } + NSMutableSet* nonPencilTouches; + if(pencilTouches.count>0){ + [_pencilHandler touchesEnded:pencilTouches withEvent:event]; + nonPencilTouches = [touches mutableCopy]; + [nonPencilTouches minusSet:pencilTouches]; + } #endif if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE - forTouches:touches + forTouches:nonPencilTouches ? nonPencilTouches : touches withEvent:event]) { // If it's a mouse event, we're done return; @@ -1073,22 +1109,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; hasUserInteracted = YES; + NSSet* targetTouches = nonPencilTouches ? nonPencilTouches : touches; if(settings.touchMode.intValue == NativeTouch || settings.touchMode.intValue == RelativeTouch){ - [self->touchHandler touchesEnded:touches withEvent:event]; // when touches ended, must call the native touchhandler before onScreenControls, since the NSSet of touches captured by on screen button shall be updated later - [self->_onScreenControls handleTouchUpEvent:touches]; + [self->touchHandler touchesEnded:targetTouches withEvent:event]; // when touches ended, must call the native touchhandler before onScreenControls, since the NSSet of touches captured by on screen button shall be updated later + [self->_onScreenControls handleTouchUpEvent:targetTouches]; } - else if(![_onScreenControls handleTouchUpEvent:touches]) [touchHandler touchesEnded:touches withEvent:event]; + else if(![_onScreenControls handleTouchUpEvent:targetTouches]) [touchHandler touchesEnded:targetTouches withEvent:event]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [touchHandler touchesCancelled:touches withEvent:event]; #if !TARGET_OS_TV if (settings.touchMode.intValue == NativeTouchOnly) return; //This is a native touch oriented fork, in pure native touch mode, this call back method deals with native touch only. - if (@available(iOS 13.4, *)){ //now only pencil events are restricted - for (UITouch* touch in touches) { - if (touch.type == UITouchTypePencil) { - [self touchesEnded:touches withEvent:event]; - } + for (UITouch* touch in touches) { + if (touch.type == UITouchTypePencil) { + [self touchesEnded:touches withEvent:event]; } } #endif @@ -1434,7 +1469,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } - (void)switchPencilHover{ - [_pencilHandler switchPencilHoever]; + [_pencilHandler switchPencilHover]; } - (void)enablePencilHover{ @@ -1445,6 +1480,22 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [_pencilHandler disablePencilHover]; } +- (void)setAllowSingleTouchEnabled:(BOOL)enabled{ + if([touchHandler isKindOfClass:[NativeTouchHandler class]]){ + NativeTouchHandler* handler = (NativeTouchHandler* )touchHandler; + [handler setAllowSingleTouchEnabled:enabled]; + } +} + +- (void)cleanUp{ + [keyInputField resignFirstResponder]; + keyInputField.delegate = nil; +} + +- (void)dealloc{ + NSLog(@"dealloc streamView %f", CACurrentMediaTime()); +} + #if !TARGET_OS_TV - (BOOL)isMultipleTouchEnabled { return YES; diff --git a/VoidLink/Input/UITouchUtil.swift b/VoidLink/Input/UITouchUtil.swift index b0d61472..861f423e 100644 --- a/VoidLink/Input/UITouchUtil.swift +++ b/VoidLink/Input/UITouchUtil.swift @@ -11,7 +11,7 @@ import UIKit @objc class UITouchUtil: NSObject { @objc public static func touches(in view: UIView, from event: UIEvent?) -> Set { guard (event?.allTouches) != nil else { return [] } - return event?.allTouches?.filter({ $0.view == view }) ?? [] + return event?.allTouches?.filter({ $0.view == view && $0.type == .direct}) ?? [] } @objc public static func distance(between touch1: UITouch?, and touch2: UITouch?, in view: UIView?) -> CGFloat { diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index f14ef6aa..8b4ef06c 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -1192,6 +1192,29 @@ } } }, + "Brush Shortcut" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "笔刷快捷键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆刷快速鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆刷快速鍵" + } + } + } + }, "Calibrating..." : { "extractionState" : "manual", "localizations" : { @@ -2033,6 +2056,35 @@ } } }, + "doubleTapShortcutStackTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brush shortcut: %@\nEraser shortcut: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "笔刷快捷键: %@\n橡皮擦快捷键: %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆刷快速鍵: %@\n橡皮擦快速鍵: %@" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆刷快速鍵: %@\n橡皮擦快速鍵: %@" + } + } + } + }, "Down" : { "extractionState" : "manual", "localizations" : { @@ -2281,6 +2333,29 @@ } } }, + "Enter brush keyboard shortcut:" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入笔刷快捷键:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入筆刷快速鍵:" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入筆刷快速鍵:" + } + } + } + }, "Enter Custom Resolution" : { "extractionState" : "manual", "localizations" : { @@ -2304,6 +2379,29 @@ } } }, + "Enter eraser keyboard shortcut:" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入橡皮擦快捷键:" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入橡皮擦快速鍵:" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "輸入橡皮擦快速鍵:" + } + } + } + }, "Enter IP address to add host manually" : { "extractionState" : "manual", "localizations" : { @@ -2402,6 +2500,75 @@ } } }, + "Eraser Shortcut" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "橡皮擦快捷键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "橡皮擦快速鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "橡皮擦快速鍵" + } + } + } + }, + "Example: b, ctrl+b, alt+b ..." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "例如: b, ctrl+b, alt+b …" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "例如: b, ctrl+b, alt+b …" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "例如: b, ctrl+b, alt+b …" + } + } + } + }, + "Example: e, ctrl+e, alt+e ..." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "例如: e, ctrl+e, alt+e …" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "例如: e, ctrl+e, alt+e …" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "例如: e, ctrl+e, alt+e …" + } + } + } + }, "Exit" : { "extractionState" : "manual", "localizations" : { @@ -4822,6 +4989,35 @@ } } }, + "pencilKeyboardCmdURL" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/ma82z70rdt6u99b4#32535f99" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/nm6l9yixu0idsybg#32535f99" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/nm6l9yixu0idsybg#32535f99" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://voidlink.yuque.com/org-wiki-voidlink-znirha/fa3tgr/nm6l9yixu0idsybg#32535f99" + } + } + } + }, "PencilProPackDescription" : { "extractionState" : "manual" }, diff --git a/VoidLink/Stream/StreamManager.m b/VoidLink/Stream/StreamManager.m index 18ef2e07..d64ea23a 100644 --- a/VoidLink/Stream/StreamManager.m +++ b/VoidLink/Stream/StreamManager.m @@ -117,6 +117,7 @@ - (void) stopStream { [_connection terminate]; + _callbacks = nil; } - (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl { diff --git a/VoidLink/Stream/VideoDecoderRenderer.m b/VoidLink/Stream/VideoDecoderRenderer.m index 046895b1..d3ad0c48 100644 --- a/VoidLink/Stream/VideoDecoderRenderer.m +++ b/VoidLink/Stream/VideoDecoderRenderer.m @@ -36,7 +36,7 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size, @implementation VideoDecoderRenderer { dispatch_queue_t _sq, _vtq; StreamView* _view; - id _callbacks; + __weak id _callbacks; float _streamAspectRatio; AVSampleBufferDisplayLayer* _displayLayer; diff --git a/VoidLink/Utility/GraphicUtils.swift b/VoidLink/Utility/GraphicUtils.swift index febaf63b..03c3f5c0 100644 --- a/VoidLink/Utility/GraphicUtils.swift +++ b/VoidLink/Utility/GraphicUtils.swift @@ -11,19 +11,26 @@ import Foundation import SVGKit @objc public class GraphicUtils: NSObject { - @objc public class func makeCenteredSVGLayer( + @objc public class func makeSVGLayer( from file: String, in container: CALayer, + at normalizedPosition: CGPoint = .zero, targetSize: CGSize ) -> CALayer { - guard let svg = SVGKImage(named: file) else { - fatalError("Failed to load SVG \(file)") + guard let url = Bundle.main.url(forResource: file, withExtension: "svg"), + let data = try? Data(contentsOf: url) else { + return CALayer() } - return _makeCenteredSVGLayer( + guard let svg = SVGKImage(data: data) else { + fatalError("Failed to load SVG \(file)") + } + + return _makeSVGLayer( from: svg, in: container, + at: normalizedPosition, targetSize: targetSize ) } @@ -33,16 +40,17 @@ import SVGKit in container: CALayer, targetSize: CGSize ) -> CALayer { - return _makeCenteredSVGLayer( + return _makeSVGLayer( from: svg, in: container, targetSize: targetSize ) } - @objc public class func _makeCenteredSVGLayer( + @objc public class func _makeSVGLayer( from svg: SVGKImage, in container: CALayer, + at normalizedPosition: CGPoint = .zero, targetSize: CGSize, getWrapperLayer: Bool = true ) -> CALayer { @@ -64,14 +72,15 @@ import SVGKit y: -unionRect.origin.y ) ) - + + let realPosition = (normalizedPosition == .zero + ? CGPoint(x: container.bounds.midX,y: container.bounds.midY) + : CGPoint(x: container.bounds.width*normalizedPosition.x,y: container.bounds.height*normalizedPosition.y)) + let wrapper = CALayer() wrapper.bounds = CGRect(origin: .zero, size: unionRect.size) wrapper.anchorPoint = CGPoint(x: 0.5, y: 0.5) - wrapper.position = CGPoint( - x: container.bounds.midX, - y: container.bounds.midY - ) + wrapper.position = realPosition let scale = min( targetSize.width / unionRect.width, @@ -93,7 +102,7 @@ import SVGKit wrappedLayer.bounds = CGRect(x: 0, y: 0, width: container.bounds.size.width, height: container.bounds.size.height) wrappedLayer.position = CGPoint(x: container.bounds.midX, y: container.bounds.midY) wrappedLayer.insertSublayer(wrapper, at: 0) - + return wrappedLayer } diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h index 6bb3f921..12edc7b8 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN @interface LayoutOnScreenControlsViewController : UIViewController - (void)profileRefresh; - (void)reloadOnScreenWidgetViews; -- (void)presentProfilesTableView; +- (void)presentProfilesTableViewWithPickProfile:(bool)pickProfile; @property LayoutOnScreenControls *layoutOSC; // object that contains a view which contains the on screen controller buttons that allows the user to drag and positions each button on the screen using touch @property (nonatomic) NSMutableSet* onScreenWidgetViews; @@ -129,6 +129,9 @@ NS_ASSUME_NONNULL_BEGIN @property (strong, nonatomic) IBOutlet UILabel *walkModeThresholdLabel; @property (strong, nonatomic) IBOutlet UISlider *walkModeThresholdSlider; +@property (weak, nonatomic) IBOutlet UIStackView *collectedWidgetsStack; +@property (weak, nonatomic) IBOutlet UISegmentedControl *collectedWidgetsSelector; +@property (weak, nonatomic) IBOutlet UISegmentedControl *revealModeSelector; @property (weak, nonatomic) IBOutlet UIStackView *widgetPanelStack; diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index a548a9dd..f1acb078 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -103,8 +103,8 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { return newPosition; } -- (void)reloadLegacyOnScreenControls{ - [self.layoutOSC updateControls]; // creates and saves a 'Default' OSC profile or loads the o//ne the user selected on the previous screen +- (void)reloadLegacyOnScreenControls:(OSCProfile* )profile{ + [self.layoutOSC updateLegacyWidgetsWith:profile]; // creates and saves a 'Default' OSC profile or loads the o//ne the user selected on the previous screen [self addInnerAnalogSticksToOuterAnalogLayers]; [self.layoutOSC.layoutChanges removeAllObjects]; // since a new OSC profile is being loaded, this will remove all previous layout changes made from the array [self OSCLayoutChanged]; // fades the 'Undo Button' out @@ -123,74 +123,94 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { else return longSide; } +- (void)dummytest{ + +} -- (void)reloadOnScreenWidgetViews{ - NSLog(@"reloadOnScreenWidgets %f", CACurrentMediaTime()); - OnScreenWidgetView.isTweakingHighlight = false; - OnScreenWidgetView.editMode = true; - [self hideStickIndicators]; - +- (void)clearOnScreenWidgets{ for (UIView *subview in self.view.subviews) { if ([subview isKindOfClass:[OnScreenWidgetView class]]) { [subview removeFromSuperview]; } } - [self.onScreenWidgetViews removeAllObjects]; +} + +- (void)reloadOnScreenWidgetViews{ + OnScreenWidgetView.isTweakingHighlight = false; + OnScreenWidgetView.editMode = true; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self hideStickIndicators]; + [self clearOnScreenWidgets]; + + int16_t sequence = -1; + + // _activeCustomOscButtonPositionDict will be updated every time when the osc profile is reloaded + OSCProfile *oscProfile = [self->profilesManager getSelectedProfile]; //returns the currently selected OSCProfile + NSLog(@"reloadOnScreenWidgets %lu", oscProfile.buttonStatesEncoded.count); + bool hasLegacyWidget = false; + for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { + // OnScreenButtonState* buttonState = [NSKeyedUnarchiver unarchivedObjectOfClass:[OnScreenButtonState class] fromData:buttonStateEncoded error:nil]; + OnScreenButtonState* buttonState = [self->profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; + NSLog(@"reloadOnScreenWidgets name %@", buttonState.name); + if(buttonState.widgetType == CustomOnScreenWidget){ + OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView + widgetView.sequence = buttonState.sequence == -1 ? ++sequence : buttonState.sequence; + widgetView.guidelineDelegate = (id)self; + widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place + widgetView.widthFactor = buttonState.widthFactor; + widgetView.heightFactor = buttonState.heightFactor; + widgetView.componentSizeFactor = buttonState.componentSizeFactor; + widgetView.borderWidth = buttonState.borderWidth; + widgetView.highlightSizeFactor = buttonState.highlightSizeFactor; + widgetView.autoTapInterval = buttonState.autoTapInterval; + [widgetView setVibrationWithStyle:buttonState.vibrationStyle]; + widgetView.mouseButtonAction = buttonState.mouseButtonAction; + widgetView.sensitivityFactorX = buttonState.sensitivityFactorX; + widgetView.sensitivityFactorY = buttonState.sensitivityFactorY; + widgetView.slideThreshold = buttonState.slideThreshold; + widgetView.yawFactor = buttonState.yawFactor; + widgetView.pitchFactor = buttonState.pitchFactor; + widgetView.rollFactor = buttonState.rollFactor; + widgetView.decelerationRateX = buttonState.decelerationRateX; + widgetView.decelerationRateY = buttonState.decelerationRateY; + widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; + widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold; + widgetView.minStickOffset = buttonState.minStickOffset; + widgetView.buttonMode = buttonState.buttonMode; + // Add the widgetView to the view controller's view + [self.view insertSubview:widgetView belowSubview:self.widgetPanelStack]; + buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; + [widgetView setLocationWithPosition:buttonState.position]; + widgetView.sizeReference = [self getCurrentWidgetSizeReference]; + [widgetView resizeWidgetView]; // resize must be called after relocation + [widgetView adjustTransparencyWithAlpha:buttonState.backgroundAlpha tweakBorderAlpha:NO]; + [widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha]; + [widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha]; + [widgetView tweakHighlightAlphaWithAlpha:buttonState.highlightAlpha]; + [widgetView adjustBorderWithWidth:buttonState.borderWidth]; + [self.onScreenWidgetViews addObject:widgetView]; + } + else if(buttonState.widgetType == LegacyOnScreenControls) hasLegacyWidget = true; + } + NSLog(@"reloadOnScreenWidgets hasLegacyWidget %d %f", hasLegacyWidget, CACurrentMediaTime()); + if(hasLegacyWidget) [self reloadLegacyOnScreenControls:oscProfile]; + else{ + for(CALayer* layer in self.layoutOSC.OSCButtonLayerPool){ + [layer removeFromSuperlayer]; + } + [self.layoutOSC.OSCButtonLayerPool removeAllObjects]; + } + }); - NSLog(@"reload os Key here"); - - // _activeCustomOscButtonPositionDict will be updated every time when the osc profile is reloaded - OSCProfile *oscProfile = [profilesManager getSelectedProfile]; //returns the currently selected OSCProfile - for (NSData *buttonStateEncoded in oscProfile.buttonStatesEncoded) { - // OnScreenButtonState* buttonState = [NSKeyedUnarchiver unarchivedObjectOfClass:[OnScreenButtonState class] fromData:buttonStateEncoded error:nil]; - OnScreenButtonState* buttonState = [profilesManager unarchiveButtonStateEncoded:buttonStateEncoded]; - if(buttonState.widgetType == CustomOnScreenWidget){ - OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape]; //reconstruct widgetView - widgetView.identifier = buttonState.identifier; - NSLog(@"cmd: %@, id: %@", buttonState.name, widgetView.identifier); - widgetView.guidelineDelegate = (id)self; - widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place - widgetView.widthFactor = buttonState.widthFactor; - widgetView.heightFactor = buttonState.heightFactor; - widgetView.componentSizeFactor = buttonState.componentSizeFactor; - widgetView.borderWidth = buttonState.borderWidth; - widgetView.highlightSizeFactor = buttonState.highlightSizeFactor; - widgetView.autoTapInterval = buttonState.autoTapInterval; - [widgetView setVibrationWithStyle:buttonState.vibrationStyle]; - widgetView.mouseButtonAction = buttonState.mouseButtonAction; - widgetView.sensitivityFactorX = buttonState.sensitivityFactorX; - widgetView.sensitivityFactorY = buttonState.sensitivityFactorY; - widgetView.slideThreshold = buttonState.slideThreshold; - widgetView.yawFactor = buttonState.yawFactor; - widgetView.pitchFactor = buttonState.pitchFactor; - widgetView.rollFactor = buttonState.rollFactor; - widgetView.decelerationRateX = buttonState.decelerationRateX; - widgetView.decelerationRateY = buttonState.decelerationRateY; - widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; - widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold; - widgetView.minStickOffset = buttonState.minStickOffset; - widgetView.buttonMode = buttonState.buttonMode; - // Add the widgetView to the view controller's view - [self.view insertSubview:widgetView belowSubview:self.widgetPanelStack]; - buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; - [widgetView setLocationWithPosition:buttonState.position]; - widgetView.sizeReference = [self getCurrentWidgetSizeReference]; - [widgetView resizeWidgetView]; // resize must be called after relocation - [widgetView adjustTransparencyWithAlpha:buttonState.backgroundAlpha tweakBorderAlpha:NO]; - [widgetView tweakLabelAlphaWithAlpha:buttonState.labelAlpha]; - [widgetView tweakBorderAlphaWithAlpha:buttonState.borderAlpha]; - [widgetView tweakHighlightAlphaWithAlpha:buttonState.highlightAlpha]; - [widgetView adjustBorderWithWidth:buttonState.borderWidth]; - [self.onScreenWidgetViews addObject:widgetView]; - } - } } - (void) viewDidLoad { [super viewDidLoad]; profilesManager = [OSCProfilesManager sharedManager:self.view.bounds]; + NSLog(@"self.view.bounds %f, %f", self.view.bounds.size.width, self.view.bounds.size.height); self.onScreenWidgetViews = [[NSMutableSet alloc] init]; // will be revised to read persisted data , somewhere else [OSCProfilesManager setOnScreenWidgetViewsSet:self.onScreenWidgetViews]; // pass the keyboard button dict to profiles manager @@ -220,7 +240,6 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { self.layoutOSC._level = OnScreenControlsLevelCustom; self.layoutOSC.layoutToolVC = self; //[self.layoutOSC show]; // draw on screen controls - [self.layoutOSC show]; // draw on screen controls [self addInnerAnalogSticksToOuterAnalogLayers]; // allows inner and analog sticks to be dragged together around the screen together as one unit which is the expected behavior @@ -269,6 +288,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { widgetPanelStoredCenter = self.widgetPanelStack.center; [super viewDidAppear:animated]; [self profileRefresh]; + [self setupWidgetPanel]; } - (void)viewWillAppear:(BOOL)animated{ @@ -285,7 +305,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reloadOnScreenWidgetViews) + selector:@selector(dummytest) name:@"OscLayoutProfileSelctedInTableView" // This is a special notification for reloading the on screen keyboard buttons. which can't be executed by _oscProfilesTableViewController.needToUpdateOscLayoutTVC code block, and has to be triggered by a notification object:nil]; @@ -324,8 +344,9 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { _oscProfilesTableViewController.layoutViewBounds = self.view.bounds; [OSCProfilesManager setLayoutViewBounds:self.view.bounds]; [OSCProfilesManager setOnScreenWidgetViewsSet:self.onScreenWidgetViews]; // pass the keyboard button dict to profiles manager + [self reloadOnScreenWidgetViews]; - [self reloadLegacyOnScreenControls]; + // [self reloadLegacyOnScreenControls]; } - (void)handleEnterBackground{ @@ -740,8 +761,8 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { NSString *widgetShape = [widgetInitParams[@"shape"] lowercaseString]; widgetInitParams[@"cmdString"] = cmdString; - bool noValidKeyboardString = [CommandManager.shared extractKeyStringsFromComboCommandFrom:cmdString] == nil; // this is a invalid string. - bool noValidSuperComboButtonString = [CommandManager.shared extractSinglCmdStringsFromComboKeysFrom:cmdString] == nil; // this is a invalid string. + bool noValidKeyboardString = [CommandManager.shared extractKeyStringsFrom:cmdString] == nil; // this is a invalid string. + bool noValidSuperComboButtonString = [CommandManager.shared extractCmdStringsFrom:cmdString] == nil; // this is a invalid string. bool noValidMouseButtonString = ![CommandManager.mouseButtonMappings.allKeys containsObject:cmdString]; bool noValidTouchPadString = ![CommandManager.touchPadCmds containsObject:cmdString]; bool noValidOscButtonString = ![CommandManager.oscButtonMappings.allKeys containsObject:cmdString]; @@ -765,8 +786,9 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { - (void) updateWidget:(OnScreenWidgetView* )widget byParams:(NSMutableDictionary* )widgetInitParams createNew:(bool)createNew{ if(![self isWidgetParamsValid:widgetInitParams]) return; - OnScreenWidgetView* newWidget = [[OnScreenWidgetView alloc] initWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"]]; //reconstruct widgetView - newWidget.identifier = createNew ? [UUIDHelper newUUID] : widget.identifier; + OSCProfile* profile = [[OSCProfilesManager sharedManager:CGRectZero] getSelectedProfile]; + OnScreenWidgetView* newWidget = [[OnScreenWidgetView alloc] initWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"] profile:profile]; //reconstruct widgetView + newWidget.sequence = widget.sequence; newWidget.guidelineDelegate = (id)self; newWidget.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place newWidget.widthFactor = widget.widthFactor; @@ -798,7 +820,10 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [self.view insertSubview:newWidget belowSubview:self.widgetPanelStack]; [newWidget accessWidgetAttributes]; - if(createNew) [newWidget setLocationWithPosition:CGPointMake(90, 130)]; + if(createNew){ + [newWidget setLocationWithPosition:CGPointMake(90, 130)]; + newWidget.sequence = [newWidget getAvailableSequence]; + } else [newWidget setLocationWithPosition:widget.center]; newWidget.sizeReference = widget.sizeReference; [newWidget resizeWidgetView]; // resize must be called after relocation @@ -819,14 +844,17 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { - (void) createWidgetFromParams: (NSMutableDictionary*) widgetInitParams{ if(![self isWidgetParamsValid:widgetInitParams]) return; //saving & present the keyboard button: - OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"]]; - widgetView.identifier = [UUIDHelper newUUID]; + OSCProfile* profile = [[OSCProfilesManager sharedManager:CGRectZero] getSelectedProfile]; + OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"] profile:profile]; + [self.view insertSubview:widgetView belowSubview:self.widgetPanelStack]; + widgetView.hidden = true; + widgetView.sequence = [widgetView getAvailableSequence]; widgetView.guidelineDelegate = (id)self; widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place [self.onScreenWidgetViews addObject:widgetView]; // Add the widgetView to the view controller's view - [self.view insertSubview:widgetView belowSubview:self.widgetPanelStack]; [widgetView setLocationWithPosition:CGPointMake(90, 130)]; + widgetView.hidden = false; [widgetView resizeWidgetView]; [widgetView setVibrationWithStyle:UIImpactFeedbackStyleLight]; } @@ -834,7 +862,6 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { /* show pop up notification that lets users choose to save the current OSC layout configuration as a profile they can load when they want. User can also choose to cancel out of this pop up */ - (IBAction) saveTapped:(id)sender { - /* OSCProfile* targetProfile = [profilesManager getAllProfiles][0]; OSCProfile* currentProfile = [profilesManager getSelectedProfile]; @@ -846,7 +873,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [self clearSickInput]; [OSCProfilesManager setLayoutViewBounds:self.view.bounds]; - if([self->profilesManager updateSelectedProfile:self.layoutOSC.OSCButtonLayers]){ + if([self->profilesManager updateSelectedProfile:self.layoutOSC.OSCButtonLayerPool]){ UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [LocalizationHelper localizedStringForKey:@"Current profile updated successfully"] preferredStyle:UIAlertControllerStyleAlert]; [savedAlertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { }]]; @@ -904,7 +931,6 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { */ - (void)widgetViewTapped: (NSNotification *)notification{ - [self hideStickIndicators]; [self clearSickInput]; @@ -1064,7 +1090,15 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { self.buttonModeStack.hidden = selectedWidgetView.widgetType != WidgetTypeEnumButton; [self.buttonModeSelector setEnabled:!selectedWidgetView.isFuncationalButton forSegmentAtIndex:0]; [self.buttonModeSelector setEnabled:!selectedWidgetView.isFuncationalButton forSegmentAtIndex:1]; + [self.buttonModeSelector setEnabled:!selectedWidgetView.isFuncationalButton + || [selectedWidgetView.functionalButtonString containsString:@"ABSTCHDRAG"] + forSegmentAtIndex:3]; [self.buttonModeSelector setSelectedSegmentIndex:selectedWidgetView.buttonMode]; + + self.collectedWidgetsStack.hidden = !selectedWidgetView.isFolder; + [self.collectedWidgetsSelector setSelectedSegmentIndex:selectedWidgetView.folded ? 1 :0]; + [self.revealModeSelector setSelectedSegmentIndex:selectedWidgetView.revealMode]; + [OnScreenWidgetView setCollectionWithHidden:selectedWidgetView.folded for:selectedWidgetView]; [self autoFitStack:self.widgetPanelStack]; @@ -1228,6 +1262,19 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { } } +- (void)collectionHiddenChanged:(UISegmentedControl* )sender{ + if(self->selectedWidgetView != nil && self->widgetViewSelected){ + [OnScreenWidgetView setCollectionWithHidden:sender.selectedSegmentIndex==1 for:selectedWidgetView]; + } +} + +- (void)revealModeChanged:(UISegmentedControl* )sender{ + if(self->selectedWidgetView != nil && self->widgetViewSelected){ + selectedWidgetView.revealMode = sender.selectedSegmentIndex; + [OnScreenWidgetView setCollectionWithHidden:selectedWidgetView.folded for:selectedWidgetView]; + } +} + - (void)vibrationStyleChanged:(UISegmentedControl* )sender{ bool vibraiontOn; if (@available(iOS 13.0, *)) { @@ -1538,7 +1585,12 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [self.buttonModeSelector setTitleTextAttributes:whiteFontAttributes forState:UIControlStateNormal]; self.buttonModeStack.hidden = YES; - + [self.collectedWidgetsSelector addTarget:self action:@selector(collectionHiddenChanged:) forControlEvents:(UIControlEventValueChanged)]; + [self.collectedWidgetsSelector setTitleTextAttributes:whiteFontAttributes forState:UIControlStateNormal]; + [self.revealModeSelector addTarget:self action:@selector(revealModeChanged:) forControlEvents:(UIControlEventValueChanged)]; + [self.revealModeSelector setTitleTextAttributes:whiteFontAttributes forState:UIControlStateNormal]; + self.collectedWidgetsStack.hidden = YES; + if([self isIPhone]){ [self.vibrationStyleSelector addTarget:self action:@selector(vibrationStyleChanged:) forControlEvents:(UIControlEventValueChanged)]; self.vibrationStyleStack.hidden = YES; @@ -1637,8 +1689,9 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { } - (void)handleProfileTablViewDismiss{ - [self profileRefresh]; if(_quickSwitchEnabled) [self dismissViewControllerAnimated:NO completion:nil]; + else [self setupWidgetPanel]; + // setup: current profile lable, button width slider, button height slider & button alpha slider } /* Basically the same method as loadTapped, without parameter*/ @@ -1652,10 +1705,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { else { storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil]; } - - // setup: current profile lable, button width slider, button height slider & button alpha slider - [self setupWidgetPanel]; - + //initialiaze _oscProfilesTableViewController self->_oscProfilesTableViewController = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"]; @@ -1664,20 +1714,22 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { 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 __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; - [weakSelf reloadLegacyOnScreenControls]; - strongSelf->_oscProfilesTableViewController.currentOSCButtonLayers = weakSelf.layoutOSC.OSCButtonLayers; //pass updated OSCLayout to OSCProfileTableView again + [weakSelf reloadOnScreenWidgetViews]; + strongSelf->_oscProfilesTableViewController.currentOSCButtonLayers = weakSelf.layoutOSC.OSCButtonLayerPool; //pass updated OSCLayout to OSCProfileTableView again }; [self.oscProfilesTableViewController profileViewRefresh]; // execute this will make sure OSCLayout is updated from persisted profile, not any cache. - [self reloadOnScreenWidgetViews]; + NSLog(@"profileRefresh %f", CACurrentMediaTime()); + // [self reloadOnScreenWidgetViews]; // [self presentViewController:vc animated:YES completion:nil]; } -- (void) presentProfilesTableView{ - [self saveTapped:nil]; +- (void) presentProfilesTableViewWithPickProfile:(bool)pickProfile{ [self hideStickIndicators]; selectedWidgetView = nil; + // if(pickProfile) [self clearOnScreenWidgets]; + UIStoryboard *storyboard; BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); if (isIPhone) { @@ -1692,21 +1744,24 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { __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 - [weakSelf reloadLegacyOnScreenControls]; + if(!pickProfile || true) [weakSelf reloadOnScreenWidgetViews]; }; self.widgetPanelStack.hidden = YES; - _oscProfilesTableViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayers; + _oscProfilesTableViewController.currentOSCButtonLayers = self.layoutOSC.OSCButtonLayerPool; // _oscProfilesTableViewController.modalPresentationStyle = UIModalPresentationCurrentContext; _oscProfilesTableViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext; - [self presentViewController:_oscProfilesTableViewController animated:YES completion:nil]; + // _oscProfilesTableViewController.modalPresentationStyle = UIModalPresentationPageSheet; + _oscProfilesTableViewController.pickProfileEnabled = pickProfile; + [self presentViewController:_oscProfilesTableViewController animated:NO completion:nil]; } /* Presents the view controller that lists all OSC profiles the user can choose from */ - (IBAction) loadTapped:(id)sender { - [self presentProfilesTableView]; + [self saveTapped:nil]; + [self presentProfilesTableViewWithPickProfile:false]; } @@ -1849,5 +1904,9 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { widgetPanelMovedByTouch = false; } +- (void)dealloc{ + NSLog(@"dealloc layoutToolVC %f ...", CACurrentMediaTime()); +} + @end diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.h b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.h index ebd90c87..71e69271 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.h +++ b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.h @@ -29,9 +29,10 @@ typedef NS_ENUM(NSUInteger, FileOperation) { @property (weak, nonatomic) IBOutlet UITableView *tableView; @property (nonatomic, copy) void (^needToUpdateOscLayoutTVC)(void); -@property (nonatomic, assign) NSMutableArray *currentOSCButtonLayers; +@property (nonatomic, assign) NSMutableSet *currentOSCButtonLayers; @property (nonatomic, assign) CGRect layoutViewBounds; @property (weak, nonatomic) IBOutlet UINavigationBar *profileTableViewNavigationBar; +@property (nonatomic, assign) bool pickProfileEnabled; @end diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m index bf23f9b9..28fd9b1c 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/OSCProfilesTableViewController.m @@ -19,7 +19,7 @@ const double NAV_BAR_HEIGHT = 50; -@interface OSCProfilesTableViewController () +@interface OSCProfilesTableViewController () @end @@ -42,9 +42,15 @@ const double NAV_BAR_HEIGHT = 50; return [self getCurrentOrientation]; // 90 Degree rotation not allowed in streaming or app view } +/* - (void) viewDidDisappear:(BOOL)animated{ [[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutTableViewCloseNotification" object:self]; // notify other view that oscLayoutManager is closing [super viewDidDisappear:animated]; +}*/ + +- (void) viewWillDisappear:(BOOL)animated{ + [super viewWillDisappear:animated]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"OscLayoutTableViewCloseNotification" object:self]; // notify other view that oscLayoutManager is closing } @@ -88,6 +94,17 @@ const double NAV_BAR_HEIGHT = 50; self.tableView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5]; // set background color & transparency // self.tableView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2]; // set background color & transparency + UITapGestureRecognizer *tap = + [[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(dismissSelf)]; + tap.cancelsTouchesInView = NO; + tap.delegate = (id)self; + + [self.view addGestureRecognizer:tap]; +} + +- (void)dismissSelf { + [self dismissViewControllerAnimated:NO completion:nil]; } - (void) viewDidAppear:(BOOL)animated { @@ -180,6 +197,7 @@ const double NAV_BAR_HEIGHT = 50; //[self dismissViewControllerAnimated:YES completion:nil]; //[selfparentLayoutOSCViewController] [self.tableView reloadData]; // table view will be refreshed by calling reloadData + // if(_pickProfileEnabled) return; if (self.needToUpdateOscLayoutTVC) { // tells the presenting view controller to lay out the on screen buttons according to the selected profile's instructions self.needToUpdateOscLayoutTVC(); } @@ -215,7 +233,7 @@ const double NAV_BAR_HEIGHT = 50; } - (IBAction) exitTapped:(id)sender { - [self dismissViewControllerAnimated:YES completion:nil]; + [self dismissViewControllerAnimated:NO completion:nil]; } #pragma mark - UIDocumentPickerDelegate @@ -438,5 +456,13 @@ didPickDocumentsAtURLs:(NSArray *)urls { [self profileViewRefresh]; // update OSC layout when table view option is changed } +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer + shouldReceiveTouch:(UITouch *)touch { + if (_pickProfileEnabled && [touch.view isDescendantOfView:self.tableView]) { + return true; + } + if(touch.view == self.view) return true; + return false; +} @end diff --git a/VoidLink/ViewControllers/MainFrameViewController.m b/VoidLink/ViewControllers/MainFrameViewController.m index 3e9ac4ab..e73ba3e4 100644 --- a/VoidLink/ViewControllers/MainFrameViewController.m +++ b/VoidLink/ViewControllers/MainFrameViewController.m @@ -72,7 +72,7 @@ UIView* menuSeparator; UIView* snapshot; SettingsViewController* settingsViewController; - StreamFrameViewController* streamFrameViewController; + __weak StreamFrameViewController* streamFrameViewController; id navBarAppearanceStandard; bool _viewJustAppeared; TemporaryApp * launchedApp; @@ -1257,6 +1257,7 @@ static NSMutableSet* hostList; streamFrameViewController.mainFrameViewcontroller = self; streamFrameViewController.streamConfig = _streamConfig; } + NSLog(@"streamVC seque... %lu %f",(uintptr_t)streamFrameViewController , CACurrentMediaTime()); } - (void) showLoadingFrame:(void (^)(void))completion { diff --git a/VoidLink/ViewControllers/SWRevealViewController.h b/VoidLink/ViewControllers/SWRevealViewController.h index a26d14ed..18a7da67 100755 --- a/VoidLink/ViewControllers/SWRevealViewController.h +++ b/VoidLink/ViewControllers/SWRevealViewController.h @@ -206,7 +206,7 @@ typedef NS_ENUM(NSInteger, SWRevealToggleAnimationType) - (void)setRightViewController:(UIViewController *)rightViewController animated:(BOOL)animated; // Front view controller, can be nil on initialization but must be supplied by the time the view is loaded -@property (nonatomic) UIViewController *frontViewController; +@property (nonatomic, weak) UIViewController *frontViewController; - (void)setFrontViewController:(UIViewController *)frontViewController animated:(BOOL)animated; // Sets the frontViewController using a default set of chained animations consisting on moving the diff --git a/VoidLink/ViewControllers/SWRevealViewController.m b/VoidLink/ViewControllers/SWRevealViewController.m index d0ad012a..45f3935a 100755 --- a/VoidLink/ViewControllers/SWRevealViewController.m +++ b/VoidLink/ViewControllers/SWRevealViewController.m @@ -645,7 +645,7 @@ static CGFloat scaledValue( CGFloat v1, CGFloat min2, CGFloat max2, CGFloat min1 UIView* _separatorLine; NSMutableArray *_animationQueue; BOOL _userInteractionStore; - UIViewController *_primaryViewController; + __weak UIViewController *_primaryViewController; } diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index 383ed3ca..abbcde22 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -256,7 +256,11 @@ @property (strong, nonatomic) IBOutlet UIStackView *pencilTickIntervalStack; @property (strong, nonatomic) IBOutlet UISlider *pencilTickIntervalSlider; +@property (weak, nonatomic) IBOutlet UIStackView *doubleTapShortcutStack; +@property (weak, nonatomic) IBOutlet UISwitch *doubleTapShortcutSwitch; +@property (weak, nonatomic) IBOutlet UIStackView *pencilPausesNativeTouchStack; +@property (weak, nonatomic) IBOutlet UISwitch *pencilPausesNativeTouchSwitch; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 5fedac4c..17931a1a 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -393,6 +393,8 @@ BOOL isCustomResolution(int resolutionSelected) { [self.synthPhysicalInputSwitch setOn:oscProfile.synthesizePhysicalStick]; [self.pressureCurveSwitch setOn:oscProfile.pressureCurveEnabled]; + [self.doubleTapShortcutSwitch setOn:oscProfile.doubleTapShorcutEnabled]; + [self.pencilPausesNativeTouchSwitch setOn:oscProfile.pencilPausesNativeTouch]; } - (void)saveGameProfileConfigs{ @@ -414,6 +416,8 @@ BOOL isCustomResolution(int resolutionSelected) { && (int16_t)(oscProfile.physicalLeftStickMinOffset) == (int16_t)self.leftStickMinOffsetSlider.value && (int16_t)(oscProfile.physicalRightStickMinOffset) == (int16_t)self.rightStickMinOffsetSlider.value && oscProfile.pressureCurveEnabled == self.pressureCurveSwitch.isOn + && oscProfile.doubleTapShorcutEnabled == self.doubleTapShortcutSwitch.isOn + && oscProfile.pencilPausesNativeTouch == self.pencilPausesNativeTouchSwitch.isOn ); if(!configNotChanged){ @@ -431,6 +435,8 @@ BOOL isCustomResolution(int resolutionSelected) { oscProfile.physicalLeftStickMinOffset = (int16_t)self.leftStickMinOffsetSlider.value; oscProfile.physicalRightStickMinOffset = (int16_t)self.rightStickMinOffsetSlider.value; oscProfile.pressureCurveEnabled = self.pressureCurveSwitch.isOn; + oscProfile.doubleTapShorcutEnabled = self.doubleTapShortcutSwitch.isOn; + oscProfile.pencilPausesNativeTouch = self.pencilPausesNativeTouchSwitch.isOn; [oscProfileMan replaceSelectedProfileWith:oscProfile overwriteDefault:YES]; } } @@ -866,6 +872,8 @@ BOOL isCustomResolution(int resolutionSelected) { [self addSetting:self.pencilTickStack ofId:@"pencilTickStack" withInfoTag:YES withDynamicLabel:NO to:pencilSection]; [self addSetting:self.pencilTickIntervalStack ofId:@"pencilTickIntervalStack" withInfoTag:NO withDynamicLabel:YES to:pencilSection]; [self addSetting:self.pressureCurveStack ofId:@"pressureCurveStack" withInfoTag:NO withDynamicLabel:NO to:pencilSection]; + [self addSetting:self.doubleTapShortcutStack ofId:@"doubleTapShortcutStack" withInfoTag:YES withDynamicLabel:NO to:pencilSection]; + [self addSetting:self.pencilPausesNativeTouchStack ofId:@"pencilPausesNativeTouchStack" withInfoTag:NO withDynamicLabel:NO to:pencilSection]; [pencilSection addToParentStack:_parentStack]; } @@ -1472,6 +1480,15 @@ BOOL isCustomResolution(int resolutionSelected) { tipText = [LocalizationHelper localizedStringForKey:@"audioConfigStackTip"]; showOnlineDocAction = false; } + + if([sender.superview.accessibilityIdentifier isEqualToString: @"doubleTapShortcutStack"]){ + oscProfile = [oscProfileMan getSelectedProfile]; + tipText = [LocalizationHelper localizedStringForKey:@"doubleTapShortcutStackTip" + , [oscProfile.brushShortcut isEqualToString:@""] ? [LocalizationHelper localizedStringForKey:@"Null"] : oscProfile.brushShortcut + , [oscProfile.eraserShortcut isEqualToString:@""] ? [LocalizationHelper localizedStringForKey:@"Null"] : oscProfile.eraserShortcut + ]; + showOnlineDocAction = false; + } UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Tips"] message:tipText preferredStyle:UIAlertControllerStyleAlert]; @@ -2369,7 +2386,7 @@ BOOL isCustomResolution(int resolutionSelected) { [self mapGyroToChanged:self.mapGyroToSelector]; } else{ - MotionHandler* motionHandler = [MotionHandler sharedInstance]; + MotionHandler* motionHandler = [MotionHandler sharedWithProfile:nil]; [motionHandler calibrateGyroBiasWithDuration:5 completion:^{ Settings* currentSettings = [self->dataMan retrieveSettings]; currentSettings.gyroBiasX = [NSNumber numberWithDouble:motionHandler.gyroBiasX]; @@ -3355,6 +3372,8 @@ BOOL isCustomResolution(int resolutionSelected) { dispatch_async(dispatch_get_main_queue(), ^{ self.pencilTickSelector.selectedSegmentIndex = PencilTickDisabled; [self pencilTickModeChanged:self.pencilTickSelector]; + [self.doubleTapShortcutSwitch setOn:false]; + [self.pencilPausesNativeTouchSwitch setOn:false]; }); } @@ -3371,6 +3390,29 @@ BOOL isCustomResolution(int resolutionSelected) { } } +- (void)doubleTapShortcutSwitchFlipped:(UISwitch* )sender{ + if(sender.isOn && !settingsViewJustLoaded){ + if (@available(iOS 15.0, *)) { + [IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) { + if(info.valid) [PencilHandler enterDoubleTapShortcutsIn:self]; + else { + [IAPManager inAppPurchaseActionWithViewController:self product:AddOnProductPencilProPack]; + } + }]; + } + } +} + +- (void)pencilPausesNativeTouchSwitchFlipped:(UISwitch* )sender{ + if(sender.isOn && !settingsViewJustLoaded){ + if (@available(iOS 15.0, *)) { + [IAPManager checkPurchaseInfo:AddOnProductPencilProPack completion:^(PurchaseInfo* info) { + if(!info.valid) [IAPManager inAppPurchaseActionWithViewController:self product:AddOnProductPencilProPack]; + }]; + } + } +} + - (void)loadPencilSettings:(TemporarySettings*) tempSettings{ if([self isIPad]){ self.pencilTickSelector.selectedSegmentIndex = tempSettings.pencilTickMode.intValue; @@ -3381,8 +3423,9 @@ BOOL isCustomResolution(int resolutionSelected) { [self.pencilTickIntervalSlider addTarget:self action:@selector(pencilTickIntervalSliderMoved:) forControlEvents:UIControlEventValueChanged]; [self pencilTickIntervalSliderMoved:self.pencilTickIntervalSlider]; - [self.pressureCurveSwitch setOn:true]; [self.pressureCurveSwitch addTarget:self action:@selector(pressureCurveSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + [self.doubleTapShortcutSwitch addTarget:self action:@selector(doubleTapShortcutSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; + [self.pencilPausesNativeTouchSwitch addTarget:self action:@selector(pencilPausesNativeTouchSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; } } diff --git a/VoidLink/ViewControllers/StreamFrameViewController.h b/VoidLink/ViewControllers/StreamFrameViewController.h index 0e6c5ebb..c0fd08a5 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.h +++ b/VoidLink/ViewControllers/StreamFrameViewController.h @@ -34,8 +34,9 @@ @property (nonatomic) StreamConfiguration* streamConfig; @property (nonatomic, strong) AVPictureInPictureController *pipController API_AVAILABLE(ios(9.0)); @property (nonatomic, strong) AVPictureInPictureControllerContentSource *pipContentSource API_AVAILABLE(ios(15.0)); // Needed for iOS 15+ layer-based PiP -@property (nonatomic, assign) MainFrameViewController *mainFrameViewcontroller; +@property (nonatomic, weak) MainFrameViewController *mainFrameViewcontroller; @property (nonatomic, assign) bool micStreamInitialized; +@property (nonatomic, assign) bool viewJustLoaded; @property (nonatomic, strong) MetalViewController *metalViewController; @property (nonatomic, strong) ImGuiRenderer *imguiView; diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index 6a9dd00a..d12d5d22 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -26,6 +26,7 @@ #import "VoidLink-Swift.h" #import "OSCProfilesManager.h" #import "ThemeManager.h" +#import "NativeTouchPointer.h" #include #include @@ -61,7 +62,6 @@ StreamView *_streamView; UIScrollView *_scrollView; BOOL _userIsInteracting; - bool viewJustLoaded; bool viewIsBeingResized; bool previousOnScreenWidgetEnabled; CGSize _keyboardSize; @@ -378,7 +378,7 @@ Connection.muteInBackground = _settings.muteInBackground; - if(!viewJustLoaded) [_controllerSupport updateControllerSupport:self.streamConfig delegate:self]; + if(!_viewJustLoaded) [_controllerSupport updateControllerSupport:self.streamConfig delegate:self]; // reload controllerSupport obj, this is mandatory for OSC reload,especially when the stream view is launched without OSC [_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig streamFrameTopLayerView:self.view]; //reinitiate setupStreamView process. // we got self.view passed to streamView class as the topLayerView, will be useful in many cases @@ -387,7 +387,8 @@ bool onScreenWidgetSwitched = previousOnScreenWidgetEnabled != [_streamView isOnScreenWidgetEnabled]; bool needReload = onScreenWidgetSwitched && !previousOnScreenWidgetEnabled; - if(viewJustLoaded||reloadOnscreenWidgets||needReload) [_streamView reloadOnScreenWidgetViews]; //reload keyboard buttons here. the keyboard widget view will be added to the streamframe view instead streamview, the highest layer, which saves a lot of reengineering + [_streamView reloadOnScreenWidgetViews:_viewJustLoaded||reloadOnscreenWidgets||needReload]; //reload keyboard buttons here. the keyboard widget view will be added to the streamframe view instead streamview, the highest layer, which saves a lot of reengineering + if(onScreenWidgetSwitched && previousOnScreenWidgetEnabled) [_streamView clearOnScreenWidgets]; previousOnScreenWidgetEnabled = [_streamView isOnScreenWidgetEnabled]; @@ -444,13 +445,10 @@ [self startDisplayLink]; } - _motionHandler = [MotionHandler sharedInstance]; + _motionHandler = [MotionHandler sharedWithProfile: nil]; _motionHandler.gyroBiasX = _settings.gyroBiasX.doubleValue; _motionHandler.gyroBiasY = _settings.gyroBiasY.doubleValue; _motionHandler.gyroBiasZ = _settings.gyroBiasZ.doubleValue; - - _streamView.onScreenControls.instanceReceiverDelegate = _motionHandler; - [_streamView.onScreenControls sendInstance]; TouchPadGestureHandler.enablePinch = _settings.enablePinch; TouchPadGestureHandler.ctrlDownForPinch = _settings.ctrlDownForPinch; @@ -474,7 +472,7 @@ - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - viewJustLoaded = false; + _viewJustLoaded = false; _deviceWindow = self.view.window; previousOnScreenWidgetEnabled = [_streamView isOnScreenWidgetEnabled]; if (@available(iOS 13.0, *)) { @@ -499,6 +497,11 @@ } self->_streamView.originalFrame = self->_streamView.frame; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSLog(@"pausing..."); + nil; + }); // check to see if external screen is connected/disconnected @@ -583,7 +586,7 @@ - (void)viewDidLoad { - viewJustLoaded = true; + _viewJustLoaded = true; viewIsBeingResized = false; [super viewDidLoad]; @@ -768,7 +771,7 @@ [self presentViewController:_layoutOnScreenControlsVC animated:YES completion:nil]; } -- (void)switchWidgetProfile{ +- (void)openWidgetProfileTableWithPickProfile:(BOOL)pickProfile{ [_streamView saveRelocatedWidgetViews]; _streamView.widgetToolOpened = true; [self->_streamView disableOnScreenControls]; @@ -777,7 +780,7 @@ _layoutOnScreenControlsVC.toolbarStackView.hidden = true; _layoutOnScreenControlsVC.toolbarRootView.hidden = true; [self presentViewController:_layoutOnScreenControlsVC animated:NO completion:^{ - [self->_layoutOnScreenControlsVC presentProfilesTableView]; + [self->_layoutOnScreenControlsVC presentProfilesTableViewWithPickProfile:pickProfile]; }]; } @@ -799,8 +802,8 @@ [self->_streamView disableOnScreenControls]; // add this to get realtime back menu working. [self->_streamView reloadOnScreenControlsWith:(ControllerSupport*)_controllerSupport andConfig:(StreamConfiguration*)_streamConfig]; - [self->_streamView showOnScreenControls]; - [self->_streamView reloadOnScreenWidgetViews]; //update keyboard buttons here + // [self->_streamView reloadLegacyWidgets]; + [self->_streamView reloadOnScreenWidgetViews:true]; //update keyboard buttons here } - (void)setUserInteractionEnabledForStreamView:(bool)enabled{ @@ -817,7 +820,8 @@ - (void)willMoveToParentViewController:(UIViewController *)parent { // Only cleanup when we're being destroyed if (parent == nil) { - //NSLog(@"gyro cleanup, count: %ld", _controller.count); + _streamView = nil; + [_streamView cleanUp]; [_controllerSupport cleanup]; [UIApplication sharedApplication].idleTimerDisabled = NO; @@ -832,7 +836,11 @@ self.metalViewController = nil; NSLog(@"Metal renderer stopped and cleaned up."); } + [NativeTouchPointer cleanUpContext]; [[NSNotificationCenter defaultCenter] removeObserver:self]; + for(UIView* view in self.view.subviews){ + [view removeFromSuperview]; + } } } @@ -936,7 +944,7 @@ - (void) returnToMainFrame { [_streamView clearOnScreenWidgets]; if(micHandler) [micHandler clean]; - PencilHandler.sharedInstance = nil; + PencilHandler.shared = nil; // Reset display mode back to default [self updatePreferredDisplayMode:NO]; @@ -1188,7 +1196,7 @@ [self.view sendSubviewToBack:self->_streamView]; } - [self->_streamView showOnScreenControls]; + // [self->_streamView showOnScreenControls]; [self->_controllerSupport connectionEstablished]; @@ -1609,6 +1617,28 @@ [_streamView disablePencilHover]; } +- (void)setAllowSingleTouchEnabled:(BOOL)enabled{ + [_streamView setAllowSingleTouchEnabled:enabled]; +} + +- (void)replaceBrushWithShortcut:(NSString *)shortcut{ + if(PencilHandler.shared){ + [PencilHandler.shared replaceBrushWith:shortcut]; + } +} + +- (void)replaceEraserWithShortcut:(NSString *)shortcut{ + if(PencilHandler.shared){ + [PencilHandler.shared replaceEraserWith:shortcut]; + } +} + +- (void)presentPressureCurveVC{ + PressureCurveViewController* pressureCurveVC = [[PressureCurveViewController alloc] init]; + pressureCurveVC.modalPresentationStyle = UIModalPresentationOverFullScreen; + [self presentViewController:pressureCurveVC animated:YES completion:nil]; +} + #if !TARGET_OS_TV // Require a confirmation when streaming to activate a system gesture - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { @@ -1645,7 +1675,8 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - + [self->_streamView saveRelocatedWidgetViews]; + if (_isRestoringFromPiP) { Log(LOG_I, @"View size changed during PiP restore, skipping redundant reconfiguration."); return; @@ -1668,6 +1699,7 @@ - (void)dealloc { [self stopDisplayLink]; + NSLog(@"dealloc StreamFrameViewController %f", CACurrentMediaTime()); } - (void)setupDisplayLink { @@ -1698,5 +1730,4 @@ }); } - @end diff --git a/VoidLink/ViewControllers/ToolboxViewController.swift b/VoidLink/ViewControllers/ToolboxViewController.swift index 21150123..4ba11daf 100644 --- a/VoidLink/ViewControllers/ToolboxViewController.swift +++ b/VoidLink/ViewControllers/ToolboxViewController.swift @@ -10,7 +10,7 @@ import UIKit @objc protocol ToolboxSpecialEntryDelegate: NSObjectProtocol { @objc optional func openWidgetLayoutTool() - @objc optional func switchWidgetProfile() + @objc optional func openWidgetProfileTable(pickProfile: Bool) @objc optional func bringUpSoftKeyboard() @objc optional func enterPip() @objc optional func toggleStatsOverlay() @@ -18,7 +18,7 @@ import UIKit } -@objc public class ToolboxViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { +@objc public class ToolboxViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIGestureRecognizerDelegate { @objc weak var specialEntryDelegate: ToolboxSpecialEntryDelegate? public let tableView = UITableView() @@ -30,6 +30,8 @@ import UIKit private let viewBackgroundColor = UIColor(white: 0.2, alpha: 0.8); private let highlightColor = UIColor(white: 0.55, alpha: 0.8); private let titleLabel = UILabel() + private var contentView = UIView() + @objc public var specialEntries : NSMutableArray = ["widgetSwitchTool", "widgetLayoutTool", "bringUpSoftKeyboard", "enterPip", "toggleStatsOverlay", "disconnectAndQuitApp"] private let specialEntryAliasDic : [String:String] = [ "widgetSwitchTool":SwiftLocalizationHelper.localizedString(forKey: "[ Switch on-screen widget profile ]"), @@ -48,6 +50,7 @@ import UIKit } override public func viewDidLoad() { + super.viewDidLoad() setupViews() @@ -58,6 +61,11 @@ import UIKit // Register the cell class tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + + let tap = UITapGestureRecognizer(target: self, action: #selector(dismissSelf)) + tap.delegate = self + view.addGestureRecognizer(tap) + //pass self to the CommandManager CommandManager.shared.viewController = self } @@ -75,21 +83,21 @@ import UIKit //setupConstraints() } - private func setupViews() { - + contentView = UIView(frame: self.view.frame) + // Set corner radius - view.layer.cornerRadius = 20 // Adjust the corner radius - view.layer.masksToBounds = true + contentView.layer.cornerRadius = 20 // Adjust the corner radius + contentView.layer.masksToBounds = true // Set up the title label titleLabel.text = SwiftLocalizationHelper.localizedString(forKey: "Toolbox") titleLabel.font = UIFont.boldSystemFont(ofSize: 20) // Adjust font size as needed titleLabel.textColor = UIColor.white // Adjust color as needed titleLabel.textAlignment = .center - view.addSubview(titleLabel) + contentView.addSubview(titleLabel) - view.backgroundColor = viewBackgroundColor + contentView.backgroundColor = viewBackgroundColor tableView.backgroundColor = .clear tableView.rowHeight = isIPhone() ? 47 : 60 tableView.separatorColor = .white.withAlphaComponent(0.33) @@ -107,12 +115,14 @@ import UIKit pinButton.titleLabel?.font = UIFont.systemFont(ofSize: 20) // Add subviews - view.addSubview(tableView) - view.addSubview(addButton) - view.addSubview(deleteButton) - view.addSubview(editButton) - view.addSubview(exitButton) - view.addSubview(pinButton) + contentView.addSubview(tableView) + contentView.addSubview(addButton) + contentView.addSubview(deleteButton) + contentView.addSubview(editButton) + contentView.addSubview(exitButton) + contentView.addSubview(pinButton) + + self.view.addSubview(contentView) // Setup button targets addButton.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside) @@ -134,14 +144,10 @@ import UIKit } @objc public func setupConstraints() { - - guard view.superview != nil else { - return - } NSLayoutConstraint.deactivate(view.constraints) - view.translatesAutoresizingMaskIntoConstraints = false + contentView.translatesAutoresizingMaskIntoConstraints = false tableView.translatesAutoresizingMaskIntoConstraints = false addButton.translatesAutoresizingMaskIntoConstraints = false @@ -154,47 +160,47 @@ import UIKit NSLayoutConstraint.activate([ // Set the width and height of the view - view.widthAnchor.constraint(equalTo: view.superview!.widthAnchor, multiplier: isIPhone() ? 0.6 : 0.52), - view.heightAnchor.constraint(equalTo: view.superview!.heightAnchor, multiplier: 0.93), + contentView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: isIPhone() ? 0.6 : 0.52), + contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.93), // Set the width and height of the view //view.leadingAnchor.constraint(equalTo: view.superview!.leadingAnchor, constant: 60), //view.trailingAnchor.constraint(equalTo: view.superview!.trailingAnchor, constant: -60), // Center the view horizontally and vertically - view.centerXAnchor.constraint(equalTo: view.superview!.centerXAnchor), - view.centerYAnchor.constraint(equalTo: view.superview!.centerYAnchor), + contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor), // ViewTitle constrains - titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 13.5), - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), - titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), + titleLabel.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 13.5), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), // TableView constraints - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -60), + tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), + tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), + tableView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 50), + tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -60), // ExitButton constraints - exitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), - exitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + exitButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 25), + exitButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), // EditButton constraints editButton.leadingAnchor.constraint(equalTo: exitButton.trailingAnchor, constant: 25), - editButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + editButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), // AddButton constraints - addButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -25), - addButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + addButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -25), + addButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), // DeleteButton constraints deleteButton.trailingAnchor.constraint(equalTo: addButton.leadingAnchor, constant: -25), - deleteButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + deleteButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), // DeleteButton constraints pinButton.trailingAnchor.constraint(equalTo: deleteButton.leadingAnchor, constant: -25), - pinButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10), + pinButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), ]) } @@ -357,8 +363,12 @@ import UIKit else { let command = CommandManager.shared.getAllCommands()[indexPath.row-specialEntries.count] sendKeyboardCommand(command) + if !viewPinned { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.dismiss(animated: false, completion: nil) + } + } // dimiss the view in sending mode & the view is not pinned } - if !viewPinned { dismiss(animated: false, completion: nil) } // dimiss the view in sending mode & the view is not pinned } else { let specialEntrySelected = indexPath.row < specialEntries.count @@ -372,30 +382,42 @@ import UIKit } private func handleSpecialEntries(index:Int){ - usleep(1000*100) - dismiss(animated: true, completion: nil) - switch specialEntries[index] as? String { - case "widgetLayoutTool": - specialEntryDelegate?.openWidgetLayoutTool!() - case "widgetSwitchTool": - specialEntryDelegate?.switchWidgetProfile?() - case "bringUpSoftKeyboard": - specialEntryDelegate?.bringUpSoftKeyboard?() - case "enterPip": - specialEntryDelegate?.enterPip?() - case "toggleStatsOverlay": - specialEntryDelegate?.toggleStatsOverlay?() - case "disconnectAndQuitApp": - specialEntryDelegate?.disconnectAndQuitApp?() - default: break + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.dismiss(animated: false, completion: nil) + switch self.specialEntries[index] as? String { + case "widgetLayoutTool": + self.specialEntryDelegate?.openWidgetLayoutTool!() + case "widgetSwitchTool": + self.specialEntryDelegate?.openWidgetProfileTable?(pickProfile: false) + case "bringUpSoftKeyboard": + self.specialEntryDelegate?.bringUpSoftKeyboard?() + case "enterPip": + self.specialEntryDelegate?.enterPip?() + case "toggleStatsOverlay": + self.specialEntryDelegate?.toggleStatsOverlay?() + case "disconnectAndQuitApp": + self.specialEntryDelegate?.disconnectAndQuitApp?() + default: break + } } } private func sendKeyboardCommand(_ cmd: RemoteCommand) { print("Sending key-value") - let keyboardCmdStrings = CommandManager.shared.extractKeyStringsFromComboCommand(from: cmd.cmdString) + let keyboardCmdStrings = CommandManager.shared.extractKeyStrings(from: cmd.cmdString) CommandManager.shared.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings!) } + + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldReceive touch: UITouch + ) -> Bool { + return touch.view == view + } + + @objc private func dismissSelf(){ + dismiss(animated: false, completion: nil) + } } diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 4d7d1958..c55917b0 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -3031,6 +3031,36 @@ } } }, + "hZw-pC-Qxm.text" : { + "comment" : "Class = \"UILabel\"; text = \"Pencil Pauses Native Touch\"; ObjectID = \"hZw-pC-Qxm\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Pencil Pauses Native Touch" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用笔时暂停多点触控" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用筆時暫停多點觸控" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用筆時暫停多點觸控" + } + } + } + }, "iel-xl-xd9.text" : { "comment" : "Class = \"UILabel\"; text = \"Profile:\"; ObjectID = \"iel-xl-xd9\";", "extractionState" : "extracted_with_value", @@ -4009,6 +4039,18 @@ } } }, + "MyP-Aq-htW.text" : { + "comment" : "Class = \"UILabel\"; text = \" Reveal mode \"; ObjectID = \"MyP-Aq-htW\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : " Reveal mode " + } + } + } + }, "MzN-ZR-qyM.segmentTitles[0]" : { "comment" : "Class = \"UISegmentedControl\"; MzN-ZR-qyM.segmentTitles[0] = \"Auto\"; ObjectID = \"MzN-ZR-qyM\";", "extractionState" : "extracted_with_value", @@ -4705,6 +4747,30 @@ } } }, + "Qr8-Df-7Zu.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; Qr8-Df-7Zu.segmentTitles[0] = \"coexist\"; ObjectID = \"Qr8-Df-7Zu\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "coexist" + } + } + } + }, + "Qr8-Df-7Zu.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; Qr8-Df-7Zu.segmentTitles[1] = \"exclusive\"; ObjectID = \"Qr8-Df-7Zu\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "exclusive" + } + } + } + }, "Qs9-iS-YoW.text" : { "comment" : "Class = \"UILabel\"; text = \"Divider Position\"; ObjectID = \"Qs9-iS-YoW\";", "extractionState" : "extracted_with_value", @@ -4981,6 +5047,30 @@ } } }, + "sch-0e-nfa.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; sch-0e-nfa.segmentTitles[0] = \"display\"; ObjectID = \"sch-0e-nfa\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "display" + } + } + } + }, + "sch-0e-nfa.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; sch-0e-nfa.segmentTitles[1] = \"hide\"; ObjectID = \"sch-0e-nfa\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "hide" + } + } + } + }, "shy-hG-9wY.text" : { "comment" : "Class = \"UILabel\"; text = \"Remember Menu Fold/Expand\"; ObjectID = \"shy-hG-9wY\";", "extractionState" : "extracted_with_value", @@ -5185,6 +5275,36 @@ } } }, + "v5f-j3-pEc.text" : { + "comment" : "Class = \"UILabel\"; text = \"Double Tap Shorcut\"; ObjectID = \"v5f-j3-pEc\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Double Tap Shorcut" + } + }, + "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", @@ -5389,6 +5509,18 @@ } } }, + "WLg-7a-5wu.text" : { + "comment" : "Class = \"UILabel\"; text = \"Collected widgets \"; ObjectID = \"WLg-7a-5wu\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Collected widgets " + } + } + } + }, "x9i-JN-FSn.text" : { "comment" : "Class = \"UILabel\"; text = \"Roll factor\"; ObjectID = \"x9i-JN-FSn\";", "extractionState" : "extracted_with_value",