- feat: global controller navigation

- shorten initial host detection
This commit is contained in:
True Zhuanjia
2026-07-26 16:07:03 +08:00
parent 6460036661
commit 9a634da3ed
66 changed files with 6572 additions and 1659 deletions
+8 -6
View File
@@ -39,6 +39,8 @@
2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; }; 2A263D4F2FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; };
2A263D502FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; }; 2A263D502FF3BA2800154DEA /* PublicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */; };
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */; }; 2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */; };
2A7113FF2F5A6C65001D852D /* UIAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FE2F5A6C65001D852D /* UIAppView.swift */; };
2A7114012F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */; };
2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; }; 2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
2AAA4E842F7666EE001E20DC /* LocalizationHelperExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */; }; 2AAA4E842F7666EE001E20DC /* LocalizationHelperExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* LocalizationHelperExtension.swift */; };
2AAA4E872F985F12001E20DC /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; }; 2AAA4E872F985F12001E20DC /* LocalizationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FD0B502C31338D006B5BE5 /* LocalizationHelper.m */; };
@@ -156,7 +158,6 @@
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494F19FF2174002D2A60 /* SettingsViewController.m */; }; FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494F19FF2174002D2A60 /* SettingsViewController.m */; };
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */; }; FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */; };
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD349611A0089F6002D2A60 /* DataManager.m */; }; FBD349621A0089F6002D2A60 /* DataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD349611A0089F6002D2A60 /* DataManager.m */; };
FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E519F82297001C18A8 /* UIAppView.m */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -214,6 +215,8 @@
2A263D492FF3B3F800154DEA /* PublicExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicExtensions.swift; sourceTree = "<group>"; }; 2A263D492FF3B3F800154DEA /* PublicExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicExtensions.swift; sourceTree = "<group>"; };
2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUtils.swift; sourceTree = "<group>"; }; 2A263D4E2FF3BA2800154DEA /* PublicUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUtils.swift; sourceTree = "<group>"; };
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; }; 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
2A7113FE2F5A6C65001D852D /* UIAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAppView.swift; sourceTree = "<group>"; };
2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GamepadNavigationIllustrationHud.swift; sourceTree = "<group>"; };
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = "<group>"; }; 2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = "<group>"; };
2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackExtension.swift; sourceTree = "<group>"; }; 2AC1EAFA2F922602004EDA80 /* UIStackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackExtension.swift; sourceTree = "<group>"; };
3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesTableViewController.swift; sourceTree = "<group>"; }; 3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesTableViewController.swift; sourceTree = "<group>"; };
@@ -422,8 +425,6 @@
FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SWRevealViewController.m; sourceTree = "<group>"; }; FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SWRevealViewController.m; sourceTree = "<group>"; };
FBD349601A0089F6002D2A60 /* DataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DataManager.h; path = Database/DataManager.h; sourceTree = "<group>"; }; FBD349601A0089F6002D2A60 /* DataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DataManager.h; path = Database/DataManager.h; sourceTree = "<group>"; };
FBD349611A0089F6002D2A60 /* DataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DataManager.m; path = Database/DataManager.m; sourceTree = "<group>"; }; FBD349611A0089F6002D2A60 /* DataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DataManager.m; path = Database/DataManager.m; sourceTree = "<group>"; };
FBDE86E419F82297001C18A8 /* UIAppView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIAppView.h; sourceTree = "<group>"; };
FBDE86E519F82297001C18A8 /* UIAppView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIAppView.m; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -699,8 +700,8 @@
848F45692DDCD12500581E36 /* HostCardView.swift */, 848F45692DDCD12500581E36 /* HostCardView.swift */,
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */, 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */,
847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */, 847D2FDA2DDA015F00937A62 /* MenuSectionView.swift */,
FBDE86E419F82297001C18A8 /* UIAppView.h */, 2A7113FE2F5A6C65001D852D /* UIAppView.swift */,
FBDE86E519F82297001C18A8 /* UIAppView.m */, 2A7114002F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift */,
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */, 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */,
1888C1902E24C2C800108730 /* AboutView.swift */, 1888C1902E24C2C800108730 /* AboutView.swift */,
1888C1912E24C33100108730 /* AboutViewController.swift */, 1888C1912E24C33100108730 /* AboutViewController.swift */,
@@ -1154,7 +1155,8 @@
FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */, FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */,
FB9AFD401A7E127D00872C98 /* AppListResponse.m in Sources */, FB9AFD401A7E127D00872C98 /* AppListResponse.m in Sources */,
FB89463419F646E200339C8A /* Utils.m in Sources */, FB89463419F646E200339C8A /* Utils.m in Sources */,
FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */, 2A7113FF2F5A6C65001D852D /* UIAppView.swift in Sources */,
2A7114012F5A6C65001D852D /* GamepadNavigationIllustrationHud.swift in Sources */,
FB89462F19F646E200339C8A /* Connection.m in Sources */, FB89462F19F646E200339C8A /* Connection.m in Sources */,
FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */, FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */,
843E93BE2C41528700B1651F /* OrientationHelper.swift in Sources */, 843E93BE2C41528700B1651F /* OrientationHelper.swift in Sources */,
+28 -1
View File
@@ -10,7 +10,26 @@
import SwiftUI import SwiftUI
@available(iOS 13.0, *) @available(iOS 13.0, *)
@objc class AboutViewController: UIViewController { @objc class AboutViewController: UIViewController, ControllerUINavigationDelegate {
func getNavigationElements() -> [ControllerNavigationElement] {
var elements: [ControllerNavigationElement] = []
elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .x, action: "ok"))
return elements
}
func navigateByController(forward: Bool) {}
func navigateByController(downward: Bool) {}
func persistControllerNavigationHighlight() {}
func restoreControllerNavigationHighlight() {}
func restoreControllerNavigationHighlightAfterSettingsModeSwitch() {}
func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) {}
func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) {
if pressed, navigation.action == "ok" {
self.dismiss(animated: true)
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@@ -32,6 +51,14 @@ import SwiftUI
modalPresentationStyle = .formSheet modalPresentationStyle = .formSheet
} }
override func viewDidAppear(_ animated: Bool) {
ControllerNavigator.setUINavigationDelegate(self)
}
override func viewDidDisappear(_ animated: Bool) {
ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: self)
}
} }
func isIPhone() -> Bool { func isIPhone() -> Bool {
+10
View File
@@ -76,6 +76,7 @@ import UIKit
@objc public static var actionCancelled:Bool = false @objc public static var actionCancelled:Bool = false
@objc public static var alertController:UIAlertController = UIAlertController() @objc public static var alertController:UIAlertController = UIAlertController()
@objc public static var cancelButtonString:String = "Cancel" @objc public static var cancelButtonString:String = "Cancel"
@objc static var completion: (() -> Void)? = nil
@objc public static var autoCompletion:Bool = false @objc public static var autoCompletion:Bool = false
@objc public static var isCountingDown:Bool = false @objc public static var isCountingDown:Bool = false
@@ -93,6 +94,8 @@ import UIKit
let originalMessage = message let originalMessage = message
var isAlertDismissed = false var isAlertDismissed = false
AlertControllerUtil.completion = completion
guard let viewController = viewController else {return} guard let viewController = viewController else {return}
alertController = UIAlertController( alertController = UIAlertController(
@@ -148,16 +151,19 @@ import UIKit
isCountingDown = false isCountingDown = false
isAlertDismissed = true isAlertDismissed = true
timer.cancel() timer.cancel()
if autoCompletion { if autoCompletion {
alertController.message = originalMessage alertController.message = originalMessage
applyLeftAlignment(title: title, message: originalMessage) applyLeftAlignment(title: title, message: originalMessage)
autoCompletion = false autoCompletion = false
completion?() completion?()
AlertControllerUtil.completion = nil
alertController.dismiss(animated: false) alertController.dismiss(animated: false)
return return
} }
if PublicUtils.isRunningOnMacAsiPadApp && buttonTitle != "" { if PublicUtils.isRunningOnMacAsiPadApp && buttonTitle != "" {
AlertControllerUtil.completion = nil
alertController.dismiss(animated: false) { alertController.dismiss(animated: false) {
showAlert( showAlert(
in: viewController, in: viewController,
@@ -176,6 +182,10 @@ import UIKit
confirmAction.isEnabled = true confirmAction.isEnabled = true
alertController.message = originalMessage alertController.message = originalMessage
applyLeftAlignment(title: title, message: originalMessage) applyLeftAlignment(title: title, message: originalMessage)
if #available(iOS 13.0, *) {
ControllerNavigator.setUINavigationDelegate(alertController)
}
} else { } else {
if !autoCompletion { if !autoCompletion {
let countdownMessage = makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds) let countdownMessage = makeCountdownMessage(baseMessage: originalMessage, remainingSeconds: remainingSeconds)
+122 -93
View File
@@ -71,27 +71,32 @@
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="zpX-lo-MLA" userLabel="Resolutionstak"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="zpX-lo-MLA" userLabel="Resolutionstak">
<rect key="frame" x="16" y="57" width="380" height="92.5"/> <rect key="frame" x="16" y="57" width="380" height="92.5"/>
<subviews> <subviews>
<label verifyAmbiguity="off" opaque="NO" clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Resolution " lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pYZ-GR-EwO"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Iv4-iA-dkv" userLabel="ResolutionSelectorStack">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="380" height="59.5"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="17"/> <subviews>
<nil key="textColor"/> <label verifyAmbiguity="off" opaque="NO" clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Resolution " lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pYZ-GR-EwO">
<nil key="highlightedColor"/> <rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<size key="shadowOffset" width="0.0" height="0.0"/> <fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="17"/>
</label> <nil key="textColor"/>
<segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="5" translatesAutoresizingMaskIntoConstraints="NO" id="ckc-Dm-8ex"> <nil key="highlightedColor"/>
<rect key="frame" x="0.0" y="25.5" width="380" height="35"/> <size key="shadowOffset" width="0.0" height="0.0"/>
<segments> </label>
<segment title="360p" enabled="NO"/> <segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="5" translatesAutoresizingMaskIntoConstraints="NO" id="ckc-Dm-8ex">
<segment title="720p"/> <rect key="frame" x="0.0" y="25.5" width="380" height="35"/>
<segment title="1080p"/> <segments>
<segment title="4K"/> <segment title="360p" enabled="NO"/>
<segment title="Safe Area"/> <segment title="720p"/>
<segment title="FullScr/Window"/> <segment title="1080p"/>
<segment title="Custom"/> <segment title="4K"/>
</segments> <segment title="Safe Area"/>
<color key="tintColor" systemColor="linkColor"/> <segment title="FullScr/Window"/>
<color key="selectedSegmentTintColor" systemColor="linkColor"/> <segment title="Custom"/>
</segmentedControl> </segments>
<color key="tintColor" systemColor="linkColor"/>
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
</segmentedControl>
</subviews>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="djj-8b-2pB" userLabel="ResolutionInnerStack2"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="djj-8b-2pB" userLabel="ResolutionInnerStack2">
<rect key="frame" x="0.0" y="64.5" width="380" height="28"/> <rect key="frame" x="0.0" y="64.5" width="380" height="28"/>
<subviews> <subviews>
@@ -972,16 +977,16 @@
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="0n9-vQ-WC2"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="0n9-vQ-WC2">
<rect key="frame" x="852" y="850" width="300" height="31"/> <rect key="frame" x="852" y="850" width="310.5" height="31"/>
<subviews> <subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Controller → Mouse" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="P1g-rW-Mgp" userLabel="uiLabel"> <label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Controller Navigation" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="P1g-rW-Mgp" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="150" height="31"/> <rect key="frame" x="0.0" y="0.0" width="160.5" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ect-CM-odK"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ect-CM-odK">
<rect key="frame" x="170" y="0.0" width="132" height="31"/> <rect key="frame" x="180.5" y="0.0" width="132" height="31"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="130" id="G04-14-Q74"/> <constraint firstAttribute="width" constant="130" id="G04-14-Q74"/>
</constraints> </constraints>
@@ -991,44 +996,24 @@
<constraint firstAttribute="height" constant="31" id="7kp-OR-e5f"/> <constraint firstAttribute="height" constant="31" id="7kp-OR-e5f"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="lBq-CW-xFb"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="xXr-dB-QWZ">
<rect key="frame" x="852" y="899" width="380" height="55.5"/> <rect key="frame" x="852" y="911" width="380" height="55.5"/>
<subviews> <subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Pointer Velocity" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xnd-C4-8N9" userLabel="uiLabel"> <label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Streaming Radial Menu Delay" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9d1-RN-lqW" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/> <rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="60" translatesAutoresizingMaskIntoConstraints="NO" id="w6S-nD-AXU"> <slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="3" translatesAutoresizingMaskIntoConstraints="NO" id="qZX-tG-Pwf">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/> <rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/> <color key="tintColor" systemColor="linkColor"/>
</slider> </slider>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="width" constant="380" id="4aR-eF-inq"/> <constraint firstAttribute="height" constant="55.5" id="3OW-fn-a4x"/>
<constraint firstAttribute="height" constant="55.5" id="ScL-Y2-bDH"/> <constraint firstAttribute="width" constant="380" id="Vyh-Uu-Qdl"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="5gN-F1-wWR">
<rect key="frame" x="852" y="974" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Curve Exponent" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6Cy-aH-DXp" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" minValue="1" maxValue="5" translatesAutoresizingMaskIntoConstraints="NO" id="ciD-6k-n6o">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="380" id="h9D-G7-Nuh"/>
<constraint firstAttribute="height" constant="55.5" id="kLV-xF-5SV"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="v6i-HA-XRx"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="v6i-HA-XRx">
@@ -1056,46 +1041,6 @@
<constraint firstAttribute="width" constant="380" id="qOk-ee-a8A"/> <constraint firstAttribute="width" constant="380" id="qOk-ee-a8A"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="B5K-lN-ZSc">
<rect key="frame" x="852" y="1055" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Left Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3XV-jL-NYF" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="KtJ-qH-KJX">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="380" id="EJU-Jy-UYb"/>
<constraint firstAttribute="height" constant="55.5" id="z8Y-An-DwI"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="yl0-WY-ZqV">
<rect key="frame" x="852" y="1129" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Right Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f3L-bA-RW6" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="qt5-Hq-mIa">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="380" id="Skp-ya-Odl"/>
<constraint firstAttribute="height" constant="55.5" id="f4J-WQ-LBI"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="A31-WC-As7"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="A31-WC-As7">
<rect key="frame" x="441" y="1363" width="280" height="31"/> <rect key="frame" x="441" y="1363" width="280" height="31"/>
<subviews> <subviews>
@@ -2198,6 +2143,86 @@
<constraint firstAttribute="height" constant="31" id="EpQ-lj-XBq"/> <constraint firstAttribute="height" constant="31" id="EpQ-lj-XBq"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="lBq-CW-xFb">
<rect key="frame" x="852" y="987" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Controller Mouse Velocity" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xnd-C4-8N9" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="60" translatesAutoresizingMaskIntoConstraints="NO" id="w6S-nD-AXU">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="380" id="4aR-eF-inq"/>
<constraint firstAttribute="height" constant="55.5" id="ScL-Y2-bDH"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="5gN-F1-wWR">
<rect key="frame" x="852" y="1062" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Curve Exponent" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6Cy-aH-DXp" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" minValue="1" maxValue="5" translatesAutoresizingMaskIntoConstraints="NO" id="ciD-6k-n6o">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="380" id="h9D-G7-Nuh"/>
<constraint firstAttribute="height" constant="55.5" id="kLV-xF-5SV"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="B5K-lN-ZSc">
<rect key="frame" x="852" y="1143" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Left Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3XV-jL-NYF" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="KtJ-qH-KJX">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="380" id="EJU-Jy-UYb"/>
<constraint firstAttribute="height" constant="55.5" id="z8Y-An-DwI"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="yl0-WY-ZqV">
<rect key="frame" x="852" y="1217" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Right Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="f3L-bA-RW6" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="16.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="qt5-Hq-mIa">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="380" id="Skp-ya-Odl"/>
<constraint firstAttribute="height" constant="55.5" id="f4J-WQ-LBI"/>
</constraints>
</stackView>
</subviews> </subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view> </view>
@@ -2230,10 +2255,11 @@
<outlet property="controllerMouseExpoStack" destination="5gN-F1-wWR" id="hgt-xH-dts"/> <outlet property="controllerMouseExpoStack" destination="5gN-F1-wWR" id="hgt-xH-dts"/>
<outlet property="controllerMouseVelocitySlider" destination="w6S-nD-AXU" id="rws-D6-zMT"/> <outlet property="controllerMouseVelocitySlider" destination="w6S-nD-AXU" id="rws-D6-zMT"/>
<outlet property="controllerMouseVelocityStack" destination="lBq-CW-xFb" id="zOW-iC-wBn"/> <outlet property="controllerMouseVelocityStack" destination="lBq-CW-xFb" id="zOW-iC-wBn"/>
<outlet property="controllerToMouseStack" destination="0n9-vQ-WC2" id="DhB-o2-VWn"/> <outlet property="controllerNavigationStack" destination="0n9-vQ-WC2" id="DhB-o2-VWn"/>
<outlet property="controllerToMouseSwitch" destination="Ect-CM-odK" id="SCB-GH-0hd"/> <outlet property="controllerNavigationSwitch" destination="Ect-CM-odK" id="SCB-GH-0hd"/>
<outlet property="ctrlDownForPinchStack" destination="4Jb-ey-ca7" id="Ou2-Qz-VO6"/> <outlet property="ctrlDownForPinchStack" destination="4Jb-ey-ca7" id="Ou2-Qz-VO6"/>
<outlet property="ctrlDownForPinchSwitch" destination="zfr-Wn-dB4" id="GXR-zn-daI"/> <outlet property="ctrlDownForPinchSwitch" destination="zfr-Wn-dB4" id="GXR-zn-daI"/>
<outlet property="customResolutionStack" destination="djj-8b-2pB" id="MQH-Fc-cH9"/>
<outlet property="customResolutionSwitch" destination="E5w-kK-dOo" id="hrt-v2-8dY"/> <outlet property="customResolutionSwitch" destination="E5w-kK-dOo" id="hrt-v2-8dY"/>
<outlet property="delayLeftClickStack" destination="Xp7-7O-Ndt" id="CwB-Wy-beh"/> <outlet property="delayLeftClickStack" destination="Xp7-7O-Ndt" id="CwB-Wy-beh"/>
<outlet property="delayLeftClickSwitch" destination="Haf-rg-zLB" id="FaA-3p-JDw"/> <outlet property="delayLeftClickSwitch" destination="Haf-rg-zLB" id="FaA-3p-JDw"/>
@@ -2340,6 +2366,7 @@
<outlet property="renderingBackendSelector" destination="53H-ST-OtY" id="jhu-Pw-Phg"/> <outlet property="renderingBackendSelector" destination="53H-ST-OtY" id="jhu-Pw-Phg"/>
<outlet property="renderingBackendStack" destination="6hG-dw-YSr" id="qqz-lA-EOE"/> <outlet property="renderingBackendStack" destination="6hG-dw-YSr" id="qqz-lA-EOE"/>
<outlet property="resolutionSelector" destination="ckc-Dm-8ex" id="rl6-rx-wd3"/> <outlet property="resolutionSelector" destination="ckc-Dm-8ex" id="rl6-rx-wd3"/>
<outlet property="resolutionSelectorStack" destination="Iv4-iA-dkv" id="vlg-5X-iMR"/>
<outlet property="resolutionStack" destination="zpX-lo-MLA" id="9Hu-I9-hjH"/> <outlet property="resolutionStack" destination="zpX-lo-MLA" id="9Hu-I9-hjH"/>
<outlet property="reverseHoldButtonStack" destination="mB8-aJ-7qO" id="3bo-br-Tcb"/> <outlet property="reverseHoldButtonStack" destination="mB8-aJ-7qO" id="3bo-br-Tcb"/>
<outlet property="reverseHoldButtonSwitch" destination="00w-mO-Ehj" id="Ptz-f8-nTx"/> <outlet property="reverseHoldButtonSwitch" destination="00w-mO-Ehj" id="Ptz-f8-nTx"/>
@@ -2378,6 +2405,8 @@
<outlet property="squeezeShortcutSwitch" destination="VCg-Es-PQV" id="kgj-FT-fcZ"/> <outlet property="squeezeShortcutSwitch" destination="VCg-Es-PQV" id="kgj-FT-fcZ"/>
<outlet property="statsOverlaySelector" destination="NLH-qN-qCo" id="EF8-JF-O3S"/> <outlet property="statsOverlaySelector" destination="NLH-qN-qCo" id="EF8-JF-O3S"/>
<outlet property="statsOverlayStack" destination="UcP-0b-nJD" id="UdI-NS-O6k"/> <outlet property="statsOverlayStack" destination="UcP-0b-nJD" id="UdI-NS-O6k"/>
<outlet property="streamingRadialMenuDelaySlider" destination="qZX-tG-Pwf" id="V5Z-kn-irq"/>
<outlet property="streamingRadialMenuDelayStack" destination="xXr-dB-QWZ" id="I9M-3Y-HGg"/>
<outlet property="swapAbxyStack" destination="2rP-Ui-wVU" id="gUR-Vz-ORP"/> <outlet property="swapAbxyStack" destination="2rP-Ui-wVU" id="gUR-Vz-ORP"/>
<outlet property="swapAbxySwitch" destination="hv7-6b-rez" id="iKX-qk-Dep"/> <outlet property="swapAbxySwitch" destination="hv7-6b-rez" id="iKX-qk-Dep"/>
<outlet property="swapYawAndRollStack" destination="EsN-yO-7OW" id="Ncq-Y1-lku"/> <outlet property="swapYawAndRollStack" destination="EsN-yO-7OW" id="Ncq-Y1-lku"/>
+126 -98
View File
@@ -1600,27 +1600,32 @@
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="sbK-BT-6rK" userLabel="Resolutionstak"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="sbK-BT-6rK" userLabel="Resolutionstak">
<rect key="frame" x="16" y="57" width="380" height="92.5"/> <rect key="frame" x="16" y="57" width="380" height="92.5"/>
<subviews> <subviews>
<label verifyAmbiguity="off" opaque="NO" clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Resolution " lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T0w-xM-ze7"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="xpB-c8-tbW">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="380" height="59.5"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="17"/> <subviews>
<nil key="textColor"/> <label verifyAmbiguity="off" opaque="NO" clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Resolution " lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T0w-xM-ze7">
<nil key="highlightedColor"/> <rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<size key="shadowOffset" width="0.0" height="0.0"/> <fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="17"/>
</label> <nil key="textColor"/>
<segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="5" translatesAutoresizingMaskIntoConstraints="NO" id="7Sl-WI-zyo"> <nil key="highlightedColor"/>
<rect key="frame" x="0.0" y="25.5" width="380" height="35"/> <size key="shadowOffset" width="0.0" height="0.0"/>
<segments> </label>
<segment title="360p" enabled="NO"/> <segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="5" translatesAutoresizingMaskIntoConstraints="NO" id="7Sl-WI-zyo">
<segment title="720p"/> <rect key="frame" x="0.0" y="25.5" width="380" height="35"/>
<segment title="1080p"/> <segments>
<segment title="4K"/> <segment title="360p" enabled="NO"/>
<segment title="Safe Area"/> <segment title="720p"/>
<segment title="FullScr/Window"/> <segment title="1080p"/>
<segment title="Custom"/> <segment title="4K"/>
</segments> <segment title="Safe Area"/>
<color key="tintColor" systemColor="linkColor"/> <segment title="FullScr/Window"/>
<color key="selectedSegmentTintColor" systemColor="linkColor"/> <segment title="Custom"/>
</segmentedControl> </segments>
<color key="tintColor" systemColor="linkColor"/>
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
</segmentedControl>
</subviews>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="GWt-Kz-lZ9" userLabel="ResolutionInnerStack2"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="GWt-Kz-lZ9" userLabel="ResolutionInnerStack2">
<rect key="frame" x="0.0" y="64.5" width="380" height="28"/> <rect key="frame" x="0.0" y="64.5" width="380" height="28"/>
<subviews> <subviews>
@@ -3087,16 +3092,16 @@
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="xyO-r6-abA"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="xyO-r6-abA">
<rect key="frame" x="456" y="3007" width="300" height="31"/> <rect key="frame" x="456" y="3007" width="310.5" height="31"/>
<subviews> <subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Controller → Mouse" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="F0O-Ht-fW5" userLabel="uiLabel"> <label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Controller Navigation" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="F0O-Ht-fW5" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="150" height="31"/> <rect key="frame" x="0.0" y="0.0" width="160.5" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OAq-o7-H0d"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OAq-o7-H0d">
<rect key="frame" x="170" y="0.0" width="132" height="31"/> <rect key="frame" x="180.5" y="0.0" width="132" height="31"/>
<constraints> <constraints>
<constraint firstAttribute="width" constant="130" id="LDl-c7-M09"/> <constraint firstAttribute="width" constant="130" id="LDl-c7-M09"/>
</constraints> </constraints>
@@ -3106,44 +3111,24 @@
<constraint firstAttribute="height" constant="31" id="svn-9D-BlC"/> <constraint firstAttribute="height" constant="31" id="svn-9D-BlC"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="QuI-NM-qCU"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="ySL-A5-fcn">
<rect key="frame" x="459" y="3137" width="380" height="55.5"/> <rect key="frame" x="456" y="3068" width="380" height="55.5"/>
<subviews> <subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Curve Exponent" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AEN-EU-8LZ" userLabel="uiLabel"> <label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Streaming Radial Menu Delay" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bPX-Kf-kdP" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" minValue="1" maxValue="5" translatesAutoresizingMaskIntoConstraints="NO" id="scG-Rx-agb"> <slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="3" translatesAutoresizingMaskIntoConstraints="NO" id="Tgh-nL-w5K">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/> <rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/> <color key="tintColor" systemColor="linkColor"/>
</slider> </slider>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="height" constant="55.5" id="28R-1J-Wwa"/> <constraint firstAttribute="width" constant="380" id="Tb8-iE-aB1"/>
<constraint firstAttribute="width" constant="380" id="auC-uE-Gzq"/> <constraint firstAttribute="height" constant="55.5" id="sbd-1K-4IW"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="jxy-UY-jFk">
<rect key="frame" x="459" y="3062" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Pointer Velocity" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="H3s-ud-yqh" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="60" translatesAutoresizingMaskIntoConstraints="NO" id="CkB-cG-N8r">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="55.5" id="YOw-2j-9UH"/>
<constraint firstAttribute="width" constant="380" id="mG1-Vg-QwM"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="5fm-3b-ixg"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="5fm-3b-ixg">
@@ -3171,46 +3156,6 @@
<constraint firstAttribute="height" constant="56.5" id="MNC-Cn-lnb"/> <constraint firstAttribute="height" constant="56.5" id="MNC-Cn-lnb"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Twt-JJ-ihg">
<rect key="frame" x="459" y="3218" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Left Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="whI-oR-V8o" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="vOQ-zv-JOQ">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="55.5" id="Gid-x3-eh7"/>
<constraint firstAttribute="width" constant="380" id="qs5-21-iSz"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="aQs-Ww-sPZ">
<rect key="frame" x="459" y="3292" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Right Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="q6s-4H-y9n" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="nOn-5b-blZ">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="55.5" id="sam-oW-HVg"/>
<constraint firstAttribute="width" constant="380" id="tSR-LD-qKK"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="HCc-SK-NBk"> <stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="HCc-SK-NBk">
<rect key="frame" x="459" y="2252" width="280" height="31"/> <rect key="frame" x="459" y="2252" width="280" height="31"/>
<subviews> <subviews>
@@ -3555,6 +3500,86 @@
<constraint firstAttribute="height" constant="31" id="rXC-R1-f3Z"/> <constraint firstAttribute="height" constant="31" id="rXC-R1-f3Z"/>
</constraints> </constraints>
</stackView> </stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="QuI-NM-qCU">
<rect key="frame" x="456" y="3213" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Curve Exponent" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AEN-EU-8LZ" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" minValue="1" maxValue="5" translatesAutoresizingMaskIntoConstraints="NO" id="scG-Rx-agb">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="55.5" id="28R-1J-Wwa"/>
<constraint firstAttribute="width" constant="380" id="auC-uE-Gzq"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="jxy-UY-jFk">
<rect key="frame" x="456" y="3138" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Controller Mouse Velocity" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="H3s-ud-yqh" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="60" translatesAutoresizingMaskIntoConstraints="NO" id="CkB-cG-N8r">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="55.5" id="YOw-2j-9UH"/>
<constraint firstAttribute="width" constant="380" id="mG1-Vg-QwM"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Twt-JJ-ihg">
<rect key="frame" x="456" y="3294" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Left Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="whI-oR-V8o" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="vOQ-zv-JOQ">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="55.5" id="Gid-x3-eh7"/>
<constraint firstAttribute="width" constant="380" id="qs5-21-iSz"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" axis="vertical" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="aQs-Ww-sPZ">
<rect key="frame" x="456" y="3368" width="380" height="55.5"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Right Stick Minimum Offset" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="q6s-4H-y9n" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="380" height="20.5"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="16383" translatesAutoresizingMaskIntoConstraints="NO" id="nOn-5b-blZ">
<rect key="frame" x="0.0" y="25.5" width="380" height="30"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="55.5" id="sam-oW-HVg"/>
<constraint firstAttribute="width" constant="380" id="tSR-LD-qKK"/>
</constraints>
</stackView>
</subviews> </subviews>
<viewLayoutGuide key="safeArea" id="0sf-mG-PCk"/> <viewLayoutGuide key="safeArea" id="0sf-mG-PCk"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -3588,8 +3613,8 @@
<outlet property="controllerMouseExpoStack" destination="QuI-NM-qCU" id="Kmp-Cc-bWI"/> <outlet property="controllerMouseExpoStack" destination="QuI-NM-qCU" id="Kmp-Cc-bWI"/>
<outlet property="controllerMouseVelocitySlider" destination="CkB-cG-N8r" id="AMa-cl-2Wz"/> <outlet property="controllerMouseVelocitySlider" destination="CkB-cG-N8r" id="AMa-cl-2Wz"/>
<outlet property="controllerMouseVelocityStack" destination="jxy-UY-jFk" id="os6-z6-A7h"/> <outlet property="controllerMouseVelocityStack" destination="jxy-UY-jFk" id="os6-z6-A7h"/>
<outlet property="controllerToMouseStack" destination="xyO-r6-abA" id="vWd-Hf-o4C"/> <outlet property="controllerNavigationStack" destination="xyO-r6-abA" id="vWd-Hf-o4C"/>
<outlet property="controllerToMouseSwitch" destination="OAq-o7-H0d" id="xk7-q8-bjc"/> <outlet property="controllerNavigationSwitch" destination="OAq-o7-H0d" id="xk7-q8-bjc"/>
<outlet property="ctrlDownForPinchStack" destination="eYW-eD-LTg" id="eQS-WT-74l"/> <outlet property="ctrlDownForPinchStack" destination="eYW-eD-LTg" id="eQS-WT-74l"/>
<outlet property="ctrlDownForPinchSwitch" destination="q46-JG-pX4" id="jjp-BU-9RP"/> <outlet property="ctrlDownForPinchSwitch" destination="q46-JG-pX4" id="jjp-BU-9RP"/>
<outlet property="customResolutionSwitch" destination="kRO-HO-zKO" id="ZUP-Yd-QDY"/> <outlet property="customResolutionSwitch" destination="kRO-HO-zKO" id="ZUP-Yd-QDY"/>
@@ -3683,6 +3708,7 @@
<outlet property="renderingBackendSelector" destination="53H-ST-OtY" id="jhu-Pw-Ph2"/> <outlet property="renderingBackendSelector" destination="53H-ST-OtY" id="jhu-Pw-Ph2"/>
<outlet property="renderingBackendStack" destination="6hG-dw-YSr" id="qqz-lA-EO2"/> <outlet property="renderingBackendStack" destination="6hG-dw-YSr" id="qqz-lA-EO2"/>
<outlet property="resolutionSelector" destination="7Sl-WI-zyo" id="YMv-Xy-dF9"/> <outlet property="resolutionSelector" destination="7Sl-WI-zyo" id="YMv-Xy-dF9"/>
<outlet property="resolutionSelectorStack" destination="xpB-c8-tbW" id="esq-e9-gSA"/>
<outlet property="resolutionStack" destination="sbK-BT-6rK" id="hEi-h6-te9"/> <outlet property="resolutionStack" destination="sbK-BT-6rK" id="hEi-h6-te9"/>
<outlet property="reverseHoldButtonStack" destination="97k-tp-tqF" id="C7z-79-7jb"/> <outlet property="reverseHoldButtonStack" destination="97k-tp-tqF" id="C7z-79-7jb"/>
<outlet property="reverseHoldButtonSwitch" destination="Aey-h4-DoE" id="rFw-PX-RCC"/> <outlet property="reverseHoldButtonSwitch" destination="Aey-h4-DoE" id="rFw-PX-RCC"/>
@@ -3719,6 +3745,8 @@
<outlet property="softKeyboardToolbarSwitch" destination="c4W-2p-R3A" id="gf6-pq-L8o"/> <outlet property="softKeyboardToolbarSwitch" destination="c4W-2p-R3A" id="gf6-pq-L8o"/>
<outlet property="statsOverlaySelector" destination="Bda-3L-Wtg" id="9kj-wY-VjF"/> <outlet property="statsOverlaySelector" destination="Bda-3L-Wtg" id="9kj-wY-VjF"/>
<outlet property="statsOverlayStack" destination="dby-k3-aiH" id="1Ha-Y3-su4"/> <outlet property="statsOverlayStack" destination="dby-k3-aiH" id="1Ha-Y3-su4"/>
<outlet property="streamingRadialMenuDelaySlider" destination="Tgh-nL-w5K" id="91f-Q5-IIl"/>
<outlet property="streamingRadialMenuDelayStack" destination="ySL-A5-fcn" id="HrE-C7-y0g"/>
<outlet property="swapAbxyStack" destination="reE-vr-Mdk" id="y3v-ac-LHy"/> <outlet property="swapAbxyStack" destination="reE-vr-Mdk" id="y3v-ac-LHy"/>
<outlet property="swapAbxySwitch" destination="T5q-1k-zB3" id="153-6O-0Pv"/> <outlet property="swapAbxySwitch" destination="T5q-1k-zB3" id="153-6O-0Pv"/>
<outlet property="swapYawAndRollStack" destination="bOa-JN-z7e" id="Xrg-wN-OIu"/> <outlet property="swapYawAndRollStack" destination="bOa-JN-z7e" id="Xrg-wN-OIu"/>
@@ -3768,19 +3796,19 @@
<image name="trash.fill" catalog="system" width="117" height="128"/> <image name="trash.fill" catalog="system" width="117" height="128"/>
<image name="tray.full.fill" catalog="system" width="128" height="88"/> <image name="tray.full.fill" catalog="system" width="128" height="88"/>
<systemColor name="linkColor"> <systemColor name="linkColor">
<color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
<systemColor name="systemBrownColor"> <systemColor name="systemBrownColor">
<color red="0.63529411759999999" green="0.51764705879999995" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.67450980392156867" green="0.49803921568627452" blue="0.36862745098039218" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
<systemColor name="systemGray2Color"> <systemColor name="systemGray2Color">
<color red="0.68235294120000001" green="0.68235294120000001" blue="0.69803921570000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.68235294117647061" green="0.68235294117647061" blue="0.69803921568627447" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
<systemColor name="systemGray3Color"> <systemColor name="systemGray3Color">
<color red="0.78039215689999997" green="0.78039215689999997" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.7803921568627451" green="0.7803921568627451" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
<systemColor name="systemTealColor"> <systemColor name="systemTealColor">
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0" green="0.76470588235294112" blue="0.81568627450980391" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
</resources> </resources>
</document> </document>
+2 -1
View File
@@ -146,7 +146,7 @@ typedef NS_ENUM(NSInteger, PencilTickMode) {
leftClickDelayMs:(CGFloat)leftClickDelayMs leftClickDelayMs:(CGFloat)leftClickDelayMs
settingsMenuOffset:(CGFloat)settingsMenuOffset settingsMenuOffset:(CGFloat)settingsMenuOffset
passthroughGestures:(BOOL)passthroughGestures passthroughGestures:(BOOL)passthroughGestures
mapControllerToMouse:(BOOL)mapControllerToMouse enableControllerNavigation:(BOOL)mapControllerToMouse
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
controllerMouseExpo:(CGFloat)controllerMouseExpo controllerMouseExpo:(CGFloat)controllerMouseExpo
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
@@ -155,6 +155,7 @@ typedef NS_ENUM(NSInteger, PencilTickMode) {
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
softKeyboardHeight:(CGFloat)softKeyboardHeight softKeyboardHeight:(CGFloat)softKeyboardHeight
globeAsEscape:(BOOL)globeAsEscape globeAsEscape:(BOOL)globeAsEscape
streamingRadialMenuDelay:(CGFloat)streamingRadialMenuDelay
backgroundSessionTimer:(NSInteger)backgroundSessionTimer; backgroundSessionTimer:(NSInteger)backgroundSessionTimer;
- (NSArray*) getHosts; - (NSArray*) getHosts;
+4 -2
View File
@@ -123,7 +123,7 @@
leftClickDelayMs:(CGFloat)leftClickDelayMs leftClickDelayMs:(CGFloat)leftClickDelayMs
settingsMenuOffset:(CGFloat)settingsMenuOffset settingsMenuOffset:(CGFloat)settingsMenuOffset
passthroughGestures:(BOOL)passthroughGestures passthroughGestures:(BOOL)passthroughGestures
mapControllerToMouse:(BOOL)mapControllerToMouse enableControllerNavigation:(BOOL)enableControllerNavigation
controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity controllerMousePointerVelocity:(CGFloat)controllerMousePointerVelocity
controllerMouseExpo:(CGFloat)controllerMouseExpo controllerMouseExpo:(CGFloat)controllerMouseExpo
controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode controllerGyroSwitchMode:(NSInteger)controllerGyroSwitchMode
@@ -132,6 +132,7 @@
sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround sdrPerformanceWorkaround:(BOOL)sdrPerformanceWorkaround
softKeyboardHeight:(CGFloat)softKeyboardHeight softKeyboardHeight:(CGFloat)softKeyboardHeight
globeAsEscape:(BOOL)globeAsEscape globeAsEscape:(BOOL)globeAsEscape
streamingRadialMenuDelay:(CGFloat)streamingRadialMenuDelay
backgroundSessionTimer:(NSInteger)backgroundSessionTimer{ backgroundSessionTimer:(NSInteger)backgroundSessionTimer{
__block Settings* settingsToSave = settings; __block Settings* settingsToSave = settings;
@@ -208,11 +209,12 @@
settingsToSave.leftClickDelayMs = [NSNumber numberWithFloat:leftClickDelayMs]; settingsToSave.leftClickDelayMs = [NSNumber numberWithFloat:leftClickDelayMs];
settingsToSave.settingsMenuOffset = [NSNumber numberWithFloat:settingsMenuOffset]; settingsToSave.settingsMenuOffset = [NSNumber numberWithFloat:settingsMenuOffset];
settingsToSave.passthroughGestures = passthroughGestures; settingsToSave.passthroughGestures = passthroughGestures;
settingsToSave.mapControllerToMouse = mapControllerToMouse; settingsToSave.enableControllerNavigation = enableControllerNavigation;
settingsToSave.controllerMousePointerVelocity = [NSNumber numberWithFloat:controllerMousePointerVelocity]; settingsToSave.controllerMousePointerVelocity = [NSNumber numberWithFloat:controllerMousePointerVelocity];
settingsToSave.controllerMouseExpo = [NSNumber numberWithFloat:controllerMouseExpo]; settingsToSave.controllerMouseExpo = [NSNumber numberWithFloat:controllerMouseExpo];
settingsToSave.softKeyboardHeight = softKeyboardHeight; settingsToSave.softKeyboardHeight = softKeyboardHeight;
settingsToSave.globeAsEscape = globeAsEscape; settingsToSave.globeAsEscape = globeAsEscape;
settingsToSave.streamingRadialMenuDelay = @(streamingRadialMenuDelay);
settingsToSave.rememberFoldState = rememberFoldState; settingsToSave.rememberFoldState = rememberFoldState;
[self saveData]; [self saveData];
}]; }];
+1
View File
@@ -24,6 +24,7 @@
@property (atomic, nullable, retain) NSString *localAddress; @property (atomic, nullable, retain) NSString *localAddress;
@property (atomic, nullable, retain) NSString *ipv6Address; @property (atomic, nullable, retain) NSString *ipv6Address;
@property (atomic, nullable, retain) NSString *mac; @property (atomic, nullable, retain) NSString *mac;
@property (atomic, nullable, retain) NSString *controllerNavigationHighlightedAppID;
@property (atomic) int serverCodecModeSupport; @property (atomic) int serverCodecModeSupport;
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
+2
View File
@@ -45,6 +45,7 @@
if(!(host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"])) self.mac = host.mac; // try to fix invalid mac happens in some cases if(!(host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"])) self.mac = host.mac; // try to fix invalid mac happens in some cases
self.name = host.name; self.name = host.name;
self.uuid = host.uuid; self.uuid = host.uuid;
self.controllerNavigationHighlightedAppID = host.controllerNavigationHighlightedAppID;
self.serverCodecModeSupport = host.serverCodecModeSupport; self.serverCodecModeSupport = host.serverCodecModeSupport;
self.serverCert = host.serverCert; self.serverCert = host.serverCert;
@@ -101,6 +102,7 @@
} }
parentHost.name = self.name; parentHost.name = self.name;
parentHost.uuid = self.uuid; parentHost.uuid = self.uuid;
parentHost.controllerNavigationHighlightedAppID = self.controllerNavigationHighlightedAppID;
parentHost.serverCodecModeSupport = self.serverCodecModeSupport; parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
parentHost.pairState = [NSNumber numberWithInt:self.pairState]; parentHost.pairState = [NSNumber numberWithInt:self.pairState];
} }
+4 -2
View File
@@ -107,10 +107,12 @@
@property (nonatomic, retain) NSNumber * controllerMouseLeftButton; @property (nonatomic, retain) NSNumber * controllerMouseLeftButton;
@property (nonatomic, retain) NSNumber * controllerMouseRightButton; @property (nonatomic, retain) NSNumber * controllerMouseRightButton;
@property (nonatomic, retain) NSNumber * controllerMouseStick; @property (nonatomic, retain) NSNumber * controllerMouseStick;
@property (nonatomic, retain) NSNumber * controllerMouseSwitch; @property (nonatomic, retain) NSNumber * localRadialMenuButton;
@property (nonatomic) BOOL mapControllerToMouse; @property (nonatomic, retain) NSNumber * streamingRadialMenuButton;
@property (nonatomic) BOOL enableControllerNavigation;
@property (nonatomic, retain) NSNumber * controllerMousePointerVelocity; @property (nonatomic, retain) NSNumber * controllerMousePointerVelocity;
@property (nonatomic, retain) NSNumber * controllerMouseExpo; @property (nonatomic, retain) NSNumber * controllerMouseExpo;
@property (nonatomic, retain) NSNumber * streamingRadialMenuDelay;
@property (nonatomic) BOOL globeAsEscape; @property (nonatomic) BOOL globeAsEscape;
// Pencil settings: // Pencil settings:
+4 -2
View File
@@ -161,13 +161,15 @@
self.ctrlDownForPinch = settings.ctrlDownForPinch; self.ctrlDownForPinch = settings.ctrlDownForPinch;
self.settingsMenuOffset = settings.settingsMenuOffset; self.settingsMenuOffset = settings.settingsMenuOffset;
self.passthroughGestures = settings.passthroughGestures; self.passthroughGestures = settings.passthroughGestures;
self.mapControllerToMouse = settings.mapControllerToMouse; self.enableControllerNavigation = settings.enableControllerNavigation;
self.controllerMouseLeftButton = settings.controllerMouseLeftButton; self.controllerMouseLeftButton = settings.controllerMouseLeftButton;
self.controllerMouseRightButton = settings.controllerMouseRightButton; self.controllerMouseRightButton = settings.controllerMouseRightButton;
self.controllerMouseSwitch = settings.controllerMouseSwitch; self.localRadialMenuButton = settings.localRadialMenuButton;
self.streamingRadialMenuButton = settings.streamingRadialMenuButton;
self.controllerMouseStick = settings.controllerMouseStick; self.controllerMouseStick = settings.controllerMouseStick;
self.controllerMousePointerVelocity = settings.controllerMousePointerVelocity; self.controllerMousePointerVelocity = settings.controllerMousePointerVelocity;
self.controllerMouseExpo = settings.controllerMouseExpo; self.controllerMouseExpo = settings.controllerMouseExpo;
self.streamingRadialMenuDelay = settings.streamingRadialMenuDelay;
self.globeAsEscape = settings.globeAsEscape; self.globeAsEscape = settings.globeAsEscape;
// Pencil settings: // Pencil settings:
@@ -0,0 +1,515 @@
//
// GamepadNavigationIllustrationHud.swift
// VoidLink
//
// Created by True Zhuanjia on 2026/7/14.
// Copyright 2026 True Zhuanjia @ Bilibili. All rights reserved.
//
import UIKit
@available(iOS 13.0, *)
@objcMembers
final class GamepadNavigationIllustrationHud: UIView {
private enum KeyContent {
case text(String)
case symbol(String)
}
private struct Hint {
let control: ControllerElement
let keyContent: KeyContent
let title: String
let isInAction: Bool
}
private let containerView = UIView()
private let contentStackView = UIStackView()
private var themeObserver: NSObjectProtocol?
private var windowConstraints: [NSLayoutConstraint] = []
private var trailingConstraint: NSLayoutConstraint?
private static let keyBorderViewTag = 4101
private static let keyContentViewTag = 4102
private static var currentHud: GamepadNavigationIllustrationHud?
private static var activeControls = Set<ControllerElement>()
private static var actionStateMinimumEndTimes: [ControllerElement: CFTimeInterval] = [:]
private static var pendingActionStateWorkItems: [ControllerElement: DispatchWorkItem] = [:]
private static var pendingClearHudWorkItem: DispatchWorkItem?
private static let minimumActionStateDuration: CFTimeInterval = 0.09
private static let hudScale: CGFloat = PublicUtils.isIPhone ? 0.74 : 1
private static let hudWidth: CGFloat = 235
private static let edgeMargin: CGFloat = PublicUtils.isIPhone ? 5 : 24
private var hints: [Hint] = [
// Hint(control: .leftStick, keyContent: .text("L"), title: "浏览", isInAction: false),
// Hint(control: .a, keyContent: .text("A"), title: "选择", isInAction: false)
]
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
observeThemeChanges()
updateTheme()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
observeThemeChanges()
updateTheme()
}
deinit {
NSLayoutConstraint.deactivate(windowConstraints)
containerView.removeFromSuperview()
if let themeObserver {
NotificationCenter.default.removeObserver(themeObserver)
}
}
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
if let window {
updateTrailingOffset(for: window)
}
}
@discardableResult
static func showInKeyWindow() -> GamepadNavigationIllustrationHud? {
guard let window = keyWindow(), ControllerUtil.primaryGCController != nil else {
return nil
}
let hud = currentHud ?? GamepadNavigationIllustrationHud()
currentHud = hud
hud.attach(to: window)
if let mainFrameVC = ControllerNavigator.radialMenuDelegate as? MainFrameViewController {
DispatchQueue.main.asyncAfter(deadline: .now()+3) {
let streamFrameVC = StreamFrameViewController.sharedInstance()
if ControllerNavigator.radialMenuView == nil, mainFrameVC.isStreaming(), !mainFrameVC.settingsExpandedInStreamView, streamFrameVC?.hasNoPresentedVC == true {
requestHudDetachKeepingMinimumActionDuration()
}
}
}
return hud
}
static func updateCurrentTheme() {
currentHud?.updateTheme()
}
@objc static func updateNavigationElements(_ elements: [ControllerNavigationElement], forceDisplay: Bool = false) {
if !forceDisplay {
guard ControllerNavigator.enabled, ControllerUtil.primaryGCController != nil else {return}
guard !ControllerNavigator.stickReleasedInRadialMenu else {return}
}
PublicUtils.runOnMain {
let hud = showInKeyWindow()
hud?.updateHints(with: elements)
}
}
@objc static func updateHud() {
let elements = ControllerNavigator.uiNavigationDelegate?.getNavigationElements() ?? []
/*
for element in elements {
print("element \(element.action)")
} */
updateNavigationElements(elements)
}
static func updateActionState(for control: ControllerElement, isInAction: Bool) {
PublicUtils.runOnMain {
if isInAction {
pendingClearHudWorkItem?.cancel()
pendingClearHudWorkItem = nil
pendingActionStateWorkItems[control]?.cancel()
pendingActionStateWorkItems[control] = nil
actionStateMinimumEndTimes[control] = CACurrentMediaTime() + minimumActionStateDuration
let didChange = activeControls.insert(control).inserted
guard didChange else { return }
currentHud?.refreshActionStates()
} else {
requestActionStateEnd(for: control)
}
}
}
static func resetActionStates() {
PublicUtils.runOnMain {
guard !activeControls.isEmpty else { return }
Array(activeControls).forEach { requestActionStateEnd(for: $0) }
}
}
static func clearHud() {
PublicUtils.runOnMain {
if ControllerNavigator.controllerMouseEnabled {
finishHudDetach()
return
}
requestHudDetachKeepingMinimumActionDuration()
}
}
func updateTheme() {
let isDark = ThemeManager.userInterfaceStyle() == .dark
backgroundColor = isDark
? UIColor(red: 28.0 / 255.0, green: 28.0 / 255.0, blue: 30.0 / 255.0, alpha: 0.62)
: UIColor.white.withAlphaComponent(0.6)
layer.borderColor = (isDark ? UIColor.white.withAlphaComponent(0.10) : UIColor.black.withAlphaComponent(0.06)).cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = isDark ? 0.34 : 0.12
layer.shadowRadius = isDark ? 18 : 16
layer.shadowOffset = CGSize(width: 0, height: 8)
for (index, row) in contentStackView.arrangedSubviews.enumerated() {
guard hints.indices.contains(index),
let rowStack = row as? UIStackView,
let keyView = rowStack.arrangedSubviews.first,
let titleLabel = rowStack.arrangedSubviews.last as? UILabel else {
continue
}
let isInAction = hints[index].isInAction
let keyBorderView = keyView.viewWithTag(Self.keyBorderViewTag) ?? keyView
let keyContentView = keyView.viewWithTag(Self.keyContentViewTag) ?? keyView
let keyBackgroundColor = isInAction
? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.2 : 0.13)
: ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.09 : 0.05)
keyView.backgroundColor = .clear
keyBorderView.layer.borderColor = ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.69 : 0.6).cgColor
keyContentView.backgroundColor = keyBackgroundColor
keyView.transform = .identity
for subview in keyContentView.subviews {
if let keyLabel = subview as? UILabel {
keyLabel.textColor = isInAction ? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 1 : 1) : ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.83 : 0.83)
} else if let imageView = subview as? UIImageView {
imageView.tintColor = isInAction ? ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 1 : 1) : ThemeManager.appPrimaryColor.withAlphaComponent(isDark ? 0.83 : 0.83)
}
}
titleLabel.textColor = ThemeManager.textColor.withAlphaComponent(isDark ? 0.69 : 0.6)
}
}
private func setupView() {
translatesAutoresizingMaskIntoConstraints = false
isUserInteractionEnabled = false
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.isUserInteractionEnabled = false
transform = CGAffineTransform(scaleX: Self.hudScale, y: Self.hudScale)
layer.cornerRadius = 18
layer.cornerCurve = .continuous
layer.borderWidth = 1
contentStackView.axis = .vertical
contentStackView.alignment = .leading
contentStackView.spacing = 6
contentStackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentStackView)
NSLayoutConstraint.activate([
contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
contentStackView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
contentStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)
])
reloadHintRows()
}
private func updateHints(with elements: [ControllerNavigationElement]) {
let nextHints = elements.compactMap { hint(for: $0) }
hints = nextHints.isEmpty ? [] : nextHints
guard !hints.isEmpty else {return}
reloadHintRows()
updateTheme()
isHidden = hints.isEmpty
}
private func refreshActionStates() {
hints = hints.map { hint in
Hint(
control: hint.control,
keyContent: hint.keyContent,
title: hint.title,
isInAction: Self.isControlActive(hint.control)
)
}
updateTheme()
}
private func reloadHintRows() {
contentStackView.arrangedSubviews.forEach { row in
contentStackView.removeArrangedSubview(row)
row.removeFromSuperview()
}
hints.forEach { contentStackView.addArrangedSubview(makeRow(for: $0)) }
}
private func hint(for navigationElement: ControllerNavigationElement) -> Hint? {
return Hint(
control: navigationElement.control,
keyContent: keyContent(for: navigationElement.control),
title: navigationElement.action.localized,
isInAction: navigationElement.isInAction || Self.isControlActive(navigationElement.control)
)
}
private func keyContent(for control: ControllerElement) -> KeyContent {
let symbol = control.symbol
return symbol.isEmpty ? .text(control.displayName) : .symbol(symbol)
}
private func attach(to window: UIWindow) {
if containerView.superview !== window {
NSLayoutConstraint.deactivate(windowConstraints)
windowConstraints.removeAll()
removeFromSuperview()
containerView.removeFromSuperview()
window.addSubview(containerView)
containerView.addSubview(self)
let trailingConstraint = containerView.trailingAnchor.constraint(equalTo: window.safeAreaLayoutGuide.trailingAnchor)
self.trailingConstraint = trailingConstraint
windowConstraints = [
containerView.widthAnchor.constraint(equalToConstant: Self.hudWidth * Self.hudScale),
containerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: Self.hudScale),
trailingConstraint,
containerView.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -Self.edgeMargin),
widthAnchor.constraint(equalToConstant: Self.hudWidth),
centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
]
NSLayoutConstraint.activate(windowConstraints)
}
updateTrailingOffset(for: window)
window.bringSubviewToFront(containerView)
}
private func detachFromWindow() {
NSLayoutConstraint.deactivate(windowConstraints)
windowConstraints.removeAll()
trailingConstraint = nil
removeFromSuperview()
containerView.removeFromSuperview()
}
private func updateTrailingOffset(for window: UIWindow) {
let horizontalMargin = Self.edgeMargin + (PublicUtils.isIPhone ? 0 : window.safeAreaInsets.bottom)
trailingConstraint?.constant = -horizontalMargin
}
private func makeRow(for hint: Hint) -> UIStackView {
let keyView = makeKeyView(for: hint.keyContent)
let titleLabel = UILabel()
titleLabel.text = hint.title
titleLabel.font = UIFont.roundedSystemFont(ofSize: 14, weight: .medium)
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.minimumScaleFactor = 0.2
titleLabel.numberOfLines = 1
titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
let rowStackView = UIStackView(arrangedSubviews: [keyView, titleLabel])
rowStackView.axis = .horizontal
rowStackView.alignment = .center
rowStackView.spacing = 8
return rowStackView
}
private func makeKeyView(for content: KeyContent) -> UIView {
let keyContainerView = UIView()
keyContainerView.translatesAutoresizingMaskIntoConstraints = false
keyContainerView.backgroundColor = .clear
NSLayoutConstraint.activate([
keyContainerView.widthAnchor.constraint(equalToConstant: 50),
keyContainerView.heightAnchor.constraint(equalToConstant: PublicUtils.isIPhone ? 38 : 42)
])
let borderView = UIView()
borderView.tag = Self.keyBorderViewTag
borderView.translatesAutoresizingMaskIntoConstraints = false
borderView.isUserInteractionEnabled = false
borderView.backgroundColor = .clear
borderView.layer.cornerRadius = 13
borderView.layer.cornerCurve = .continuous
borderView.layer.borderWidth = 1
borderView.layer.masksToBounds = true
keyContainerView.addSubview(borderView)
let keyView = UIView()
keyView.tag = Self.keyContentViewTag
keyView.translatesAutoresizingMaskIntoConstraints = false
keyView.layer.cornerRadius = 12
keyView.layer.cornerCurve = .continuous
keyView.layer.masksToBounds = true
keyContainerView.addSubview(keyView)
NSLayoutConstraint.activate([
borderView.leadingAnchor.constraint(equalTo: keyContainerView.leadingAnchor),
borderView.trailingAnchor.constraint(equalTo: keyContainerView.trailingAnchor),
borderView.topAnchor.constraint(equalTo: keyContainerView.topAnchor),
borderView.bottomAnchor.constraint(equalTo: keyContainerView.bottomAnchor),
keyView.leadingAnchor.constraint(equalTo: keyContainerView.leadingAnchor, constant: 1),
keyView.trailingAnchor.constraint(equalTo: keyContainerView.trailingAnchor, constant: -1),
keyView.topAnchor.constraint(equalTo: keyContainerView.topAnchor, constant: 1),
keyView.bottomAnchor.constraint(equalTo: keyContainerView.bottomAnchor, constant: -1)
])
switch content {
case .text(let text):
let keyLabel = InsetLabel()
keyLabel.contentInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.text = text
keyLabel.textAlignment = .center
keyLabel.font = UIFont.roundedSystemFont(ofSize: 12, weight: .medium)
keyLabel.adjustsFontSizeToFitWidth = true
keyLabel.minimumScaleFactor = 0.2
keyLabel.numberOfLines = 1
keyLabel.lineBreakMode = .byClipping
keyView.addSubview(keyLabel)
NSLayoutConstraint.activate([
keyLabel.leadingAnchor.constraint(equalTo: keyView.leadingAnchor),
keyLabel.trailingAnchor.constraint(equalTo: keyView.trailingAnchor),
keyLabel.topAnchor.constraint(equalTo: keyView.topAnchor),
keyLabel.bottomAnchor.constraint(equalTo: keyView.bottomAnchor)
])
case .symbol(let symbolName):
let imageView = UIImageView(image: UIImage(systemName: symbolName))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium)
keyView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: keyView.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: keyView.centerYAnchor),
imageView.widthAnchor.constraint(lessThanOrEqualTo: keyView.widthAnchor, multiplier: 0.58),
imageView.heightAnchor.constraint(lessThanOrEqualTo: keyView.heightAnchor, multiplier: 0.58)
])
}
return keyContainerView
}
private func observeThemeChanges() {
themeObserver = NotificationCenter.default.addObserver(
forName: Notification.Name(ThemeManager.ThemeDidChangeNotification),
object: nil,
queue: .main
) { [weak self] _ in
self?.updateTheme()
}
}
private static func keyWindow() -> UIWindow? {
let sceneWindows = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
return sceneWindows.first(where: { $0.isKeyWindow })
?? sceneWindows.first
?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })
?? UIApplication.shared.windows.first
}
private static func isControlActive(_ control: ControllerElement) -> Bool {
!relatedControls(for: control).isDisjoint(with: activeControls)
}
private static func requestActionStateEnd(for control: ControllerElement) {
pendingActionStateWorkItems[control]?.cancel()
let now = CACurrentMediaTime()
let remainingDuration = max((actionStateMinimumEndTimes[control] ?? now) - now, 0)
guard remainingDuration > 0 else {
finishActionStateEnd(for: control)
return
}
let workItem = DispatchWorkItem {
finishActionStateEnd(for: control)
}
pendingActionStateWorkItems[control] = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + remainingDuration, execute: workItem)
}
private static func finishActionStateEnd(for control: ControllerElement) {
pendingActionStateWorkItems[control]?.cancel()
pendingActionStateWorkItems[control] = nil
actionStateMinimumEndTimes[control] = nil
guard activeControls.remove(control) != nil else { return }
currentHud?.refreshActionStates()
}
private static func requestHudDetachKeepingMinimumActionDuration() {
pendingClearHudWorkItem?.cancel()
let remainingDuration = maximumRemainingActionStateDuration()
guard remainingDuration > 0 else {
finishHudDetach()
return
}
let workItem = DispatchWorkItem {
finishHudDetach()
}
pendingClearHudWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + remainingDuration, execute: workItem)
}
private static func finishHudDetach() {
pendingClearHudWorkItem?.cancel()
pendingClearHudWorkItem = nil
pendingActionStateWorkItems.values.forEach { $0.cancel() }
pendingActionStateWorkItems.removeAll()
actionStateMinimumEndTimes.removeAll()
activeControls.removeAll()
currentHud?.detachFromWindow()
currentHud = nil
}
private static func maximumRemainingActionStateDuration() -> CFTimeInterval {
let now = CACurrentMediaTime()
return activeControls.reduce(0) { partialResult, control in
max(partialResult, max((actionStateMinimumEndTimes[control] ?? now) - now, 0))
}
}
private static func relatedControls(for control: ControllerElement) -> Set<ControllerElement> {
switch control {
case .leftStick, .leftStickX, .leftStickY:
return [.leftStick, .leftStickX, .leftStickY]
case .rightStick, .rightStickX, .rightStickY:
return [.rightStick, .rightStickX, .rightStickY]
default:
return [control]
}
}
}
final class InsetLabel: UILabel {
var contentInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: contentInsets))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(
width: size.width + contentInsets.left + contentInsets.right,
height: size.height + contentInsets.top + contentInsets.bottom
)
}
}
+8 -2
View File
@@ -107,6 +107,7 @@ class HostCardView: UIView {
} }
@objc func appButtonTapped() { @objc func appButtonTapped() {
guard host?.pairState == pairStatePaired, host?.state == .online, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired { if longPressFired {
longPressFired = false longPressFired = false
return return
@@ -120,6 +121,7 @@ class HostCardView: UIView {
} }
@objc func launchButtonTapped() { @objc func launchButtonTapped() {
guard host?.pairState == pairStatePaired, host?.state == .online, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired { if longPressFired {
longPressFired = false longPressFired = false
return return
@@ -133,6 +135,7 @@ class HostCardView: UIView {
} }
@objc func wakeupButtonTapped() { @objc func wakeupButtonTapped() {
guard host?.state != .online, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired { if longPressFired {
longPressFired = false longPressFired = false
return return
@@ -146,6 +149,7 @@ class HostCardView: UIView {
} }
@objc func pairButtonTapped() { @objc func pairButtonTapped() {
guard !pairButton.isHidden, self.parentViewController?.hasNoPresentedVC == true else { return }
if longPressFired { if longPressFired {
longPressFired = false longPressFired = false
return return
@@ -442,7 +446,6 @@ class HostCardView: UIView {
} }
func tintAdjustmentModeDidChange() { func tintAdjustmentModeDidChange() {
NSLog("tintChanged........")
tintAdjustmentMode = .normal tintAdjustmentMode = .normal
} }
@@ -476,7 +479,10 @@ class HostCardView: UIView {
return return
} }
updateContents(for: host) DispatchQueue.main.asyncAfter(deadline: .now()+0.36) { [weak self] in
self?.updateContents(for: self?.host)
}
perform(#selector(updateLoop), with: self, afterDelay: Self.refreshCycle) perform(#selector(updateLoop), with: self, afterDelay: Self.refreshCycle)
} }
@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "circle.lefthalf.filled.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 26.5852 26.2402">
<g>
<rect height="26.2402" opacity="0" width="26.5852" x="0" y="0"/>
<path d="M13.1137 24.8338L13.1137 1.38328C6.44134 1.38328 1.38556 6.44134 1.38556 13.1137C1.38556 19.7861 6.44134 24.8338 13.1137 24.8338ZM13.1137 26.2275C20.3549 26.2275 26.2298 20.3526 26.2298 13.1137C26.2298 5.87488 20.3549 0 13.1137 0C5.87488 0 0 5.87488 0 13.1137C0 20.3526 5.87488 26.2275 13.1137 26.2275ZM13.1137 23.6443C7.29091 23.6443 2.58322 18.9366 2.58322 13.1137C2.58322 7.29091 7.29091 2.58322 13.1137 2.58322C18.9366 2.58322 23.6465 7.29091 23.6465 13.1137C23.6465 18.9366 18.9366 23.6443 13.1137 23.6443Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 940 B

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "controllerMouse.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 17.225 22.7356">
<g>
<rect height="22.7356" opacity="0" width="17.225" x="0" y="0"/>
<path d="M7.98991 16.743L14.7319 16.743C16.6897 16.743 17.5002 14.9583 16.3368 13.7776L3.19844 0.64154C1.94161-0.624432 0 0.0799009 0 1.90751L0 20.5374C0 22.1976 1.83421 22.8801 3.21901 21.5058Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 612 B

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "disableControllerMouse.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30.0974 26.0826">
<g>
<rect height="26.0826" opacity="0" width="30.0974" x="0" y="0"/>
<path d="M14.0091 18.5296L9.61512 22.9119C8.37098 24.156 6.72059 23.5467 6.72059 22.0486L6.72059 11.264ZM9.62782 2.61209L22.5136 15.5105C23.5673 16.5769 22.8436 18.1765 21.079 18.1765L20.638 18.1765L6.72059 4.29423L6.72059 3.75467C6.72059 2.09158 8.47254 1.46951 9.62782 2.61209Z" fill="white"/>
<path d="M1.75672 4.2117L21.9169 24.3084C22.2977 24.7019 22.9325 24.7019 23.3007 24.3084C23.6688 23.9275 23.6815 23.3181 23.3007 22.9246L3.15321 2.82791C2.77235 2.44705 2.15028 2.44705 1.75672 2.82791C1.38856 3.20877 1.38856 3.84354 1.75672 4.2117Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 966 B

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "macwindow.and.pointer.arrow.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 36.2556 34.0064">
<g>
<rect height="34.0064" opacity="0" width="36.2556" x="0" y="0"/>
<path d="M32.9778 10.6393L32.9778 21.2524L30.4905 18.7118L30.4905 10.7627C30.4905 8.79238 29.3426 7.64447 27.3723 7.64447L8.51517 7.64447C6.54486 7.64447 5.39694 8.79238 5.39694 10.7627L5.39694 23.2564C5.39694 25.229 6.54486 26.3665 8.51517 26.3665L24.569 26.3665L24.5407 28.4391C24.5386 28.5831 24.5481 28.722 24.5715 28.8538L8.38137 28.8538C4.91606 28.8538 2.9097 26.8578 2.9097 23.3798L2.9097 10.6393C2.9097 7.1613 4.91606 5.15722 8.38137 5.15722L27.7709 5.15722C31.057 5.15722 32.9778 7.1613 32.9778 10.6393ZM10.0795 11.0857C10.0795 11.7166 9.55111 12.2658 8.90161 12.2658C8.25212 12.2658 7.72146 11.7166 7.72146 11.0857C7.72146 10.4489 8.25212 9.89739 8.90161 9.89739C9.55111 9.89739 10.0795 10.4489 10.0795 11.0857ZM13.8213 11.0857C13.8213 11.7166 13.2929 12.2658 12.6434 12.2658C11.9917 12.2658 11.4633 11.7166 11.4633 11.0857C11.4633 10.4489 11.9917 9.89739 12.6434 9.89739C13.2929 9.89739 13.8213 10.4489 13.8213 11.0857ZM17.5631 11.0857C17.5631 11.7166 17.0324 12.2658 16.3852 12.2658C15.7335 12.2658 15.2051 11.7166 15.2051 11.0857C15.2051 10.4489 15.7335 9.89739 16.3852 9.89739C17.0324 9.89739 17.5631 10.4489 17.5631 11.0857Z" fill="white"/>
<path d="M32.5208 32.1175C33.1251 31.8661 33.4145 31.1857 33.1527 30.5836L31.2545 26.174L33.8561 26.0963C34.3956 26.0847 34.6538 25.5545 34.2699 25.1498L27.3886 18.1123C27.0219 17.7456 26.4847 17.9589 26.472 18.4685L26.3463 28.4749C26.3313 29.0318 26.9747 29.2161 27.3355 28.8462L29.1464 26.9975L30.9661 31.4878C31.2002 32.0852 31.9083 32.3747 32.5208 32.1175Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,24 @@
{
"images" : [
{
"filename" : "rectangle.portrait.and.arrow.right.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 34.2565 27.6836">
<g>
<rect height="27.6836" opacity="0" width="34.2565" x="0" y="0"/>
<path d="M23.7582 5.55752L23.7582 10.1349L20.9311 10.1349L20.9311 5.6619C20.9311 3.85035 19.9125 2.82713 18.0963 2.82713L7.33593 2.82713C5.52438 2.82713 4.49712 3.85035 4.49712 5.6619L4.49712 22.0171C4.49712 23.8373 5.52438 24.8518 7.33593 24.8518L18.0963 24.8518C19.9125 24.8518 20.9311 23.8373 20.9311 22.0171L20.9311 17.5359L23.7582 17.5359L23.7582 22.1301C23.7582 25.6945 21.7778 27.679 18.2134 27.679L7.22751 27.679C3.6631 27.679 1.66999 25.6945 1.66999 22.1301L1.66999 5.55752C1.66999 1.99311 3.6631 0 7.22751 0L18.2134 0C21.7778 0 23.7582 1.99311 23.7582 5.55752Z" fill="white"/>
<path d="M13.1283 13.8354C13.1283 14.5549 13.714 15.1331 14.4006 15.1331L27.0257 15.1331L30.8363 14.8838C31.4191 14.8423 31.8888 14.4142 31.8888 13.8354C31.8888 13.2527 31.4191 12.8245 30.8363 12.783L27.0257 12.5292L14.4006 12.5292C13.714 12.5292 13.1283 13.116 13.1283 13.8354ZM25.8536 9.43889C25.8536 9.7568 25.9875 10.0845 26.231 10.3067L28.0471 12.0034L30.4369 13.8354L28.0471 15.6588L26.231 17.3642C25.9875 17.5864 25.8536 17.9014 25.8536 18.2193C25.8536 18.8327 26.3152 19.3681 26.9649 19.3681C27.2961 19.3681 27.5396 19.2337 27.7704 19.0029L31.7792 14.8423C32.1139 14.5036 32.2351 14.1782 32.2351 13.8354C32.2351 13.4841 32.1139 13.1633 31.7792 12.8286L27.7704 8.65533C27.5396 8.42453 27.2961 8.30279 26.9649 8.30279C26.3152 8.30279 25.8536 8.81687 25.8536 9.43889Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -7,6 +7,7 @@
// //
#import "StreamView.h" #import "StreamView.h"
#import "TemporarySettings.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
File diff suppressed because it is too large Load Diff
+5
View File
@@ -26,6 +26,8 @@
@property (readonly) bool shallDisableGyroHotSwitch; @property (readonly) bool shallDisableGyroHotSwitch;
+(ControllerSupport*) sharedInstance;
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate; -(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
-(void) connectionEstablished; -(void) connectionEstablished;
@@ -60,4 +62,7 @@
-(void)updateControllerSupport:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate; -(void)updateControllerSupport:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
-(void) reinitiatePrimaryController;
-(void)sendNavigationButtonPress;
@end @end
+229 -364
View File
@@ -24,6 +24,7 @@
@import AudioToolbox; @import AudioToolbox;
static const double MOUSE_SPEED_DIVISOR = 1.25; static const double MOUSE_SPEED_DIVISOR = 1.25;
static __weak ControllerSupport *VLSharedControllerSupport = nil;
@interface ControllerSupport() @interface ControllerSupport()
@@ -50,22 +51,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
float accumulatedScrollX; float accumulatedScrollX;
float accumulatedScrollY; float accumulatedScrollY;
int _controllerMouseSwitch; bool _controllerNavigationEnabled;
bool _mouseSwitchButtonPressed;
bool _mouseSwitchButtonBeingClicked;
NSTimeInterval mouseSwitchDownTimestamp;
int _controllerMouseLeftButton;
int _controllerMouseRightButton;
ControllerMouseStick _controllerMouseStick;
bool _mapControllerToMouse;
bool _controllerMouseEnabledFlag;
float stickToMouseInputX;
float stickToMouseInputY;
float stickToWheelInputX;
float stickToWheelInputY;
float _stickToMouseExpo;
float _stickToMouseVelocity;
CADisplayLink *_displayLink;
float stickMaxOffset; float stickMaxOffset;
float _leftStickMinOffset; float _leftStickMinOffset;
@@ -100,6 +86,11 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
__weak MotionHandler* motionHandler; __weak MotionHandler* motionHandler;
} }
+(ControllerSupport*) sharedInstance
{
return VLSharedControllerSupport;
}
// UPDATE_BUTTON_FLAG(controller, flag, pressed) // UPDATE_BUTTON_FLAG(controller, flag, pressed)
#define UPDATE_BUTTON_FLAG(controller, x, y) \ #define UPDATE_BUTTON_FLAG(controller, x, y) \
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x]) ((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
@@ -1020,39 +1011,6 @@ double rc_expo(double x, double expo) {
return x > 0 ? y : -y; return x > 0 ? y : -y;
} }
- (void)sendStickToMouseMoveEventWithStickX:(float)stickX stickY:(float)stickY expo:(float)expo {
CGFloat mouseDeltaX = _stickToMouseVelocity*rc_expo(stickX, expo);
CGFloat mouseDeltaY = _stickToMouseVelocity*rc_expo(stickY, expo);
LiSendMouseMoveEvent(mouseDeltaX, -mouseDeltaY);
}
- (void)stopDisplayLink {
[_displayLink invalidate];
_displayLink = nil;
}
- (void)displayLinkCallBack {
if(!_controllerMouseEnabledFlag){
return;
}
NSTimeInterval delay = 0.5/_displayLink.preferredFramesPerSecond*NSEC_PER_SEC;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self sendStickToMouseMoveEventWithStickX:self->stickToMouseInputX stickY:self->stickToMouseInputY expo:self->_stickToMouseExpo];
LiSendHighResScrollEvent(15*self->stickToWheelInputY);
LiSendHighResHScrollEvent(15*self->stickToWheelInputX);
});
}
- (void)sendControllerMouseSwitchClick:(VoidController*) voidController{
[self setButtonFlag:voidController flags:self->_controllerMouseSwitch];
_mouseSwitchButtonBeingClicked = true;
[self updateFinished:voidController];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.03*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self clearButtonFlag:voidController flags:self->_controllerMouseSwitch];
self->_mouseSwitchButtonBeingClicked = false;
[self updateFinished:voidController];
});
}
- (bool)useMotionHandler{ - (bool)useMotionHandler{
return self->tempSettings.emulatedControllerType.intValue!=LI_CTYPE_PS return self->tempSettings.emulatedControllerType.intValue!=LI_CTYPE_PS
@@ -1068,6 +1026,199 @@ double rc_expo(double x, double expo) {
else self->_gyroEnabledFlag = self->_gyroEnabledFlag || !self->_controllerGyroSwitchEnabled; else self->_gyroEnabledFlag = self->_gyroEnabledFlag || !self->_controllerGyroSwitchEnabled;
} }
- (void)sendNavigationButtonPress {
if(!ControllerUtil.primaryGCController) return;
if(@available(iOS 13.0, *)) if(ControllerNavigator.controllerMouseEnabled) return;
VoidController* voidController = [self->_voidControllers objectForKey:@(ControllerUtil.primaryGCController.playerIndex)];
int radialMenuButton = ControllerElementNull;
if (@available(iOS 13.0, *)) radialMenuButton = ControllerNavigator.radialMenuButton;
[self setButtonFlag:voidController flags:(int)radialMenuButton];
[self updateFinished:voidController];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.03*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self clearButtonFlag:voidController flags:radialMenuButton];
[self updateFinished:voidController];
});
}
- (void) listenToGCController:(GCController*) controller {
/*
if(self->_swapABXYButtons){
switch (self->_controllerMouseLeftButton) {
case ControllerElementA:
self->_controllerMouseLeftButton = ControllerElementB;
break;
case ControllerElementB:
self->_controllerMouseLeftButton = ControllerElementA;
break;
case ControllerElementX:
self->_controllerMouseLeftButton = ControllerElementY;
break;
case ControllerElementY:
self->_controllerMouseLeftButton = ControllerElementX;
break;
default:
break;
}
switch (self->_controllerMouseRightButton) {
case ControllerElementA:
self->_controllerMouseRightButton = ControllerElementB;
break;
case ControllerElementB:
self->_controllerMouseRightButton = ControllerElementA;
break;
case ControllerElementX:
self->_controllerMouseRightButton = ControllerElementY;
break;
case ControllerElementY:
self->_controllerMouseRightButton = ControllerElementX;
break;
default:
break;
}
}
*/
__block ControllerElement radialMenuButton = ControllerElementNull;
__block bool controllerNavigatorEnabled = false;
[ControllerUtil listenWithController:controller swapABXY:self->_swapABXYButtons handler:^(NSDictionary * elementDict, GCExtendedGamepad * gamepad, GCControllerElement * element) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
short leftStickX, leftStickY;
short rightStickX, rightStickY;
unsigned char leftTrigger, rightTrigger;
if (@available(iOS 13.0, *)){
radialMenuButton = ControllerNavigator.radialMenuButton;
controllerNavigatorEnabled = ControllerNavigator.enabled;
}
for(NSNumber* elementFlagId in elementDict){
GCControllerElement * element = (GCControllerElement *)elementDict[elementFlagId];
if ([element isKindOfClass:[GCControllerButtonInput class]]) {
GCControllerButtonInput* button = (GCControllerButtonInput *)element;
if(controllerNavigatorEnabled && voidController.playerIndex==0 && elementFlagId.intValue == radialMenuButton){
if(!button.pressed){
// [self sendNavigationButtonPress:voidController];
}
}
// controller switch buttons
if(true){
if(button.pressed){
if (elementFlagId.intValue == self->_controllerGyroSwitchToggle
&& !self->_controllerGyroSwitchTogglePressed) {
self->_controllerGyroSwitchTogglePressed = true;
self->_gyroEnabledFlag = !self->_gyroEnabledFlag;
[self switchMotionControlOnOffByControllerButton];
}
if (elementFlagId.intValue == self->_controllerGyroSwitchHold
&& !self->_controllerGyroSwitchHoldPressed) {
self->_controllerGyroSwitchHoldPressed = true;
self->_gyroEnabledFlag = !self->_reverseHoldButton;
[self switchMotionControlOnOffByControllerButton];
}
}
else{
if (elementFlagId.intValue == self->_controllerGyroSwitchToggle
&& self->_controllerGyroSwitchTogglePressed) {
self->_controllerGyroSwitchTogglePressed = false;
}
if (elementFlagId.intValue == self->_controllerGyroSwitchHold
&& self->_controllerGyroSwitchHoldPressed) {
self->_controllerGyroSwitchHoldPressed = false;
self->_gyroEnabledFlag = self->_reverseHoldButton;
[self switchMotionControlOnOffByControllerButton];
}
}
}
if(controller != ControllerUtil.primaryGCController || elementFlagId.intValue!=radialMenuButton || !controllerNavigatorEnabled) UPDATE_BUTTON_FLAG(voidController, elementFlagId.intValue, button.pressed);
}
}
CGFloat leftStickXRaw = gamepad.leftThumbstick.xAxis.value * self->stickMaxOffset;
CGFloat leftStickYRaw = gamepad.leftThumbstick.yAxis.value * self->stickMaxOffset;
CGFloat rightStickXRaw = gamepad.rightThumbstick.xAxis.value * self->stickMaxOffset;
CGFloat rightStickYRaw = gamepad.rightThumbstick.yAxis.value * self->stickMaxOffset;
CGVector leftStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(leftStickXRaw, leftStickYRaw) minOffset:self->_leftStickMinOffset circulate:false];
CGVector rightStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(rightStickXRaw, rightStickYRaw) minOffset:self->_rightStickMinOffset circulate:false];
leftStickX = leftStickOffset.dx;
leftStickY = leftStickOffset.dy;
rightStickX = rightStickOffset.dx;
rightStickY = rightStickOffset.dy;
/*
if(self->oscProfile.mapGyroTo!=mapGyroToControllerStick
||!self->oscProfile.rollToLeftStick) [self updateLeftStick:voidController x:leftStickX y:leftStickY];
*/
if([self useMotionHandler]
&& self->oscProfile.mapGyroTo==mapGyroToControllerStick
&& self->oscProfile.yawPitchToRightStick
&& self->_gyroEnabledFlag
) {
[self->motionHandler mixPhysicalRightStickAndGyroInputWithX:rightStickX y:rightStickY];
}
else [self updateRightStick: voidController.playerIndex==0?self->_oscController:voidController x:rightStickX y:rightStickY];
if([self useMotionHandler]
&& self->oscProfile.mapGyroTo==mapGyroToControllerStick
&& self->oscProfile.rollToLeftStick
&& self->_gyroEnabledFlag
) [self->motionHandler mixPhysicalLeftStickAndGyroInputWithX:leftStickX y:leftStickY];
else [self updateLeftStick: voidController.playerIndex==0?self->_oscController:voidController x:leftStickX y:leftStickY];
leftTrigger = gamepad.leftTrigger.value * 0xFF;
rightTrigger = gamepad.rightTrigger.value * 0xFF;
[self updateTriggers:voidController left:leftTrigger right:rightTrigger];
[self updateFinished:voidController];
if (@available(iOS 14.0, *)) {
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
[self handleControllerTouchpad:voidController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]
index:0];
}
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) {
[self handleControllerTouchpad:voidController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]
index:1];
}
}
});
}];
}
-(void) reinitiatePrimaryController {
if(@available(iOS 13.0, *)){
if(ControllerNavigator.controllerMouseEnabled){
[ControllerNavigator startControllerMouse];
return;
}
if(ControllerNavigator.radialMenuView) return;
}
if(![StreamFrameViewController sharedInstance]) return;
MainFrameViewController* mainFrameVC =(MainFrameViewController* )ControllerUtil.delegate;
if(mainFrameVC.settingsExpandedInStreamView) return;
[ControllerUtil stopListeningPrimaryControllerWithStopListenToRadialMenuButton:false];
[self listenToGCController: ControllerUtil.primaryGCController];
if (@available(iOS 13.0, *)){
NSLog(@"ControllerUtil setUINavigationDelegate %f",CACurrentMediaTime());
[ControllerNavigator setStreamFrameVCAsUINavigationDelegate];
}
}
-(void) registerControllerCallbacks:(GCController*) controller -(void) registerControllerCallbacks:(GCController*) controller
{ {
if (controller != NULL) { if (controller != NULL) {
@@ -1106,299 +1257,19 @@ double rc_expo(double x, double expo) {
if (controller.extendedGamepad != NULL) { if (controller.extendedGamepad != NULL) {
// Disable system gestures on the gamepad to avoid interfering // Disable system gestures on the gamepad to avoid interfering
// with in-game controller actions // with in-game controller actions
if (@available(iOS 14.0, tvOS 14.0, *)) { if (@available(iOS 14.0, tvOS 14.0, *)) {
for (GCControllerElement* element in controller.physicalInputProfile.allElements) { for (GCControllerElement* element in controller.physicalInputProfile.allElements) {
element.preferredSystemGestureState = GCSystemGestureStateDisabled; element.preferredSystemGestureState = GCSystemGestureStateDisabled;
} }
} }
if(self->_swapABXYButtons){ if(controller == ControllerUtil.primaryGCController){
switch (self->_controllerMouseLeftButton) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1*NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
case ControllerButtonA: [self reinitiatePrimaryController];
self->_controllerMouseLeftButton = ControllerButtonB;
break;
case ControllerButtonB:
self->_controllerMouseLeftButton = ControllerButtonA;
break;
case ControllerButtonX:
self->_controllerMouseLeftButton = ControllerButtonY;
break;
case ControllerButtonY:
self->_controllerMouseLeftButton = ControllerButtonX;
break;
default:
break;
}
switch (self->_controllerMouseRightButton) {
case ControllerButtonA:
self->_controllerMouseRightButton = ControllerButtonB;
break;
case ControllerButtonB:
self->_controllerMouseRightButton = ControllerButtonA;
break;
case ControllerButtonX:
self->_controllerMouseRightButton = ControllerButtonY;
break;
case ControllerButtonY:
self->_controllerMouseRightButton = ControllerButtonX;
break;
default:
break;
}
}
[ControllerUtil listenWithController:controller swapABXY:self->_swapABXYButtons handler:^(NSDictionary * buttonDict, GCExtendedGamepad * gamepad, GCControllerElement * element) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
short leftStickX, leftStickY;
short rightStickX, rightStickY;
unsigned char leftTrigger, rightTrigger;
for(NSNumber* buttonFlagId in buttonDict){
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId];
if(self->_mapControllerToMouse && voidController.playerIndex==0){
if(button.pressed){
if(buttonFlagId.intValue == self->_controllerMouseSwitch){
self->_mouseSwitchButtonPressed = true;
if(self->mouseSwitchDownTimestamp == 0) self->mouseSwitchDownTimestamp = CACurrentMediaTime();
}
}
else{
if(buttonFlagId.intValue == self->_controllerMouseSwitch && self->_mouseSwitchButtonPressed){
if(CACurrentMediaTime()-self->mouseSwitchDownTimestamp>1){
self->_controllerMouseEnabledFlag = !self->_controllerMouseEnabledFlag;
[self updateLeftStick:voidController x:0 y:0];
[self updateRightStick:voidController x:0 y:0];
}
else [self sendControllerMouseSwitchClick:voidController];
self->_mouseSwitchButtonPressed = false;
self->mouseSwitchDownTimestamp = 0;
}
}
}
else self->_controllerMouseEnabledFlag = false;
// controller switch buttons
if(true){
if(button.pressed){
if (buttonFlagId.intValue == self->_controllerGyroSwitchToggle
&& !self->_controllerGyroSwitchTogglePressed) {
self->_controllerGyroSwitchTogglePressed = true;
self->_gyroEnabledFlag = !self->_gyroEnabledFlag;
[self switchMotionControlOnOffByControllerButton];
}
if (buttonFlagId.intValue == self->_controllerGyroSwitchHold
&& !self->_controllerGyroSwitchHoldPressed) {
self->_controllerGyroSwitchHoldPressed = true;
self->_gyroEnabledFlag = !self->_reverseHoldButton;
[self switchMotionControlOnOffByControllerButton];
}
}
else{
if (buttonFlagId.intValue == self->_controllerGyroSwitchToggle
&& self->_controllerGyroSwitchTogglePressed) {
self->_controllerGyroSwitchTogglePressed = false;
}
if (buttonFlagId.intValue == self->_controllerGyroSwitchHold
&& self->_controllerGyroSwitchHoldPressed) {
self->_controllerGyroSwitchHoldPressed = false;
self->_gyroEnabledFlag = self->_reverseHoldButton;
[self switchMotionControlOnOffByControllerButton];
}
}
}
if(self->_controllerMouseEnabledFlag){
if(buttonFlagId.intValue == self->_controllerMouseLeftButton || buttonFlagId.intValue == self->_controllerMouseRightButton){
if(buttonFlagId.intValue == self->_controllerMouseLeftButton) LiSendMouseButtonEvent(button.pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_LEFT);
if(buttonFlagId.intValue == self->_controllerMouseRightButton) LiSendMouseButtonEvent(button.pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, NO);
}
else if(buttonFlagId.intValue!=self->_controllerMouseSwitch || !self->_mapControllerToMouse) UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, button.pressed);
}
else if(buttonFlagId.intValue!=self->_controllerMouseSwitch || !self->_mapControllerToMouse) UPDATE_BUTTON_FLAG(voidController, buttonFlagId.intValue, button.pressed);
}
CGFloat leftStickXRaw = gamepad.leftThumbstick.xAxis.value * self->stickMaxOffset;
CGFloat leftStickYRaw = gamepad.leftThumbstick.yAxis.value * self->stickMaxOffset;
CGFloat rightStickXRaw = gamepad.rightThumbstick.xAxis.value * self->stickMaxOffset;
CGFloat rightStickYRaw = gamepad.rightThumbstick.yAxis.value * self->stickMaxOffset;
CGVector leftStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(leftStickXRaw, leftStickYRaw) minOffset:self->_leftStickMinOffset circulate:false];
CGVector rightStickOffset = [ControllerUtil compensatedWithOffsetVector:CGVectorMake(rightStickXRaw, rightStickYRaw) minOffset:self->_rightStickMinOffset circulate:false];
leftStickX = self->_controllerMouseEnabledFlag ? 0 : leftStickOffset.dx;
leftStickY = self->_controllerMouseEnabledFlag ? 0 : leftStickOffset.dy;
rightStickX = self->_controllerMouseEnabledFlag ? 0 : rightStickOffset.dx;
rightStickY = self->_controllerMouseEnabledFlag ? 0 : rightStickOffset.dy;
if(self->_controllerMouseEnabledFlag){
self->stickToMouseInputX = self->_controllerMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value : gamepad.rightThumbstick.xAxis.value;
self->stickToMouseInputY = self->_controllerMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value : gamepad.rightThumbstick.yAxis.value;
self->stickToWheelInputX = self->_controllerMouseStick == LeftStickToMouse ? gamepad.rightThumbstick.xAxis.value: gamepad.leftThumbstick.xAxis.value;
self->stickToWheelInputY = self->_controllerMouseStick == LeftStickToMouse ? gamepad.rightThumbstick.yAxis.value: gamepad.leftThumbstick.yAxis.value;
}
else{
self->stickToMouseInputX = 0;
self->stickToMouseInputY = 0;
}
/*
if(self->oscProfile.mapGyroTo!=mapGyroToControllerStick
||!self->oscProfile.rollToLeftStick) [self updateLeftStick:voidController x:leftStickX y:leftStickY];
*/
if([self useMotionHandler]
&& self->oscProfile.mapGyroTo==mapGyroToControllerStick
&& self->oscProfile.yawPitchToRightStick
&& self->_gyroEnabledFlag
) [self->motionHandler mixPhysicalRightStickAndGyroInputWithX:rightStickX y:rightStickY];
else [self updateRightStick: voidController.playerIndex==0?self->_oscController:voidController x:rightStickX y:rightStickY];
if([self useMotionHandler]
&& self->oscProfile.mapGyroTo==mapGyroToControllerStick
&& self->oscProfile.rollToLeftStick
&& self->_gyroEnabledFlag
) [self->motionHandler mixPhysicalLeftStickAndGyroInputWithX:leftStickX y:leftStickY];
else [self updateLeftStick: voidController.playerIndex==0?self->_oscController:voidController x:leftStickX y:leftStickY];
leftTrigger = gamepad.leftTrigger.value * 0xFF;
rightTrigger = gamepad.rightTrigger.value * 0xFF;
[self updateTriggers:voidController left:leftTrigger right:rightTrigger];
[self updateFinished:voidController];
if (@available(iOS 14.0, *)) {
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
[self handleControllerTouchpad:voidController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]
index:0];
}
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) {
[self handleControllerTouchpad:voidController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]
index:1];
}
}
}); });
}]; }
else [self listenToGCController:controller];
/*
controller.extendedGamepad.valueChangedHandler =
^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
short leftStickX, leftStickY;
short rightStickX, rightStickY;
unsigned char leftTrigger, rightTrigger;
if (self->_swapABXYButtons) {
UPDATE_BUTTON_FLAG(voidController, B_FLAG, gamepad.buttonA.pressed);
UPDATE_BUTTON_FLAG(voidController, A_FLAG, gamepad.buttonB.pressed);
UPDATE_BUTTON_FLAG(voidController, Y_FLAG, gamepad.buttonX.pressed);
UPDATE_BUTTON_FLAG(voidController, X_FLAG, gamepad.buttonY.pressed);
}
else {
UPDATE_BUTTON_FLAG(voidController, A_FLAG, gamepad.buttonA.pressed);
UPDATE_BUTTON_FLAG(voidController, B_FLAG, gamepad.buttonB.pressed);
UPDATE_BUTTON_FLAG(voidController, X_FLAG, gamepad.buttonX.pressed);
UPDATE_BUTTON_FLAG(voidController, Y_FLAG, gamepad.buttonY.pressed);
}
UPDATE_BUTTON_FLAG(voidController, UP_FLAG, gamepad.dpad.up.pressed);
UPDATE_BUTTON_FLAG(voidController, DOWN_FLAG, gamepad.dpad.down.pressed);
UPDATE_BUTTON_FLAG(voidController, LEFT_FLAG, gamepad.dpad.left.pressed);
UPDATE_BUTTON_FLAG(voidController, RIGHT_FLAG, gamepad.dpad.right.pressed);
UPDATE_BUTTON_FLAG(voidController, LB_FLAG, gamepad.leftShoulder.pressed);
UPDATE_BUTTON_FLAG(voidController, RB_FLAG, gamepad.rightShoulder.pressed);
// Yay, iOS 12.1 now supports analog stick buttons
if (@available(iOS 12.1, tvOS 12.1, *)) {
if (gamepad.leftThumbstickButton != nil) {
UPDATE_BUTTON_FLAG(voidController, LS_CLK_FLAG, gamepad.leftThumbstickButton.pressed);
}
if (gamepad.rightThumbstickButton != nil) {
UPDATE_BUTTON_FLAG(voidController, RS_CLK_FLAG, gamepad.rightThumbstickButton.pressed);
}
}
if (@available(iOS 13.0, tvOS 13.0, *)) {
// Options button is optional (only present on Xbox One S and PS4 gamepads)
if (gamepad.buttonOptions != nil) {
UPDATE_BUTTON_FLAG(voidController, BACK_FLAG, gamepad.buttonOptions.pressed);
// For older MFi gamepads, the menu button will already be handled by
// the controllerPausedHandler.
UPDATE_BUTTON_FLAG(voidController, PLAY_FLAG, gamepad.buttonMenu.pressed);
}
}
if (@available(iOS 14.0, tvOS 14.0, *)) {
// Home/Guide button is optional (only present on Xbox One S and PS4 gamepads)
if (gamepad.buttonHome != nil) {
UPDATE_BUTTON_FLAG(voidController, SPECIAL_FLAG, gamepad.buttonHome.pressed);
}
// Xbox One/Series controllers
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne]) {
UPDATE_BUTTON_FLAG(voidController, PADDLE1_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].pressed);
}
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo]) {
UPDATE_BUTTON_FLAG(voidController, PADDLE2_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].pressed);
}
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree]) {
UPDATE_BUTTON_FLAG(voidController, PADDLE3_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].pressed);
}
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour]) {
UPDATE_BUTTON_FLAG(voidController, PADDLE4_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].pressed);
}
if (@available(iOS 15.0, tvOS 15.0, *)) {
if (gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare]) {
UPDATE_BUTTON_FLAG(voidController, MISC_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare].pressed);
}
}
// DualShock/DualSense controllers
if (gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton]) {
UPDATE_BUTTON_FLAG(voidController, TOUCHPAD_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton].pressed);
}
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
[self handleControllerTouchpad:voidController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]
index:0];
}
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) {
[self handleControllerTouchpad:voidController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]
index:1];
}
}
leftStickX = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value * 0x7FFE : 0;
leftStickY = self->controllerToMouseStick != LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value * 0x7FFE : 0;;
rightStickX = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.xAxis.value * 0x7FFE : 0;
rightStickY = self->controllerToMouseStick != RightStickToMouse ? gamepad.rightThumbstick.yAxis.value * 0x7FFE : 0;
self->stickToMouseInputX = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.xAxis.value : gamepad.rightThumbstick.xAxis.value;
self->stickToMouseInputY = self->controllerToMouseStick == LeftStickToMouse ? gamepad.leftThumbstick.yAxis.value : gamepad.rightThumbstick.yAxis.value;
leftTrigger = gamepad.leftTrigger.value * 0xFF;
rightTrigger = gamepad.rightTrigger.value * 0xFF;
[self updateLeftStick:voidController x:leftStickX y:leftStickY];
[self updateRightStick:voidController x:rightStickX y:rightStickY];
[self updateTriggers:voidController left:leftTrigger right:rightTrigger];
[self updateFinished:voidController];
};
*/
} }
} else { } else {
Log(LOG_W, @"Tried to register controller callbacks on NULL controller"); Log(LOG_W, @"Tried to register controller callbacks on NULL controller");
@@ -1749,29 +1620,9 @@ double rc_expo(double x, double expo) {
_oscEnabled = _oscEnabled || (OnScreenControlsLevel)[tempSettings.onscreenControls integerValue] != OnScreenControlsLevelOff || streamConfig.gyroMode != GyroModeOff; _oscEnabled = _oscEnabled || (OnScreenControlsLevel)[tempSettings.onscreenControls integerValue] != OnScreenControlsLevelOff || streamConfig.gyroMode != GyroModeOff;
_gyroSensitivity = tempSettings.gyroSensitivity.floatValue; _gyroSensitivity = tempSettings.gyroSensitivity.floatValue;
_mapControllerToMouse = tempSettings.mapControllerToMouse; if (@available(iOS 13.0, *)) {
_controllerMouseSwitch = tempSettings.controllerMouseSwitch.intValue; _controllerNavigationEnabled = ControllerNavigator.enabled;
mouseSwitchDownTimestamp = 0; [ControllerNavigator configureControllerMouseWith:tempSettings];
_mouseSwitchButtonPressed = false;
_mouseSwitchButtonBeingClicked = false;
_controllerMouseStick = tempSettings.controllerMouseStick.intValue;
_controllerMouseLeftButton = tempSettings.controllerMouseLeftButton.intValue;
_controllerMouseRightButton = tempSettings.controllerMouseRightButton.intValue;
_stickToMouseExpo = tempSettings.controllerMouseExpo.floatValue;
_stickToMouseVelocity = tempSettings.controllerMousePointerVelocity.floatValue*60/tempSettings.framerate.intValue;
stickToMouseInputX = 0;
stickToMouseInputY = 0;
stickToWheelInputY = 0;
[self stopDisplayLink];
if(_mapControllerToMouse){
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallBack)];
if (@available(iOS 15.0, tvOS 15.0, *)) {
[_displayLink setPreferredFrameRateRange:CAFrameRateRangeMake(tempSettings.framerate.intValue,tempSettings.framerate.intValue, tempSettings.framerate.intValue)];
}
else {
_displayLink.preferredFramesPerSecond = tempSettings.framerate.intValue;
}
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
} }
_controllerGyroSwitchEnabled = oscProfile.controllerGyroSwitchMode != ControllerGyroSwitchDisabled; _controllerGyroSwitchEnabled = oscProfile.controllerGyroSwitchMode != ControllerGyroSwitchDisabled;
@@ -1787,9 +1638,11 @@ double rc_expo(double x, double expo) {
_leftStickMinOffset = oscProfile.physicalLeftStickMinOffset; _leftStickMinOffset = oscProfile.physicalLeftStickMinOffset;
_rightStickMinOffset = oscProfile.physicalRightStickMinOffset; _rightStickMinOffset = oscProfile.physicalRightStickMinOffset;
if(oscProfile.controllerGyroSwitchMode == ControllerGyroSwitchDisabled && ![self useMotionHandler]) _gyroEnabledFlag = true; // if(oscProfile.controllerGyroSwitchMode == ControllerGyroSwitchDisabled && ![self useMotionHandler]) _gyroEnabledFlag = false;
_gyroEnabledFlag = false;
if(![self useMotionHandler]) [self->motionHandler stopMotionUpdateWithInterruptNoneGyroInput:false];
// if(![self useMotionHandler]) [self->motionHandler stopMotionUpdateWithInterruptNoneGyroInput:false];
[self->motionHandler stopMotionUpdateWithInterruptNoneGyroInput:false];
} }
- (void)resetGyroInputForController:(VoidController* )voidController{ - (void)resetGyroInputForController:(VoidController* )voidController{
@@ -1833,6 +1686,9 @@ double rc_expo(double x, double expo) {
-(id)initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate -(id)initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate
{ {
self = [super init]; self = [super init];
if (self) {
VLSharedControllerSupport = self;
}
NSLog(@"controller support init"); NSLog(@"controller support init");
@@ -1985,7 +1841,7 @@ double rc_expo(double x, double expo) {
[self initializeControllerHaptics:_oscController]; [self initializeControllerHaptics:_oscController];
_gyroMode = AlwaysDevice; _gyroMode = AlwaysDevice;
_controllerMouseEnabledFlag = false; // _controllerMouseEnabledFlag = false;
_gyroEnabledFlag = false; _gyroEnabledFlag = false;
oscProfileMan = [OSCProfilesManager sharedManager:CGRectZero]; oscProfileMan = [OSCProfilesManager sharedManager:CGRectZero];
@@ -2096,6 +1952,10 @@ double rc_expo(double x, double expo) {
-(void) cleanup -(void) cleanup
{ {
if (VLSharedControllerSupport == self) {
VLSharedControllerSupport = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:_controllerConnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_controllerConnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_controllerDisconnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_controllerDisconnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_mouseConnectObserver]; [[NSNotificationCenter defaultCenter] removeObserver:_mouseConnectObserver];
@@ -2137,8 +1997,13 @@ double rc_expo(double x, double expo) {
[self unregisterMouseCallbacks:mouse]; [self unregisterMouseCallbacks:mouse];
} }
} }
}
[self stopDisplayLink];
-(void) dealloc
{
if (VLSharedControllerSupport == self) {
VLSharedControllerSupport = nil;
}
} }
@end @end
+388 -54
View File
@@ -12,7 +12,22 @@ import Combine
import CoreGraphics import CoreGraphics
import Foundation import Foundation
@objc enum ControllerButton: Int { @objc enum ControllerElementType: Int {
case button
case stick
case stickAxis
case touchpad
case undefined
}
@objc enum ControllerElementPosition: Int {
case left
case right
case middle
case undefined
}
@objc enum ControllerElement: Int32 {
// Face buttons // Face buttons
case a = 0x1000 case a = 0x1000
case b = 0x2000 case b = 0x2000
@@ -34,8 +49,8 @@ import Foundation
case rightStickButton = 0x0080 case rightStickButton = 0x0080
// Menu/Back/Special // Menu/Back/Special
case menu = 0x0010 // PLAY_FLAG case start = 0x0010 // PLAY_FLAG
case back = 0x0020 // BACK_FLAG case select = 0x0020 // BACK_FLAG
case special = 0x0400 case special = 0x0400
// Extended buttons (Sunshine only) // Extended buttons (Sunshine only)
@@ -49,37 +64,209 @@ import Foundation
case leftTrigger = 0x400000 case leftTrigger = 0x400000
case rightTrigger = 0x800000 case rightTrigger = 0x800000
case leftStick = 0x800010
case leftStickX = 0x80011
case leftStickY = 0x800012
case rightStick = 0x800020
case rightStickX = 0x800021
case rightStickY = 0x800022
case null = 0xFFFFFF case null = 0xFFFFFF
var displayName: String {
switch self {
case .a: return "A"
case .b: return "B"
case .x: return "X"
case .y: return "Y"
case .dpadUp: return "DPadU".localized
case .dpadDown: return "DPadD".localized
case .dpadLeft: return "DPadL".localized
case .dpadRight: return "DPadR".localized
case .leftShoulder: return "LB"
case .rightShoulder: return "RB"
case .leftStickButton: return "LS"
case .rightStickButton: return "RS"
case .start: return "Start"
case .select: return "Select"
case .special: return "Home"
case .paddle1: return "Paddle1".localized
case .paddle2: return "Paddle2".localized
case .paddle3: return "Paddle3".localized
case .paddle4: return "Paddle4".localized
case .touchpadButton: return "TouchBtn".localized
case .misc: return "Misc"
case .leftTrigger: return "LT"
case .rightTrigger: return "RT"
case .leftStick: return "LStick".localized
case .leftStickX: return "LStickX".localized
case .leftStickY: return "LStickY".localized
case .rightStick: return "RStick".localized
case .rightStickX: return "RStickX".localized
case .rightStickY: return "RStickY".localized
case .null: return "Null".localized
}
}
var symbol: String {
switch self {
case .dpadUp:
if #available(iOS 15.0, *) {return "dpad.up.filled"}
else {return ""}
case .dpadDown:
if #available(iOS 15.0, *) {return "dpad.down.filled"}
else {return ""}
case .dpadLeft:
if #available(iOS 15.0, *) {return "dpad.left.filled"}
else {return ""}
case .dpadRight:
if #available(iOS 15.0, *) {return "dpad.right.filled"}
else {return ""}
case .a:
if #available(iOS 15.0, *) {return "circle.grid.cross.down.filled"}
else {return ""}
case .b:
if #available(iOS 15.0, *) {return "circle.grid.cross.right.filled"}
else {return ""}
case .x:
if #available(iOS 15.0, *) {return "circle.grid.cross.left.filled"}
else {return ""}
case .y:
if #available(iOS 15.0, *) {return "circle.grid.cross.up.filled"}
else {return ""}
default: return ""
}
}
var type: ControllerElementType {
switch self {
case .a: return .button
case .b: return .button
case .x: return .button
case .y: return .button
case .dpadUp: return .button
case .dpadDown: return .button
case .dpadLeft: return .button
case .dpadRight: return .button
case .leftShoulder: return .button
case .rightShoulder: return .button
case .leftStickButton: return .button
case .rightStickButton: return .button
case .start: return .button
case .select: return .button
case .special: return .button
case .paddle1: return .button
case .paddle2: return .button
case .paddle3: return .button
case .paddle4: return .button
case .touchpadButton: return .button
case .misc: return .button
case .leftTrigger: return .button
case .rightTrigger: return .button
case .leftStick: return .stick
case .leftStickX: return .stickAxis
case .leftStickY: return .stickAxis
case .rightStick: return .stick
case .rightStickX: return .stickAxis
case .rightStickY: return .stickAxis
case .null: return .undefined
}
}
var position: ControllerElementPosition {
switch self {
case .a: return .right
case .b: return .right
case .x: return .right
case .y: return .right
case .dpadUp: return .left
case .dpadDown: return .left
case .dpadLeft: return .left
case .dpadRight: return .left
case .leftShoulder: return .left
case .rightShoulder: return .right
case .leftStickButton: return .left
case .rightStickButton: return .right
case .start: return .right
case .select: return .left
case .special: return .undefined
case .paddle1: return .right
case .paddle2: return .right
case .paddle3: return .left
case .paddle4: return .left
case .touchpadButton: return .middle
case .misc: return .undefined
case .leftTrigger: return .left
case .rightTrigger: return .right
case .leftStick: return .left
case .leftStickX: return .left
case .leftStickY: return .left
case .rightStick: return .right
case .rightStickX: return .right
case .rightStickY: return .right
case .null: return .undefined
}
}
} }
@objc protocol ControllerUtilDelegate: AnyObject {
func isStreaming() -> Bool
func isInAppView() -> Bool
}
@objc class ControllerUtil: NSObject { @objc class ControllerUtil: NSObject {
@objc static weak var delegate: ControllerUtilDelegate?
static private let stickMaxOffset:CGFloat = 0x7FFE static private let stickMaxOffset:CGFloat = 0x7FFE
@objc static var navigationActionTriggered:Bool = false @objc static var navigationActionTriggered:Bool = false
@objc static private(set) var navigationActionTriggeredPrivate:Bool = false @objc static private(set) var navigationActionTriggeredPrivate:Bool = false
@objc static func listen( @objc static func listen(
controller: GCController, controller: GCController,
swapABXY: Bool, swapABXY: Bool,
handler: @escaping (_ buttonDict: NSDictionary, handler: @escaping (_ elementDict: NSDictionary,
_ gamepad: GCExtendedGamepad, _ gamepad: GCExtendedGamepad,
_ element: GCControllerElement) -> Void _ element: GCControllerElement) -> Void
) { ) {
guard let gamepad = controller.extendedGamepad else { return } guard let gamepad = controller.extendedGamepad else { return }
// 内部生成 map // 内部生成 map
var tempMap: [NSNumber: GCControllerButtonInput] = [:] var tempMap: [NSNumber: GCControllerElement] = [:]
let swiftMap = buildMapping(for: controller, swapABXY: swapABXY) let swiftMap = buildMapping(for: controller, swapABXY: swapABXY)
for (button, input) in swiftMap { for (button, input) in swiftMap {
tempMap[NSNumber(value: button.rawValue)] = input tempMap[NSNumber(value: button.rawValue)] = input
} }
let buttonDict = tempMap as NSDictionary let elementDict = tempMap as NSDictionary
// 单一 gamepad.valueChangedHandler // 单一 gamepad.valueChangedHandler
gamepad.valueChangedHandler = { gamepad, element in gamepad.valueChangedHandler = { gamepad, element in
handler(buttonDict, gamepad, element) handler(elementDict, gamepad, element)
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
// print("controller.playerIndex \(controller.playerIndex)")
if controller.playerIndex == .index1 { if controller.playerIndex == .index1 {
GamepadOverlayStateCenter.shared.publish(snapshot: GamepadOverlaySnapshot(gamepad: gamepad)) GamepadOverlayStateCenter.shared.publish(snapshot: GamepadOverlaySnapshot(gamepad: gamepad))
} }
@@ -87,12 +274,12 @@ import Foundation
} }
} }
// MARK: - 构建按钮映射 // MARK: - 构建按钮映射
private static func buildMapping(for controller: GCController, swapABXY:Bool) static func buildMapping(for controller: GCController, swapABXY:Bool)
-> [ControllerButton : GCControllerButtonInput] -> [ControllerElement : GCControllerElement]
{ {
var result: [ControllerButton : GCControllerButtonInput] = [:] var result: [ControllerElement : GCControllerElement] = [:]
ControllerUtil.swapABXY = swapABXY
if let pad = controller.extendedGamepad { if let pad = controller.extendedGamepad {
// Face buttons // Face buttons
@@ -114,7 +301,7 @@ import Foundation
result[.rightShoulder] = pad.rightShoulder result[.rightShoulder] = pad.rightShoulder
result[.leftTrigger] = pad.leftTrigger result[.leftTrigger] = pad.leftTrigger
result[.rightTrigger] = pad.rightTrigger result[.rightTrigger] = pad.rightTrigger
// Stick buttons // Stick buttons
if #available(iOS 12.1, *) { if #available(iOS 12.1, *) {
result[.leftStickButton] = pad.leftThumbstickButton result[.leftStickButton] = pad.leftThumbstickButton
@@ -127,10 +314,19 @@ import Foundation
result[.dpadLeft] = pad.dpad.left result[.dpadLeft] = pad.dpad.left
result[.dpadRight] = pad.dpad.right result[.dpadRight] = pad.dpad.right
// Sticks
result[.leftStick] = pad.leftThumbstick
result[.leftStickX] = pad.leftThumbstick.xAxis
result[.leftStickY] = pad.leftThumbstick.yAxis
result[.rightStick] = pad.rightThumbstick
result[.rightStickX] = pad.rightThumbstick.xAxis
result[.rightStickY] = pad.rightThumbstick.yAxis
// Menu / Options / Share // Menu / Options / Share
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
result[.menu] = pad.buttonMenu result[.start] = pad.buttonMenu
if let options = pad.buttonOptions { result[.back] = options } if let options = pad.buttonOptions { result[.select] = options }
} }
if #available(iOS 14.0, tvOS 14.0, *) { if #available(iOS 14.0, tvOS 14.0, *) {
@@ -163,49 +359,185 @@ import Foundation
} }
} }
} }
if controller === primaryGCController {ControllerUtil.primaryControllerElementMap = result}
return result return result
} }
@objc static var activeStreamingGCControllers:NSMutableSet = NSMutableSet() @objc static var activeStreamingGCControllers:NSMutableSet = NSMutableSet()
@objc static func string(for button: ControllerButton) -> String { @objc static var swapABXY:Bool = false
switch button {
case .a: return "A" @objc static func string(for element: ControllerElement) -> String {
case .b: return "B" return element.displayName
case .x: return "X" }
case .y: return "Y"
@objc static func position(for element: ControllerElement) -> ControllerElementPosition {
case .dpadUp: return LocalizationHelper.localizedString(forKey: "Up") return element.position
case .dpadDown: return LocalizationHelper.localizedString(forKey: "Down") }
case .dpadLeft: return LocalizationHelper.localizedString(forKey: "Left")
case .dpadRight: return LocalizationHelper.localizedString(forKey: "Right")
// MARK: - primary controller section
case .leftShoulder: return "LB"
case .rightShoulder: return "RB" private static var connectObserver: NSObjectProtocol?
private static var disconnectObserver: NSObjectProtocol?
case .leftStickButton: return "LS"
case .rightStickButton: return "RS" @objc static weak var primaryGCController: GCController?
static var primaryControllerElementMap: [ControllerElement : GCControllerElement]?
case .menu: return "Menu" private static var primaryControllerAxisStates: [ControllerElement : ComparisonResult] = [:]
case .back: return "Back"
case .special: return "Home" @objc static func installControllerObserversIfNeeded() {
guard connectObserver == nil, disconnectObserver == nil else { return }
case .paddle1: return LocalizationHelper.localizedString(forKey: "Paddle1")
case .paddle2: return LocalizationHelper.localizedString(forKey: "Paddle2") if GCController.controllers().first(where: { $0.extendedGamepad != nil }) != nil {
case .paddle3: return LocalizationHelper.localizedString(forKey: "Paddle3") preparePrimaryController()
case .paddle4: return LocalizationHelper.localizedString(forKey: "Paddle4") }
case .touchpadButton: return LocalizationHelper.localizedString(forKey: "Touch button")
case .misc: return "Misc" connectObserver = NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
case .leftTrigger: return "LT" object: nil,
case .rightTrigger: return "RT" queue: .main
) { notification in
case .null: return LocalizationHelper.localizedString(forKey: "Null") guard (notification.object as? GCController)?.extendedGamepad != nil else { return }
preparePrimaryController()
default: return "UNKNOWN" }
disconnectObserver = NotificationCenter.default.addObserver(
forName: .GCControllerDidDisconnect,
object: nil,
queue: .main
) { notification in
preparePrimaryController()
}
}
@objc static func removeControllerObservers() {
if let connectObserver {
NotificationCenter.default.removeObserver(connectObserver)
self.connectObserver = nil
}
if let disconnectObserver {
NotificationCenter.default.removeObserver(disconnectObserver)
self.disconnectObserver = nil
} }
} }
@objc static func disableSysGestures(_ controller: GCController?){
if let controller = controller {
if #available(iOS 14.0, *) {
for element in controller.physicalInputProfile.allElements {
element.preferredSystemGestureState = .disabled
}
}
}
}
private static func preparePrimaryController() {
guard let controller = GCController.controllers().first(where: { $0.extendedGamepad != nil }) else {
stopListeningPrimaryController()
ControllerUtil.primaryGCController = nil
if #available(iOS 13.0, *) {
ControllerNavigator.stop()
}
return
}
guard GCController.controllers().count == 1 else { return }
primaryGCController = controller
_ = buildMapping(for: controller, swapABXY: false)
disableSysGestures(controller)
if #available(iOS 13.0, *) {
if ControllerNavigator.enabled {
ControllerNavigator.start()
GamepadNavigationIllustrationHud.updateNavigationElements(ControllerNavigator.uiNavigationDelegate?.getNavigationElements() ?? [])
ControllerNavigator.restoreUINavigationHighlight()
}
}
}
@objc static func stopListeningPrimaryController(stopListenToRadialMenuButton: Bool = false) {
guard let primaryControllerButtonMap = primaryControllerElementMap else { return }
for gcElement in primaryControllerButtonMap.values {
if let gcButton = gcElement as? GCControllerButtonInput {
if #available(iOS 13.0, *) {
if !stopListenToRadialMenuButton, gcButton === primaryControllerButtonMap[ControllerNavigator.radialMenuButton] as? GCControllerButtonInput {continue}
}
gcButton.pressedChangedHandler = nil
}
if let gcStick = gcElement as? GCControllerDirectionPad {
gcStick.valueChangedHandler = nil
}
if let gcAxis = gcElement as? GCControllerAxisInput {
gcAxis.valueChangedHandler = nil
}
}
primaryControllerAxisStates.removeAll()
primaryGCController?.extendedGamepad?.valueChangedHandler = nil
}
@objc static func listenPrimaryControllerButton(_ button: ControllerElement, ignoreSwapABXY: Bool = true, handler: @escaping (_ pressed: Bool) -> Void){
if var elementMap = ControllerUtil.primaryControllerElementMap {
if ignoreSwapABXY {
elementMap[.a] = ControllerUtil.primaryGCController?.extendedGamepad?.buttonA
elementMap[.b] = ControllerUtil.primaryGCController?.extendedGamepad?.buttonB
elementMap[.x] = ControllerUtil.primaryGCController?.extendedGamepad?.buttonX
elementMap[.y] = ControllerUtil.primaryGCController?.extendedGamepad?.buttonY
}
if let gcButton = elementMap[button] as? GCControllerButtonInput {
gcButton.pressedChangedHandler = { _, _, pressed in
handler(pressed)
}
}
}
}
@objc static func listenPrimaryControllerStick(_ stick: ControllerElement, handler: @escaping (_ offsetVector: CGVector) -> Void){
if let elementMap = ControllerUtil.primaryControllerElementMap {
if let gcStick = elementMap[stick] as? GCControllerDirectionPad {
gcStick.valueChangedHandler = { _, xValue, yValue in
handler(CGVector(dx: CGFloat(xValue), dy: CGFloat(yValue)))
}
}
}
}
@objc static func listenPrimaryControllerStickAxis(_ stickAxis: ControllerElement, threshold: CGFloat, handler: @escaping (_ state: ComparisonResult) -> Void) {
guard threshold > 0 else { return }
guard let elementMap = ControllerUtil.primaryControllerElementMap,
let gcAxis = elementMap[stickAxis] as? GCControllerAxisInput else { return }
primaryControllerAxisStates[stickAxis] = stickAxisState(for: CGFloat(gcAxis.value), threshold: threshold)
gcAxis.valueChangedHandler = { _, value in
let newState = stickAxisState(for: CGFloat(value), threshold: threshold)
let previousState = primaryControllerAxisStates[stickAxis] ?? .orderedSame
guard newState != previousState else { return }
primaryControllerAxisStates[stickAxis] = newState
handler(newState)
}
}
private static func stickAxisState(for value: CGFloat, threshold: CGFloat) -> ComparisonResult {
if value > threshold {
return .orderedDescending
}
if value < -threshold {
return .orderedAscending
}
return .orderedSame
}
// MARK: - input processing
@objc static func compensated(offsetVector: CGVector, minOffset: CGFloat, circulate:Bool=false) -> CGVector{ @objc static func compensated(offsetVector: CGVector, minOffset: CGFloat, circulate:Bool=false) -> CGVector{
let vectorHypot = hypot(offsetVector.dx, offsetVector.dy) let vectorHypot = hypot(offsetVector.dx, offsetVector.dy)
guard vectorHypot > 0 else {return CGVector(dx: 0, dy: 0)} guard vectorHypot > 0 else {return CGVector(dx: 0, dy: 0)}
@@ -233,9 +565,11 @@ import Foundation
} }
} }
// MARK: - realtime overlay
@available(iOS 13.0, *) @available(iOS 13.0, *)
struct GamepadOverlaySnapshot: Equatable { struct GamepadOverlaySnapshot: Equatable {
var pressedButtons: Set<ControllerButton> = [] var pressedButtons: Set<ControllerElement> = []
var dpadHighlight: Int = 0 var dpadHighlight: Int = 0
var leftStick: CGPoint = .zero var leftStick: CGPoint = .zero
var rightStick: CGPoint = .zero var rightStick: CGPoint = .zero
@@ -247,7 +581,7 @@ struct GamepadOverlaySnapshot: Equatable {
init() {} init() {}
init(gamepad: GCExtendedGamepad) { init(gamepad: GCExtendedGamepad) {
var pressedButtons = Set<ControllerButton>() var pressedButtons = Set<ControllerElement>()
if gamepad.buttonA.isPressed { pressedButtons.insert(.a) } if gamepad.buttonA.isPressed { pressedButtons.insert(.a) }
if gamepad.buttonB.isPressed { pressedButtons.insert(.b) } if gamepad.buttonB.isPressed { pressedButtons.insert(.b) }
@@ -255,8 +589,8 @@ struct GamepadOverlaySnapshot: Equatable {
if gamepad.buttonY.isPressed { pressedButtons.insert(.y) } if gamepad.buttonY.isPressed { pressedButtons.insert(.y) }
if gamepad.leftShoulder.isPressed { pressedButtons.insert(.leftShoulder) } if gamepad.leftShoulder.isPressed { pressedButtons.insert(.leftShoulder) }
if gamepad.rightShoulder.isPressed { pressedButtons.insert(.rightShoulder) } if gamepad.rightShoulder.isPressed { pressedButtons.insert(.rightShoulder) }
if gamepad.buttonMenu.isPressed { pressedButtons.insert(.menu) } if gamepad.buttonMenu.isPressed { pressedButtons.insert(.start) }
if gamepad.buttonOptions?.isPressed == true { pressedButtons.insert(.back) } if gamepad.buttonOptions?.isPressed == true { pressedButtons.insert(.select) }
if #available(iOS 14.0, *), gamepad.buttonHome?.isPressed == true { pressedButtons.insert(.special) } if #available(iOS 14.0, *), gamepad.buttonHome?.isPressed == true { pressedButtons.insert(.special) }
if gamepad.leftThumbstickButton?.isPressed == true { pressedButtons.insert(.leftStickButton) } if gamepad.leftThumbstickButton?.isPressed == true { pressedButtons.insert(.leftStickButton) }
if gamepad.rightThumbstickButton?.isPressed == true { pressedButtons.insert(.rightStickButton) } if gamepad.rightThumbstickButton?.isPressed == true { pressedButtons.insert(.rightStickButton) }
+2 -2
View File
@@ -121,8 +121,8 @@
self.physicalRightStickMinOffset = [decoder containsValueForKey:@"physicalRightStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalRightStickMinOffset"] : 0; self.physicalRightStickMinOffset = [decoder containsValueForKey:@"physicalRightStickMinOffset"] ? [decoder decodeDoubleForKey:@"physicalRightStickMinOffset"] : 0;
self.controllerGyroSwitchMode = [decoder containsValueForKey:@"controllerGyroSwitchMode"] ? [decoder decodeIntForKey:@"controllerGyroSwitchMode"] : ControllerGyroSwitchDisabled; self.controllerGyroSwitchMode = [decoder containsValueForKey:@"controllerGyroSwitchMode"] ? [decoder decodeIntForKey:@"controllerGyroSwitchMode"] : ControllerGyroSwitchDisabled;
self.reverseGyroHoldButton = [decoder containsValueForKey:@"reverseGyroHoldButton"] ? [decoder decodeBoolForKey:@"reverseGyroHoldButton"] : false; self.reverseGyroHoldButton = [decoder containsValueForKey:@"reverseGyroHoldButton"] ? [decoder decodeBoolForKey:@"reverseGyroHoldButton"] : false;
self.controllerGyroSwitchHold = [decoder containsValueForKey:@"controllerGyroSwitchHold"] ? [decoder decodeIntForKey:@"controllerGyroSwitchHold"] : ControllerButtonNull; self.controllerGyroSwitchHold = [decoder containsValueForKey:@"controllerGyroSwitchHold"] ? [decoder decodeIntForKey:@"controllerGyroSwitchHold"] : ControllerElementNull;
self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerButtonNull; self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerElementNull;
self.touchMode = [decoder containsValueForKey:@"touchMode"] ? [decoder decodeIntForKey:@"touchMode"] : NativeTouch; self.touchMode = [decoder containsValueForKey:@"touchMode"] ? [decoder decodeIntForKey:@"touchMode"] : NativeTouch;
if(self.touchMode == NativeTouchOnly) self.touchMode = NativeTouch; if(self.touchMode == NativeTouchOnly) self.touchMode = NativeTouch;
@@ -329,7 +329,7 @@ class OSCProfilesManager: NSObject {
return profiles[Int(selectedIndex)] as! OSCProfile return profiles[Int(selectedIndex)] as! OSCProfile
return (profiles.firstObject as? OSCProfile) ?? OSCProfile(name: "", buttonStates: NSMutableArray(), isSelected: false) // return (profiles.firstObject as? OSCProfile) ?? OSCProfile(name: "", buttonStates: NSMutableArray(), isSelected: false)
} }
func getIndexOfSelectedProfile() -> Int { func getIndexOfSelectedProfile() -> Int {
@@ -401,8 +401,8 @@ class OSCProfilesManager: NSObject {
normalizedPosition.x = position.x / Self.layoutViewBounds.size.width normalizedPosition.x = position.x / Self.layoutViewBounds.size.width
normalizedPosition.y = position.y / Self.layoutViewBounds.size.height normalizedPosition.y = position.y / Self.layoutViewBounds.size.height
} }
NSLog("layoutToolView bounds: %f, %f", Self.layoutViewBounds.size.width, Self.layoutViewBounds.size.height) // NSLog("layoutToolView bounds: %f, %f", Self.layoutViewBounds.size.width, Self.layoutViewBounds.size.height)
NSLog("position: %f, %f, denormalized position: %f, %f", normalizedPosition.x, normalizedPosition.y, originalPosition.x, originalPosition.y) // NSLog("position: %f, %f, denormalized position: %f, %f", normalizedPosition.x, normalizedPosition.y, originalPosition.x, originalPosition.y)
return normalizedPosition return normalizedPosition
} }
-1
View File
@@ -151,7 +151,6 @@ import CoreMotion
self.widgetYawFactor = yawFactor self.widgetYawFactor = yawFactor
self.widgetPitchFactor = pitchFactor self.widgetPitchFactor = pitchFactor
self.widgetRollFactor = rollFactor self.widgetRollFactor = rollFactor
print("self.widgetRollFactor \(self.widgetRollFactor)")
if self.motionStarter == nil { if self.motionStarter == nil {
self.motionStarter = sender self.motionStarter = sender
if sender.motionControlButtonString != "GYROPAUSE" {self.startMotionUpdate()} if sender.motionControlButtonString != "GYROPAUSE" {self.startMotionUpdate()}
+1
View File
@@ -11,6 +11,7 @@
#import "NativeTouchPointer.h" #import "NativeTouchPointer.h"
#import "OnScreenControls.h" #import "OnScreenControls.h"
#import "StreamView.h" #import "StreamView.h"
#import "VoidLink-Swift.h"
#include <Limelight.h> #include <Limelight.h>
+4 -4
View File
@@ -59,13 +59,11 @@ import ObjectiveC.runtime
func openWidgetProfileTable(pickProfile: Bool) func openWidgetProfileTable(pickProfile: Bool)
func bringUpSoftKeyboard() func bringUpSoftKeyboard()
func alterAbsTouchDragWith(mouseButton:Int32) func alterAbsTouchDragWith(mouseButton:Int32)
func switchPencilHover()
func enablePencilHover() func enablePencilHover()
func disablePencilHover() func disablePencilHover()
func setAllowSingleTouchEnabled(_ enabled:Bool) func setAllowSingleTouchEnabled(_ enabled:Bool)
func replaceBrush(shortcut:String) func replaceBrush(shortcut:String)
func replaceEraser(shortcut:String) func replaceEraser(shortcut:String)
func setPencilTilt(disabled:Bool)
func presentPressureCurveVC() func presentPressureCurveVC()
func toggleTouch(disabled:Bool) func toggleTouch(disabled:Bool)
func toggleGamepadOverlay(overlayEnabled:Bool) func toggleGamepadOverlay(overlayEnabled:Bool)
@@ -1550,7 +1548,7 @@ import ObjectiveC.runtime
private func sendLongMouseLeftButtonClickEvent() { private func sendLongMouseLeftButtonClickEvent() {
DispatchQueue.global(qos: .userInteractive).async { DispatchQueue.global(qos: .userInteractive).async {
// Logging the press event // Logging the press event
NSLog("Sending left mouse button press") // NSLog("Sending left mouse button press")
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT) LiSendMouseButtonEvent(CChar(BUTTON_ACTION_PRESS), BUTTON_LEFT)
// Wait 200 ms to simulate a real button press // Wait 200 ms to simulate a real button press
@@ -1560,7 +1558,9 @@ import ObjectiveC.runtime
LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT) LiSendMouseButtonEvent(CChar(BUTTON_ACTION_RELEASE), BUTTON_LEFT)
// NSLog("double click: first long click release") // NSLog("double click: first long click release")
} }
else{NSLog("Left mouse button release cancelled, keep pressing down, turning into dragging...")} else{
// NSLog("Left mouse button release cancelled, keep pressing down, turning into dragging...")
}
// Don't release the button if we're still dragging, this will prevent the dragging from being interrupted. // Don't release the button if we're still dragging, this will prevent the dragging from being interrupted.
} }
} }
+1 -1
View File
@@ -11,6 +11,7 @@
#import "NativeTouchPointer.h" #import "NativeTouchPointer.h"
#import "OnScreenControls.h" #import "OnScreenControls.h"
#import "StreamView.h" #import "StreamView.h"
#import "VoidLink-Swift.h"
#include <Limelight.h> #include <Limelight.h>
@@ -337,4 +338,3 @@
} }
@end @end
+114 -16
View File
@@ -2,7 +2,7 @@
// RadialMenuOverlayView.swift // RadialMenuOverlayView.swift
// VoidLink // VoidLink
// //
// Created by Codex on 2026/7/1. // Created by True砖家 on 2026/7/1.
// Copyright © 2026 True砖家 on Bilibili. All rights reserved. // Copyright © 2026 True砖家 on Bilibili. All rights reserved.
// //
@@ -11,60 +11,91 @@ import UIKit
@available(iOS 13.0, *) @available(iOS 13.0, *)
final class RadialMenuOverlayView: UIView { final class RadialMenuOverlayView: UIView {
func getNavigationElements() -> [ControllerNavigationElement] {
var elements: [ControllerNavigationElement] = []
elements.append(ControllerNavigationElement(control: ControllerNavigator.radialMenuButtonPosition == .right ? .leftStick : .rightStick, action: "focusNavigation"))
return elements
}
private enum Metrics { private enum Metrics {
static let diameter: CGFloat = 260 static let diameter: CGFloat = PublicUtils.isIPhone ? 200 : 260
static let bottomInset: CGFloat = 92 static let releaseDistanceEpsilon: CGFloat = 0.015
} }
private let selectionState = RadialMenuSelectionState() private let selectionState = RadialMenuSelectionState()
private var hostingController: UIHostingController<RadialMenuView>? private var hostingController: UIHostingController<RadialMenuView>?
private var themeObserver: NSObjectProtocol?
private var previousJoystickDistance: CGFloat?
private var isJoystickReturningToCenter = false
private var releaseLatchedSelectedIndex: Int?
static var menuSectors: [RadialMenuSector] = [] static var menuSectors: [RadialMenuSector] = []
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
setupView() setupView()
observeThemeChanges()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
setupView() setupView()
observeThemeChanges()
}
deinit {
removeThemeObserver()
} }
static func presentInKeyWindow() -> RadialMenuOverlayView? { static func presentInKeyWindow() -> RadialMenuOverlayView? {
guard let window = keyWindow() else { return nil } guard let window = keyWindow() else { return nil }
let overlayView = RadialMenuOverlayView() let overlayView = RadialMenuOverlayView()
overlayView.translatesAutoresizingMaskIntoConstraints = false overlayView.translatesAutoresizingMaskIntoConstraints = false
CATransaction.begin()
CATransaction.setDisableActions(true)
window.addSubview(overlayView) window.addSubview(overlayView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
overlayView.widthAnchor.constraint(equalToConstant: Metrics.diameter), overlayView.widthAnchor.constraint(equalToConstant: Metrics.diameter),
overlayView.heightAnchor.constraint(equalToConstant: Metrics.diameter), overlayView.heightAnchor.constraint(equalToConstant: Metrics.diameter),
overlayView.centerXAnchor.constraint(equalTo: window.centerXAnchor), overlayView.centerXAnchor.constraint(equalTo: window.centerXAnchor),
overlayView.bottomAnchor.constraint(equalTo: window.safeAreaLayoutGuide.bottomAnchor, constant: -Metrics.bottomInset) overlayView.centerYAnchor.constraint(equalTo: window.centerYAnchor)
]) ])
CATransaction.commit()
return overlayView return overlayView
} }
func updateSelection(xOffset: Float, yOffset: Float) -> RadialMenuItem? { func updateSelection(xOffset: CGFloat, yOffset: CGFloat) -> RadialMenuItem? {
guard Thread.isMainThread else {
return DispatchQueue.main.sync {
self.updateSelection(xOffset: xOffset, yOffset: yOffset)
}
}
let previousSelectedIndex = selectionState.selectedIndex let previousSelectedIndex = selectionState.selectedIndex
latchSelectionIfNeeded(
distance: Self.normalizedDistance(xOffset: xOffset, yOffset: yOffset),
selectedIndex: previousSelectedIndex
)
selectionState.updateSelection(xOffset: xOffset, yOffset: yOffset) selectionState.updateSelection(xOffset: xOffset, yOffset: yOffset)
guard previousSelectedIndex != nil, selectionState.selectedIndex == nil else { guard previousSelectedIndex != nil, selectionState.selectedIndex == nil else {
return nil return nil
} }
guard let previousSelectedIndex, let actionSelectedIndex = releaseLatchedSelectedIndex ?? previousSelectedIndex
Self.menuSectors.indices.contains(previousSelectedIndex) else { resetReleaseLatch()
guard let actionSelectedIndex,
Self.menuSectors.indices.contains(actionSelectedIndex) else {
return nil return nil
} }
return Self.menuSectors[previousSelectedIndex].item return Self.menuSectors[actionSelectedIndex].item
} }
func dismiss() { func dismiss() {
removeThemeObserver()
hostingController?.willMove(toParent: nil) hostingController?.willMove(toParent: nil)
hostingController?.view.removeFromSuperview() hostingController?.view.removeFromSuperview()
hostingController?.removeFromParent() hostingController?.removeFromParent()
@@ -77,11 +108,7 @@ final class RadialMenuOverlayView: UIView {
isUserInteractionEnabled = false isUserInteractionEnabled = false
clipsToBounds = false clipsToBounds = false
let radialMenuView = RadialMenuView( let radialMenuView = makeRadialMenuView()
sectors: Self.menuSectors,
isTouchSelectionEnabled: false,
selectionState: selectionState
)
let hostingController = UIHostingController(rootView: radialMenuView) let hostingController = UIHostingController(rootView: radialMenuView)
hostingController.view.backgroundColor = .clear hostingController.view.backgroundColor = .clear
@@ -97,9 +124,80 @@ final class RadialMenuOverlayView: UIView {
hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor), hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor) hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor)
]) ])
GamepadNavigationIllustrationHud.updateNavigationElements(self.getNavigationElements())
}
private func observeThemeChanges() {
themeObserver = NotificationCenter.default.addObserver(
forName: Notification.Name(ThemeManager.ThemeDidChangeNotification),
object: nil,
queue: .main
) { [weak self] _ in
self?.refreshTheme()
}
}
private func removeThemeObserver() {
if let themeObserver {
NotificationCenter.default.removeObserver(themeObserver)
self.themeObserver = nil
}
}
private func refreshTheme() {
hostingController?.rootView = makeRadialMenuView()
}
private func latchSelectionIfNeeded(distance: CGFloat, selectedIndex: Int?) {
defer { previousJoystickDistance = distance }
guard let previousJoystickDistance else {
return
}
if distance + Metrics.releaseDistanceEpsilon < previousJoystickDistance {
if !isJoystickReturningToCenter {
releaseLatchedSelectedIndex = selectedIndex
isJoystickReturningToCenter = true
}
} else if distance > previousJoystickDistance + Metrics.releaseDistanceEpsilon {
resetReleaseLatch(keepingPreviousDistance: true)
}
}
private func resetReleaseLatch(keepingPreviousDistance: Bool = false) {
if !keepingPreviousDistance {
previousJoystickDistance = nil
}
isJoystickReturningToCenter = false
releaseLatchedSelectedIndex = nil
}
private func makeRadialMenuView() -> RadialMenuView {
RadialMenuView(
sectors: Self.menuSectors,
isTouchSelectionEnabled: false,
style: RadialMenuStyle.themed(
for: ThemeManager.userInterfaceStyle(),
accentColor: ThemeManager.appPrimaryColor
),
selectionState: selectionState
)
} }
private static func keyWindow() -> UIWindow? { private static func keyWindow() -> UIWindow? {
UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
} }
private static func normalizedDistance(xOffset: CGFloat, yOffset: CGFloat) -> CGFloat {
let x = max(min(xOffset, 1), -1)
let y = max(min(yOffset, 1), -1)
return hypot(x, y)
}
func navigateByController(forward: Bool) {}
func navigateByController(downward: Bool) {}
func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) {}
func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) {}
} }
+1
View File
@@ -8,6 +8,7 @@
#import "RelativeTouchHandler.h" #import "RelativeTouchHandler.h"
#import "DataManager.h" #import "DataManager.h"
#import "VoidLink-Swift.h"
#include <Limelight.h> #include <Limelight.h>
+62 -11
View File
@@ -13,12 +13,15 @@ import QuartzCore
final class SafeTimer: NSObject { final class SafeTimer: NSObject {
private var timer: DispatchSourceTimer? private var timer: DispatchSourceTimer?
private let timerQueue: DispatchQueue // 私有串行队列 private let timerQueue: DispatchQueue // 私有串行队列
private let timerQueueKey = DispatchSpecificKey<UInt8>()
private let userHandler: () -> Void private let userHandler: () -> Void
private let interval: TimeInterval private let interval: TimeInterval
private let delay: TimeInterval private let delay: TimeInterval
private var shouldRunHandler: Bool = false // 控制逻辑上的暂停/恢复 private var shouldRunHandler: Bool = false // 控制逻辑上的暂停/恢复
private var isCleaned: Bool = false // 是否已经 clean private var isCleaned: Bool = false // 是否已经 clean
@objc private(set) var remainingMinimumRunCount: Int = 0
private var shouldCleanAfterMinimumRuns: Bool = false
@objc init(interval: TimeInterval = 1.0, @objc init(interval: TimeInterval = 1.0,
delay: TimeInterval = 0, delay: TimeInterval = 0,
@@ -29,6 +32,7 @@ final class SafeTimer: NSObject {
self.delay = delay self.delay = delay
self.userHandler = handler self.userHandler = handler
self.timerQueue = DispatchQueue(label: queueLabel) self.timerQueue = DispatchQueue(label: queueLabel)
self.timerQueue.setSpecific(key: timerQueueKey, value: 1)
super.init() super.init()
@@ -39,6 +43,12 @@ final class SafeTimer: NSObject {
guard let self = self, !self.isCleaned else { return } guard let self = self, !self.isCleaned else { return }
if !self.shouldRunHandler { return } if !self.shouldRunHandler { return }
self.userHandler() self.userHandler()
if self.remainingMinimumRunCount > 0 {
self.remainingMinimumRunCount -= 1
}
if self.shouldCleanAfterMinimumRuns && self.remainingMinimumRunCount == 0 {
self.cleanOnTimerQueue(force: true)
}
} }
t.resume() // timer 一开始就 resume,避免 suspend/resume 的计数问题 t.resume() // timer 一开始就 resume,避免 suspend/resume 的计数问题
@@ -46,7 +56,7 @@ final class SafeTimer: NSObject {
} }
deinit { deinit {
clean() cleanForDeinit()
} }
/// 开始逻辑上的计时 /// 开始逻辑上的计时
@@ -67,30 +77,71 @@ final class SafeTimer: NSObject {
/// 重置下一次触发时间,并开始执行 handler /// 重置下一次触发时间,并开始执行 handler
@objc func restart() { @objc func restart() {
restart(minimumRunCount: 1)
}
/// 重置下一次触发时间,并保证 clean 前至少执行 minimumRunCount 次 handler
@objc(restartWithMinimumRunCount:)
func restart(minimumRunCount: Int = 1) {
timerQueue.async { [weak self] in timerQueue.async { [weak self] in
guard let self = self, let t = self.timer, !self.isCleaned else { return } guard let self = self, let t = self.timer, !self.isCleaned else { return }
self.remainingMinimumRunCount = max(0, minimumRunCount)
self.shouldCleanAfterMinimumRuns = false
t.schedule(deadline: .now() + self.delay, repeating: self.interval) t.schedule(deadline: .now() + self.delay, repeating: self.interval)
self.shouldRunHandler = true self.shouldRunHandler = true
} }
} }
@objc func isRunning() -> Bool { @objc func isRunning() -> Bool {
return shouldRunHandler; if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
return shouldRunHandler
}
return timerQueue.sync {
shouldRunHandler && !isCleaned
}
} }
/// 彻底清理 timer,返回后保证 handler 不再执行 /// 彻底清理 timer;如果 restart(minimumRunCount:) 尚未跑够次数,则延后到最低次数满足后清理
@objc func clean() { @objc func clean() {
if isCleaned { return } if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
cleanOnTimerQueue()
return
}
// 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过) // 同步在 timerQueue 执行,保证已排队事件全部处理完(或跳过)
timerQueue.sync { timerQueue.sync {
self.isCleaned = true cleanOnTimerQueue()
self.shouldRunHandler = false }
}
if let t = self.timer { private func cleanForDeinit() {
t.setEventHandler {} // 清空 handler if DispatchQueue.getSpecific(key: timerQueueKey) != nil {
t.cancel() // 取消 timer cleanOnTimerQueue(force: true)
self.timer = nil return
} }
timerQueue.sync {
cleanOnTimerQueue(force: true)
}
}
private func cleanOnTimerQueue(force: Bool = false) {
guard !isCleaned else { return }
if !force && shouldRunHandler && remainingMinimumRunCount > 0 {
shouldCleanAfterMinimumRuns = true
return
}
isCleaned = true
shouldRunHandler = false
remainingMinimumRunCount = 0
shouldCleanAfterMinimumRuns = false
if let t = timer {
t.setEventHandler {} // 清空 handler
t.cancel() // 取消 timer
timer = nil
} }
} }
} }
+3 -6
View File
@@ -13,9 +13,10 @@
#import "ControllerSupport.h" #import "ControllerSupport.h"
#import "OnScreenControls.h" #import "OnScreenControls.h"
#import "VoidLink-Swift.h"
#import "StreamConfiguration.h" #import "StreamConfiguration.h"
@class PencilHandler;
@protocol UserInteractionDelegate <NSObject> @protocol UserInteractionDelegate <NSObject>
- (void)userInteractionBegan; - (void)userInteractionBegan;
@@ -29,11 +30,7 @@
@end @end
#if TARGET_OS_TV @interface StreamView : UIView
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
#else
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate, InputAccessoryBarDelegate>
#endif
@property (weak, nonatomic) UIView* streamFrameTopLayerView; @property (weak, nonatomic) UIView* streamFrameTopLayerView;
@property (weak, nonatomic) id<UserInteractionDelegate> interactionDelegate; @property (weak, nonatomic) id<UserInteractionDelegate> interactionDelegate;
+5 -1
View File
@@ -28,7 +28,11 @@
#import "StreamFrameViewController.h" #import "StreamFrameViewController.h"
@interface StreamView() #if TARGET_OS_TV
@interface StreamView() <X1KitMouseDelegate, UITextFieldDelegate>
#else
@interface StreamView() <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate, InputAccessoryBarDelegate>
#endif
@property (weak, nonatomic) StreamFrameViewController* streamFrameVC; @property (weak, nonatomic) StreamFrameViewController* streamFrameVC;
@end @end
@@ -10,6 +10,7 @@
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class"> <entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="activeAddressPool" optional="YES" attributeType="Binary" syncable="YES"/> <attribute name="activeAddressPool" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/> <attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="controllerNavigationHighlightedAppID" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/> <attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/> <attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/> <attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
@@ -40,17 +41,19 @@
<attribute name="controllerGyroBiasY" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/> <attribute name="controllerGyroBiasY" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerGyroBiasZ" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/> <attribute name="controllerGyroBiasZ" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseExpo" optional="YES" attributeType="Float" defaultValueString="1.6" usesScalarValueType="NO" syncable="YES"/> <attribute name="controllerMouseExpo" optional="YES" attributeType="Float" defaultValueString="1.6" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseLeftButton" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/> <attribute name="controllerMouseLeftButton" attributeType="Integer 32" defaultValueString="8" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMousePointerVelocity" optional="YES" attributeType="Float" defaultValueString="12" usesScalarValueType="NO" syncable="YES"/> <attribute name="controllerMousePointerVelocity" optional="YES" attributeType="Float" defaultValueString="12" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/> <attribute name="controllerMouseRightButton" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseStick" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/> <attribute name="controllerMouseStick" attributeType="Integer 32" defaultValueString="8388640" usesScalarValueType="NO" syncable="YES"/>
<attribute name="controllerMouseSwitch" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
<attribute name="ctrlDownForPinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/> <attribute name="ctrlDownForPinch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="customLocalRadialMenuButtonPosition" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
<attribute name="customStreamingRadialMenuButtonPosition" attributeType="Integer 32" defaultValueString="16777215" usesScalarValueType="NO" syncable="YES"/>
<attribute name="delayLeftClick" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/> <attribute name="delayLeftClick" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/> <attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="duckOtherApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="duckOtherApps" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="edgeSlidingSensitivity" optional="YES" attributeType="Float" defaultValueString="22" usesScalarValueType="NO" syncable="YES"/> <attribute name="edgeSlidingSensitivity" optional="YES" attributeType="Float" defaultValueString="22" usesScalarValueType="NO" syncable="YES"/>
<attribute name="emulatedControllerType" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/> <attribute name="emulatedControllerType" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="enableControllerNavigation" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableFrameTimebase" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="enableFrameTimebase" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableGraphs" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="enableGraphs" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
@@ -75,8 +78,8 @@
<attribute name="leftClickDelayMs" optional="YES" attributeType="Float" defaultValueString="15" usesScalarValueType="NO" syncable="YES"/> <attribute name="leftClickDelayMs" optional="YES" attributeType="Float" defaultValueString="15" usesScalarValueType="NO" syncable="YES"/>
<attribute name="liftStreamViewForKeyboard" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/> <attribute name="liftStreamViewForKeyboard" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="localMousePointerMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/> <attribute name="localMousePointerMode" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="localRadialMenuButton" attributeType="Integer 32" defaultValueString="256" usesScalarValueType="NO" syncable="YES"/>
<attribute name="localVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/> <attribute name="localVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="mapControllerToMouse" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="micVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/> <attribute name="micVolume" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/> <attribute name="mousePointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/> <attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
@@ -110,6 +113,8 @@
<attribute name="softKeyboardHeight" attributeType="Float" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/> <attribute name="softKeyboardHeight" attributeType="Float" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="statsOverlayEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlayLevel" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/> <attribute name="statsOverlayLevel" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="streamingRadialMenuButton" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="NO" syncable="YES"/>
<attribute name="streamingRadialMenuDelay" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/> <attribute name="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/> <attribute name="touchMoveEventInterval" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -51,7 +51,7 @@ class MenuSectionView: UIView {
private var toggleButton: UIButton! private var toggleButton: UIButton!
private var toggleArea: UIButton! private var toggleArea: UIButton!
private var heightConstraint: NSLayoutConstraint! private var heightConstraint: NSLayoutConstraint!
private var headerView: UIView! var headerView: UIView!
private var titleOffset: CGFloat = 41.5 private var titleOffset: CGFloat = 41.5
override init(frame: CGRect) { override init(frame: CGRect) {
@@ -69,8 +69,9 @@ class MenuSectionView: UIView {
layer.masksToBounds = true layer.masksToBounds = true
backgroundColor = .clear backgroundColor = .clear
headerView = UIView() headerView = UIButton()
headerView.translatesAutoresizingMaskIntoConstraints = false headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.accessibilityIdentifier = "sectionHeader"
addSubview(headerView) addSubview(headerView)
iconImageView = UIImageView() iconImageView = UIImageView()
@@ -273,6 +274,8 @@ class MenuSectionView: UIView {
persistedFoldState = true persistedFoldState = true
} }
setExpanded(MenuSectionView.overridePersistedFoldState ? expandable : persistedFoldState) setExpanded(MenuSectionView.overridePersistedFoldState ? expandable : persistedFoldState)
headerView.accessibilityIdentifier = "\(self.headerView.accessibilityIdentifier ?? "sectionHeader")-\(self.identifier ?? "")"
} }
func removeSubStackView(_ stackView: UIStackView) { func removeSubStackView(_ stackView: UIStackView) {
+1
View File
@@ -22,6 +22,7 @@
#import "HttpRequest.h" #import "HttpRequest.h"
#import "IdManager.h" #import "IdManager.h"
#import "LocalizationHelper.h" #import "LocalizationHelper.h"
#import "VoidLink-Swift.h"
#include <Limelight.h> #include <Limelight.h>
+22
View File
@@ -36,6 +36,7 @@ class ThemeManager: NSObject {
@objc class func setUserInterfaceStyle(_ style: UIUserInterfaceStyle) { @objc class func setUserInterfaceStyle(_ style: UIUserInterfaceStyle) {
_privateUserInterfaceStyle = style _privateUserInterfaceStyle = style
applyUserInterfaceStyleOverride(style)
if _userInterfaceStyle == style { if _userInterfaceStyle == style {
return return
@@ -48,6 +49,27 @@ class ThemeManager: NSObject {
object: nil object: nil
) )
} }
@objc class func applyUserInterfaceStyleOverride(_ style: UIUserInterfaceStyle) {
guard #available(iOS 13.0, *) else { return }
let applyOverride = {
let sceneWindows = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
let allWindows = sceneWindows + UIApplication.shared.windows
Set(allWindows).forEach { window in
window.overrideUserInterfaceStyle = style
}
}
if Thread.isMainThread {
applyOverride()
} else {
DispatchQueue.main.async(execute: applyOverride)
}
}
@objc static var menuBackgroundColor: UIColor { @objc static var menuBackgroundColor: UIColor {
_ = setPublicUIStyle() _ = setPublicUIStyle()
-36
View File
@@ -1,36 +0,0 @@
//
// UIAppView.h
// Moonlight
//
// Created by Diego Waxemberg on 10/22/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "TemporaryApp.h"
@protocol AppViewUpdateLoopDelegate <NSObject>
- (bool)isInAppView;
@end
@protocol AppCallback <NSObject>
- (void) appClicked:(TemporaryApp*)app view:(UIView*)view;
- (void) appLongClicked:(TemporaryApp*)app view:(UIView*)view;
@end
#if !TARGET_OS_TV
@interface UIAppView : UIButton <UIContextMenuInteractionDelegate>
#else
@interface UIAppView : UIButton
#endif
@property (nonatomic, weak) id<AppViewUpdateLoopDelegate> updateLoopDelegate; // Delegate property
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
- (void) updateAppImage;
@end
-291
View File
@@ -1,291 +0,0 @@
//
// UIAppView.m
// Moonlight
//
// Created by Diego Waxemberg on 10/22/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
// Created by True砖家 on 2025.7.20.
// Copyright © 2025 True砖家 @ Bilibili. All rights reserved.
//
#import "UIAppView.h"
#import "AppAssetManager.h"
#import "VoidLink-Swift.h"
static const float REFRESH_CYCLE = 1.0f;
@implementation UIAppView {
TemporaryApp* _app;
UILabel* _appLabel;
UIImageView* _appOverlay;
UIImageView* _appImage;
NSCache* _artCache;
id<AppCallback> _callback;
}
static UIImage* noImage;
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback {
self = [super init];
_app = app;
_callback = callback;
_artCache = cache;
self.layer.cornerRadius = 16;
if (@available(iOS 13.0, *)) self.layer.cornerCurve = kCACornerCurveContinuous;
self.clipsToBounds = YES;
// Cache the NoAppImage ourselves to avoid
// having to load it each time
if (noImage == nil) {
noImage = [UIImage imageNamed:@"NoAppImage"];
}
#if TARGET_OS_TV
self.frame = CGRectMake(0, 0, 200, 265);
#else
self.frame = CGRectMake(0, 0, 150, 200);
#endif
[self setAlpha:app.hidden ? 0.4 : 1.0];
_appImage = [[UIImageView alloc] initWithFrame:self.frame];
[_appImage setImage:noImage];
[self addSubview:_appImage];
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
// for tvOS devices and iOS prior to 13.0.
#if !TARGET_OS_TV
if (@available(iOS 13.0, *)) {
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
[self addInteraction:rightClickInteraction];
}
else
#endif
{
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(appLongClicked:)];
[self addGestureRecognizer:longPressRecognizer];
}
[self addTarget:self action:@selector(appClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
#if TARGET_OS_TV
_appImage.adjustsImageWhenAncestorFocused = YES;
#else
// Rasterizing the cell layer increases rendering performance by quite a bit
// but we want it unrasterized for tvOS where it must be scaled.
self.layer.shouldRasterize = NO;
// self.layer.rasterizationScale = [UIScreen mainScreen].scale;
if (@available(iOS 13.4.1, *)) {
// Allow the button style to change when moused over
self.pointerInteractionEnabled = YES;
}
#endif
[self updateAppImage];
return self;
}
- (void)didMoveToSuperview {
// Start our update loop when we are added to our cell
if (self.superview != nil) {
[self updateLoop];
}
}
- (void) appClicked:(UIView *)view {
[_callback appClicked:_app view:view];
}
- (void) appLongClicked:(UILongPressGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
[_callback appLongClicked:_app view:self];
}
}
#if !TARGET_OS_TV
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
configurationForMenuAtLocation:(CGPoint)location {
// We don't want to trigger the primary action at this point, so cancel
// tracking touch on this view now. This will also have the (intended)
// effect of removing the touch highlight on this view.
[self cancelTrackingWithEvent:nil];
[_callback appLongClicked:_app view:self];
return nil;
}
#endif
- (bool)isCurrentApp{
return [_app.id isEqualToString:_app.host.currentGame];
}
- (void) updateAppImage {
if (_appOverlay != nil) {
[_appOverlay removeFromSuperview];
_appOverlay = nil;
}
if (_appLabel != nil) {
[_appLabel removeFromSuperview];
_appLabel = nil;
}
BOOL noAppImage = false;
// First check the memory cache
//UIImage* appImage = [_artCache objectForKey:_app];
UIImage* appImage = nil;
// NSLog(@"appImage instance: %lu", (uintptr_t)appImage);
if (appImage == nil) {
// Next try to load from the on disk cache
appImage = [UIImage imageWithContentsOfFile:[AppAssetManager boxArtPathForApp:_app]];
if (appImage != nil) {
[_artCache setObject:appImage forKey:_app];
}
}
// [_artCache setObject:appImage forKey:_app];
if (appImage != nil) {
// This size of image might be blank image received from GameStream.
// TODO: Improve no-app image detection
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
!(appImage.size.width == 628.f && appImage.size.height == 888.f)){
// if(true){ // GFE 3.0
[_appImage setImage:appImage];
} else {
noAppImage = true;
}
} else {
noAppImage = true;
}
if([self isCurrentApp]) {
// Only create the app overlay if needed
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:23];
UIImageView* playIcon = [[UIImageView alloc] initWithImage:[[UIImage systemImageNamed:@"play.circle.fill" withConfiguration:config] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
//playIcon.tintColor = [ThemeManager.widgetBackgroundColor colorWithAlphaComponent:0.85];
playIcon.tintColor = [[UIColor blackColor] colorWithAlphaComponent:0.55];
_appOverlay = playIcon;
} else {
UIImageView* playIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"play.circle.fill"]];
playIcon.tintColor = [[UIColor blackColor] colorWithAlphaComponent:0.55];
_appOverlay = playIcon;
}
//_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
//_appOverlay.layer.shadowOffset = CGSizeMake(1, 1);
//_appOverlay.layer.shadowOpacity = 1;
//_appOverlay.layer.shadowRadius = 1.3;
//_appOverlay.contentMode = UIViewContentModeScaleAspectFit;
}
else if(noAppImage){
if (@available(iOS 13.0, *)) {
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:70];
UIImage* appIconImage = [[UIImage imageNamed:@"icon-pc-app"] imageWithConfiguration:config];
UIImageView* appIcon = [[UIImageView alloc] initWithImage:[appIconImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
//layIcon.tintColor = [ThemeManager.widgetBackgroundColor colorWithAlphaComponent:0.85];
appIcon.tintColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
_appOverlay = appIcon;
}
}
if(true) {
_appLabel = [[UILabel alloc] init];
_appLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.55];
[_appLabel setTextColor:[[UIColor whiteColor] colorWithAlphaComponent:1]];
//_appLabel.shadowColor = [UIColor blackColor];
[_appLabel setText:[_app.name isEqualToString:@"Steam Big Picture"] ? @"Steam" : _app.name];
[_appLabel setFont:[UIFont systemFontOfSize:15]];
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
[_appLabel setTextAlignment:NSTextAlignmentCenter];
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
[_appLabel setNumberOfLines:2];
_appLabel.lineBreakMode = NSLineBreakByTruncatingTail;
}
[self positionSubviews];
#if TARGET_OS_TV
[_appImage.overlayContentView addSubview:_appLabel];
[_appImage.overlayContentView addSubview:_appOverlay];
#else
[self addSubview:_appOverlay];
[self addSubview:_appLabel];
#endif
}
- (void) buttonSelected:(id)sender {
_appImage.layer.opacity = 0.5f;
}
- (void) buttonDeselected:(id)sender {
_appImage.layer.opacity = [self isCurrentApp] ? 0.75 : 1.0f;
}
- (void) positionSubviews {
// CGFloat padding = 28;
CGFloat verticalPadding = 10;
CGSize frameSize = _appImage.frame.size;
// CGPoint center = _appImage.center;
[_appLabel setFrame:CGRectMake(0, frameSize.height-43, frameSize.width, 43)];
//[_appLabel setFrame:CGRectMake(0, 0, 30, 12)];
if (_appOverlay != nil) {
_appOverlay.frame = [self isCurrentApp] ? CGRectMake(0, 0, frameSize.width / 2.39, frameSize.width / 2.39) : CGRectMake(0, 0, frameSize.width / 2, frameSize.width / 2);
_appOverlay.center = CGPointMake(frameSize.width/2, frameSize.height/2 - 2 * verticalPadding);
if([self isCurrentApp]) _appImage.layer.opacity = 0.75f;
//[_appLabel setFrame:CGRectMake(padding, _appOverlay.frame.size.height + padding, frameSize.width - 2 * padding, frameSize.height - _appOverlay.frame.size.height - 2 * padding)];
}
}
- (void) updateLoop {
// Stop immediately if the view has been detached
if (self.superview == nil || ![self.updateLoopDelegate isInAppView]) {
return;
}
// NSLog(@"appview update loop %f", CACurrentMediaTime());
// Update the app image if neccessary
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
[self updateAppImage];
}
// Show no shadow for hidden apps. Because we adjust the opacity of the
// cells for hidden apps, it makes them look bad when the shadow draws
// through the app tile.
// self.superview.layer.shadowOpacity = _app.hidden ? 0.0f : 0.5f;
self.superview.layer.shadowOpacity = 0;
// Update opacity if neccessary
[self setAlpha:_app.hidden ? 0.4 : 1.0];
// Queue the next refresh cycle
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
@end
+260
View File
@@ -0,0 +1,260 @@
//
// UIAppView.swift
// Moonlight
//
// Created by Diego Waxemberg on 10/22/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
// Created by True Zhuanjia on 2025.7.20.
// Copyright 2025 True Zhuanjia @ Bilibili. All rights reserved.
//
import UIKit
private let refreshCycle: TimeInterval = 1.0
@objc protocol AppViewUpdateLoopDelegate: AnyObject {
func isInAppView() -> Bool
}
@objc protocol AppCallback: AnyObject {
@objc(appClicked:view:)
func appClicked(_ app: TemporaryApp, view: UIView)
@objc(appLongClicked:view:)
func appLongClicked(_ app: TemporaryApp, view: UIView)
}
@objcMembers
final class UIAppView: UIButton {
weak var updateLoopDelegate: AppViewUpdateLoopDelegate?
private static var noImage: UIImage?
let app: TemporaryApp
fileprivate weak var callback: AppCallback?
private let artCache: NSCache<AnyObject, AnyObject>
private var appLabel: UILabel?
private var appOverlay: UIImageView?
private var appImage: UIImageView
private var contextMenuDelegate: AnyObject?
@objc(initWithApp:cache:andCallback:)
init(app: TemporaryApp, cache: NSCache<AnyObject, AnyObject>, andCallback callback: AppCallback) {
self.app = app
self.callback = callback
self.artCache = cache
if UIAppView.noImage == nil {
UIAppView.noImage = UIImage(named: "NoAppImage")
}
#if os(tvOS)
let initialFrame = CGRect(x: 0, y: 0, width: 200, height: 265)
#else
let initialFrame = CGRect(x: 0, y: 0, width: 150, height: 200)
#endif
self.appImage = UIImageView(frame: initialFrame)
super.init(frame: .zero)
layer.cornerRadius = 16
if #available(iOS 13.0, tvOS 13.0, *) {
layer.cornerCurve = .continuous
}
clipsToBounds = true
frame = initialFrame
alpha = app.hidden ? 0.4 : 1.0
appImage.image = UIAppView.noImage
addSubview(appImage)
#if !os(tvOS)
if #available(iOS 13.0, *) {
let delegate = UIAppViewContextMenuDelegate(appView: self)
contextMenuDelegate = delegate
let rightClickInteraction = UIContextMenuInteraction(delegate: delegate)
addInteraction(rightClickInteraction)
} else {
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(appLongClicked(_:)))
addGestureRecognizer(longPressRecognizer)
}
#else
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(appLongClicked(_:)))
addGestureRecognizer(longPressRecognizer)
#endif
addTarget(self, action: #selector(appClicked(_:)), for: .primaryActionTriggered)
addTarget(self, action: #selector(buttonSelected(_:)), for: .touchDown)
addTarget(self, action: #selector(buttonDeselected(_:)), for: [.touchUpInside, .touchCancel, .touchDragExit])
#if os(tvOS)
appImage.adjustsImageWhenAncestorFocused = true
#else
layer.shouldRasterize = false
if #available(iOS 13.4, *) {
isPointerInteractionEnabled = true
}
#endif
updateAppImage()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
if superview != nil {
updateLoop()
}
}
@objc private func appClicked(_ view: UIView) {
callback?.appClicked(app, view: view)
}
@objc private func appLongClicked(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
callback?.appLongClicked(app, view: self)
}
}
private var isCurrentApp: Bool {
app.id == app.host?.currentGame
}
func updateAppImage() {
appOverlay?.removeFromSuperview()
appOverlay = nil
appLabel?.removeFromSuperview()
appLabel = nil
var noAppImage = false
var loadedAppImage: UIImage?
if let boxArtPath = AppAssetManager.boxArtPath(for: app) {
loadedAppImage = UIImage(contentsOfFile: boxArtPath)
if let loadedAppImage {
artCache.setObject(loadedAppImage, forKey: app)
}
}
if let loadedAppImage {
let isGFE2BlankImage = loadedAppImage.size.width == 130.0 && loadedAppImage.size.height == 180.0
let isGFE3BlankImage = loadedAppImage.size.width == 628.0 && loadedAppImage.size.height == 888.0
if !isGFE2BlankImage && !isGFE3BlankImage {
appImage.image = loadedAppImage
} else {
noAppImage = true
}
} else {
noAppImage = true
}
if isCurrentApp {
if #available(iOS 13.0, tvOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: 23)
let image = UIImage(systemName: "play.circle.fill", withConfiguration: config)?.withRenderingMode(.alwaysTemplate)
let playIcon = UIImageView(image: image)
playIcon.tintColor = UIColor.black.withAlphaComponent(0.55)
appOverlay = playIcon
} else {
let playIcon = UIImageView(image: UIImage(named: "play.circle.fill"))
playIcon.tintColor = UIColor.black.withAlphaComponent(0.55)
appOverlay = playIcon
}
} else if noAppImage {
if #available(iOS 13.0, tvOS 13.0, *) {
let config = UIImage.SymbolConfiguration(pointSize: 70)
let image = UIImage(named: "icon-pc-app")?.withConfiguration(config).withRenderingMode(.alwaysTemplate)
let appIcon = UIImageView(image: image)
appIcon.tintColor = UIColor.black.withAlphaComponent(0.5)
appOverlay = appIcon
}
}
let label = UILabel()
label.backgroundColor = UIColor.black.withAlphaComponent(0.55)
label.textColor = UIColor.white
label.text = app.name == "Steam Big Picture" ? "Steam" : app.name
label.font = UIFont.systemFont(ofSize: 15)
label.baselineAdjustment = .alignCenters
label.textAlignment = .center
label.lineBreakMode = .byTruncatingTail
label.numberOfLines = 2
appLabel = label
positionSubviews()
#if os(tvOS)
appImage.overlayContentView.addSubview(label)
if let appOverlay {
appImage.overlayContentView.addSubview(appOverlay)
}
#else
if let appOverlay {
addSubview(appOverlay)
}
addSubview(label)
#endif
}
@objc private func buttonSelected(_ sender: Any) {
appImage.layer.opacity = 0.5
}
@objc private func buttonDeselected(_ sender: Any) {
appImage.layer.opacity = isCurrentApp ? 0.75 : 1.0
}
private func positionSubviews() {
let verticalPadding: CGFloat = 10
let frameSize = appImage.frame.size
appLabel?.frame = CGRect(x: 0, y: frameSize.height - 43, width: frameSize.width, height: 43)
guard let appOverlay else { return }
let overlaySize = isCurrentApp ? frameSize.width / 2.39 : frameSize.width / 2.0
appOverlay.frame = CGRect(x: 0, y: 0, width: overlaySize, height: overlaySize)
appOverlay.center = CGPoint(x: frameSize.width / 2.0, y: frameSize.height / 2.0 - 2.0 * verticalPadding)
if isCurrentApp {
appImage.layer.opacity = 0.75
}
}
@objc private func updateLoop() {
guard superview != nil, updateLoopDelegate?.isInAppView() == true else {
return
}
if (appOverlay != nil && !isCurrentApp) || (appOverlay == nil && isCurrentApp) {
updateAppImage()
}
superview?.layer.shadowOpacity = 0
alpha = app.hidden ? 0.4 : 1.0
perform(#selector(updateLoop), with: self, afterDelay: refreshCycle)
}
}
@available(iOS 13.0, *)
private final class UIAppViewContextMenuDelegate: NSObject, UIContextMenuInteractionDelegate {
private weak var appView: UIAppView?
init(appView: UIAppView) {
self.appView = appView
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
guard let appView else { return nil }
appView.cancelTracking(with: nil)
appView.callback?.appLongClicked(appView.app, view: appView)
return nil
}
}
+53 -1
View File
@@ -8,6 +8,14 @@
import UIKit import UIKit
extension UIViewController {
var hasNoPresentedVC: Bool {
let hasPresentedVC = self.presentedViewController == nil && self.view.window != nil
// let presentedVC = self.presentedViewController
return hasPresentedVC
}
}
public extension UIView { public extension UIView {
var parentViewController: UIViewController? { var parentViewController: UIViewController? {
var responder: UIResponder? = self var responder: UIResponder? = self
@@ -27,6 +35,15 @@ public extension CGPoint {
} }
} }
public extension Array where Element: Equatable {
@discardableResult
mutating func appendIfNotContains(_ newElement: Element) -> Bool {
guard !contains(newElement) else { return false }
append(newElement)
return true
}
}
public extension String { public extension String {
var localized: String { var localized: String {
LocalizationHelper.localizedString(forKey: self) LocalizationHelper.localizedString(forKey: self)
@@ -40,6 +57,35 @@ public extension String {
} }
} }
public extension UIFont {
static func roundedSystemFont(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont {
let font = UIFont.systemFont(ofSize: size, weight: weight)
if #available(iOS 13.0, *) {
if let roundedDescriptor = font.fontDescriptor.withDesign(.rounded) {
return UIFont(descriptor: roundedDescriptor, size: size)
}
}
return font
}
}
public extension UISlider {
func step(forward: Bool, visualStepRatio: Float) {
let range = self.maximumValue - self.minimumValue
guard range > 0 else { return }
let step = range * visualStepRatio
let nextValue = self.value + (forward ? step : -step)
let clampedValue = min(max(nextValue, self.minimumValue), self.maximumValue)
DispatchQueue.main.async {
self.setValue(clampedValue, animated: false)
self.sendActions(for: .valueChanged)
}
}
}
private var previousSelectedSegmentIndexKey: UInt8 = 0 private var previousSelectedSegmentIndexKey: UInt8 = 0
private var lastKnownSelectedSegmentIndexKey: UInt8 = 0 private var lastKnownSelectedSegmentIndexKey: UInt8 = 0
public extension UISegmentedControl { public extension UISegmentedControl {
@@ -104,7 +150,7 @@ public extension UISegmentedControl {
let currentValue = selectedSegmentIndex let currentValue = selectedSegmentIndex
if currentValue != newValue { if currentValue != newValue {
storePreviousSelectedSegmentIndex(newValue) storePreviousSelectedSegmentIndex(currentValue)
storeLastKnownSelectedSegmentIndex(newValue) storeLastKnownSelectedSegmentIndex(newValue)
} }
@@ -182,4 +228,10 @@ public extension UISegmentedControl {
method_exchangeImplementations(originalMethod, swizzledMethod) method_exchangeImplementations(originalMethod, swizzledMethod)
} }
} }
func nextIndex(forward: Bool) -> Int {
var targetIndex = forward ? self.selectedSegmentIndex + 1 : self.selectedSegmentIndex - 1
targetIndex = forward ? (targetIndex == self.numberOfSegments ? 0 : targetIndex) : (targetIndex == -1 ? self.numberOfSegments-1 : targetIndex)
return targetIndex
}
} }
+63
View File
@@ -86,6 +86,39 @@ import UIKit
} }
} }
@objc public static func controllerMouseSymbol() -> String {
if #available(iOS 17.0, tvOS 17.0, *) {
return "pointer.arrow.ipad"
}
else if #available(iOS 16.0, tvOS 16.0, *) {
return "pointer.arrow.ipad"
}
else {
return "paperplane"
}
}
@objc public static func disableControllerMouseSymbol() -> String {
if #available(iOS 17.0, tvOS 17.0, *) {
return "pointer.arrow.slash.square"
}
else if #available(iOS 16.0, tvOS 16.0, *) {
return "pointer.arrow"
}
else {
return "paperplane"
}
}
@objc public static func quitSymbol() -> String {
if #available(iOS 14.0, tvOS 14.0, *) {
return "pip.exit"
}
else {
return "arrow.left.square"
}
}
@objc public static func disconnectSymbolSize() -> CGFloat { @objc public static func disconnectSymbolSize() -> CGFloat {
if #available(iOS 26.0, tvOS 26.0, *) { if #available(iOS 26.0, tvOS 26.0, *) {
return 15 return 15
@@ -175,6 +208,36 @@ import UIKit
return rootViewController return rootViewController
} }
public static func runOnMain(_ work: @escaping () -> Void) {
if Thread.isMainThread {
work()
} else {
DispatchQueue.main.async(execute: work)
}
}
@objc public static func controllerMouseExpoMappedOffset(_ offset: CGFloat, expo: CGFloat) -> CGFloat {
guard offset != 0 else { return 0 }
let magnitude = pow(abs(offset), expo)
return offset > 0 ? magnitude : -magnitude
}
@objc public static func controllerMouseVelocity(pointerVelocity: CGFloat, frameRate: CGFloat) -> CGFloat {
guard frameRate > 0 else { return pointerVelocity }
return pointerVelocity * 60 / frameRate
}
@objc public static func controllerMouseMoveVector(stickX: CGFloat, stickY: CGFloat, velocity: CGFloat, expo: CGFloat) -> CGVector {
return CGVector(
dx: velocity * controllerMouseExpoMappedOffset(stickX, expo: expo),
dy: -velocity * controllerMouseExpoMappedOffset(stickY, expo: expo)
)
}
@objc public static func controllerMouseScrollVector(stickX: CGFloat, stickY: CGFloat, multiplier: CGFloat) -> CGVector {
return CGVector(dx: multiplier * stickX, dy: multiplier * stickY)
}
@objc(openUrl:) @objc(openUrl:)
static func openUrl(_ urlString: String) { static func openUrl(_ urlString: String) {
@@ -30,7 +30,8 @@ enum FileOperation: Int {
} }
@objcMembers @objcMembers
final class OSCProfilesTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIDocumentPickerDelegate, UIGestureRecognizerDelegate { final class OSCProfilesTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIDocumentPickerDelegate, UIGestureRecognizerDelegate, ControllerUINavigationDelegate {
@IBOutlet weak var tableView: UITableView! @IBOutlet weak var tableView: UITableView!
@IBOutlet weak var profileTableViewNavigationBar: UINavigationBar! @IBOutlet weak var profileTableViewNavigationBar: UINavigationBar!
@IBOutlet private weak var profileTableViewNavigationItem: UINavigationItem! @IBOutlet private weak var profileTableViewNavigationItem: UINavigationItem!
@@ -44,6 +45,9 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
private var profilesManager: OSCProfilesManager! private var profilesManager: OSCProfilesManager!
private var horizontalConstraintsConfigured = false private var horizontalConstraintsConfigured = false
private var profiles: NSMutableArray = []
private var selectedProfileIndex = 0
private var profilesLoadGeneration = 0
private func contentWidthMultiplier() -> CGFloat { private func contentWidthMultiplier() -> CGFloat {
PublicUtils.viewIsLandscape(view) ? (PublicUtils.isIPhone ? 0.8 : 0.65) : (PublicUtils.isIPhone ? 0.83 : 0.85) PublicUtils.viewIsLandscape(view) ? (PublicUtils.isIPhone ? 0.8 : 0.65) : (PublicUtils.isIPhone ? 0.83 : 0.85)
@@ -102,6 +106,13 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
NotificationCenter.default.post(name: Notification.Name("OscLayoutTableViewCloseNotification"), object: self) NotificationCenter.default.post(name: Notification.Name("OscLayoutTableViewCloseNotification"), object: self)
} }
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if #available(iOS 13.0, *) {
ControllerNavigator.restorePreviousUINavigationDelegate(ifCurrentDelegateIs: self)
}
}
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
@@ -144,6 +155,8 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
profileTableViewNavigationItem.leftBarButtonItems?.forEach { $0.hidesSharedBackground = true } profileTableViewNavigationItem.leftBarButtonItems?.forEach { $0.hidesSharedBackground = true }
profileTableViewNavigationItem.rightBarButtonItems?.forEach { $0.hidesSharedBackground = true } profileTableViewNavigationItem.rightBarButtonItems?.forEach { $0.hidesSharedBackground = true }
} }
reloadProfilesAsync(scrollToSelected: true)
} }
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@@ -167,9 +180,17 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
horizontalConstraintsConfigured = true horizontalConstraintsConfigured = true
} }
configureTableView() configureTableView()
if profilesManager.getAllProfiles().count > 0 { if profiles.count > 0 {
let indexPath = IndexPath(row: profilesManager.getIndexOfSelectedProfile(), section: 0) scrollToSelectedProfile(animated: true)
tableView.scrollToRow(at: indexPath, at: .middle, animated: true) } else {
reloadProfilesAsync(scrollToSelected: true)
}
if (loadingMode == .selectProfileFromMainFrame || loadingMode == .selectProfileFromStreamView)
&& ControllerUtil.primaryGCController != nil {
if #available(iOS 13.0, *) {
ControllerNavigator.setUINavigationDelegate(self)
}
} }
} }
@@ -180,6 +201,94 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
tableView.register(UINib(nibName: "ProfileTableViewCell", bundle: nil), forCellReuseIdentifier: "Cell") tableView.register(UINib(nibName: "ProfileTableViewCell", bundle: nil), forCellReuseIdentifier: "Cell")
} }
private func reloadProfilesAsync(scrollToSelected: Bool = false, notify: Bool = false) {
profilesLoadGeneration += 1
let generation = profilesLoadGeneration
let manager = profilesManager
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let manager else { return }
let loadedProfiles = manager.getAllProfiles()
let loadedSelectedIndex = Self.selectedProfileIndex(in: loadedProfiles)
DispatchQueue.main.async { [weak self] in
guard let self, generation == self.profilesLoadGeneration else { return }
self.profiles = loadedProfiles
self.selectedProfileIndex = loadedSelectedIndex
self.tableView.reloadData()
if scrollToSelected {
self.scrollToSelectedProfile(animated: true)
}
if notify {
self.notifyProfileViewDidRefresh()
}
}
}
}
private func scrollToSelectedProfile(animated: Bool) {
guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex) else {
return
}
let indexPath = IndexPath(row: selectedProfileIndex, section: 0)
tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
}
private func moveSelectedProfile(by offset: Int) {
PublicUtils.runOnMain { [weak self] in
guard let self,
self.profiles.count > 0,
self.tableView.numberOfSections > 0,
self.tableView.numberOfRows(inSection: 0) > 0 else {
return
}
let maxIndex = min(self.profiles.count, self.tableView.numberOfRows(inSection: 0)) - 1
let currentIndex = min(max(self.selectedProfileIndex, 0), maxIndex)
let nextIndex = min(max(currentIndex + offset, 0), maxIndex)
let nextIndexPath = IndexPath(row: nextIndex, section: 0)
self.tableView.selectRow(at: nextIndexPath, animated: true, scrollPosition: .middle)
self.tableView(self.tableView, didSelectRowAt: nextIndexPath)
}
}
private func validProfileIndex(_ index: Int) -> Int? {
guard profiles.count > 0 else { return nil }
return min(max(index, 0), profiles.count - 1)
}
private func notifyProfileViewDidRefresh() {
needToUpdateOscLayoutTVC?()
if loadingMode != .selectProfileFromStreamView
&& loadingMode != .pickProfile
&& loadingMode != .pickProfileData
&& loadingMode != .selectProfile
{
guard let selectedProfileIndex = validProfileIndex(selectedProfileIndex) else { return }
NotificationCenter.default.post(name: Notification.Name("GameProfileSelectedNotification"), object: self.profiles[selectedProfileIndex])
}
}
private static func selectedProfileIndex(in profiles: NSMutableArray) -> Int {
for index in 0..<profiles.count {
guard let profile = profiles.object(at: index) as? OSCProfile else {
continue
}
if profile.isSelected {
return index
}
}
return 0
}
@IBAction func duplicateTapped(_ sender: Any?) { @IBAction func duplicateTapped(_ sender: Any?) {
let inputAlert = UIAlertController( let inputAlert = UIAlertController(
title: LocalizationHelper.localizedString(forKey: "Enter the name you want to save this profile as"), title: LocalizationHelper.localizedString(forKey: "Enter the name you want to save this profile as"),
@@ -252,15 +361,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
} }
func profileViewRefresh() { func profileViewRefresh() {
tableView.reloadData() reloadProfilesAsync(notify: true)
needToUpdateOscLayoutTVC?()
if loadingMode != .selectProfileFromStreamView
&& loadingMode != .pickProfile
&& loadingMode != .pickProfileData
&& loadingMode != .selectProfile
{
NotificationCenter.default.post(name: Notification.Name("GameProfileSelectedNotification"), object: self)
}
} }
@IBAction func deleteTapped(_ sender: Any?) { @IBAction func deleteTapped(_ sender: Any?) {
@@ -312,7 +413,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
} }
try data.write(to: destinationURL, options: .atomic) try data.write(to: destinationURL, options: .atomic)
} catch { } catch {
NSLog("写入失败: \(error)") // NSLog("写入失败: \(error)")
} }
} }
@@ -331,7 +432,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
do { do {
let fileData = try Data(contentsOf: sourceURL) let fileData = try Data(contentsOf: sourceURL)
NSLog("profile file read: \(UInt32(fileData.count))") // NSLog("profile file read: \(UInt32(fileData.count))")
let classes: [AnyClass] = [NSMutableData.self, NSMutableArray.self] let classes: [AnyClass] = [NSMutableData.self, NSMutableArray.self]
let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: classes, from: fileData) as? NSMutableArray let profilesEncoded = try NSKeyedUnarchiver.unarchivedObject(ofClasses: classes, from: fileData) as? NSMutableArray
@@ -357,7 +458,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
} }
profileViewRefresh() profileViewRefresh()
NSLog("profile test: \(UInt32(profilesEncoded?.count ?? 0))") // NSLog("profile test: \(UInt32(profilesEncoded?.count ?? 0))")
} catch { } catch {
restoreFailed = true restoreFailed = true
let failedAlert = UIAlertController( let failedAlert = UIAlertController(
@@ -368,17 +469,18 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
failedAlert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default)) failedAlert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "OK"), style: .default))
present(failedAlert, animated: true) present(failedAlert, animated: true)
profileViewRefresh() profileViewRefresh()
NSLog("解码失败: \(error)") // NSLog("解码失败: \(error)")
} }
} }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
profilesManager.getAllProfiles().count profiles.count
} }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? ProfileTableViewCell, guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? ProfileTableViewCell,
let profile = profilesManager.getAllProfiles().object(at: indexPath.row) as? OSCProfile, indexPath.row < profiles.count,
let profile = profiles.object(at: indexPath.row) as? OSCProfile,
let nameLabel = cell.name else { let nameLabel = cell.name else {
return UITableViewCell() return UITableViewCell()
} }
@@ -395,7 +497,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
cell.contentView.backgroundColor = UIColor.clear cell.contentView.backgroundColor = UIColor.clear
if loadingMode != .pickProfileData, if loadingMode != .pickProfileData,
indexPath.row == profilesManager.getIndexOfSelectedProfile() { indexPath.row == selectedProfileIndex {
cell.accessoryType = UITableViewCell.AccessoryType.checkmark cell.accessoryType = UITableViewCell.AccessoryType.checkmark
let checkmarkLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20)) let checkmarkLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
checkmarkLabel.text = "✓" checkmarkLabel.text = "✓"
@@ -421,7 +523,7 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
} }
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
let profiles = profilesManager.getAllProfiles() guard indexPath.row < profiles.count else { return }
if let profile = profiles.object(at: indexPath.row) as? OSCProfile, profile.name == "Default" { if let profile = profiles.object(at: indexPath.row) as? OSCProfile, profile.name == "Default" {
let alert = UIAlertController(title: "", message: "Deleting the 'Default' profile is not allowed", preferredStyle: .alert) let alert = UIAlertController(title: "", message: "Deleting the 'Default' profile is not allowed", preferredStyle: .alert)
@@ -451,31 +553,58 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
UserDefaults.standard.synchronize() UserDefaults.standard.synchronize()
} }
tableView.reloadData() reloadProfilesAsync()
} }
} }
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard indexPath.section == 0,
indexPath.row >= 0,
indexPath.row < profiles.count else {
return
}
if loadingMode == .pickProfileData, if loadingMode == .pickProfileData,
let pickedProfile = profilesManager.getAllProfiles().object(at: indexPath.row) as? OSCProfile { let pickedProfile = profiles.object(at: indexPath.row) as? OSCProfile {
pickedProfileDataHandler?(pickedProfile) pickedProfileDataHandler?(pickedProfile)
dismiss(animated: false) dismiss(animated: false)
return return
} }
let selectedIndexPath = IndexPath(row: indexPath.row, section: 0) let selectedIndexPath = IndexPath(row: indexPath.row, section: 0)
let lastSelectedIndexPath = IndexPath(row: profilesManager.getIndexOfSelectedProfile(), section: 0) let lastSelectedIndex = validProfileIndex(selectedProfileIndex)
let lastSelectedIndexPath = lastSelectedIndex.map { IndexPath(row: $0, section: 0) }
if selectedIndexPath != lastSelectedIndexPath { if selectedIndexPath != lastSelectedIndexPath {
let selectedCell = tableView.cellForRow(at: selectedIndexPath) if let lastSelectedIndexPath {
selectedCell?.accessoryType = .checkmark let previousCell = tableView.cellForRow(at: lastSelectedIndexPath)
selectedCell?.accessoryView?.tintColor = UIColor(red: 0.5, green: 0.5, blue: 1.0, alpha: 1.0).withAlphaComponent(1.0) previousCell?.accessoryType = .none
previousCell?.accessoryView = nil
}
profilesManager.setProfileToSelected(UInt32(indexPath.row)) profilesManager.setProfileToSelected(UInt32(indexPath.row))
selectedProfileIndex = indexPath.row
let lastSelectedCell = tableView.cellForRow(at: lastSelectedIndexPath) let selectedCell = tableView.cellForRow(at: selectedIndexPath)
lastSelectedCell?.accessoryType = .none selectedCell?.accessoryType = .checkmark
tableView.deselectRow(at: lastSelectedIndexPath, animated: true) let checkmarkLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
checkmarkLabel.text = "✓"
checkmarkLabel.font = .systemFont(ofSize: 25)
checkmarkLabel.textAlignment = .center
checkmarkLabel.textColor = .green
checkmarkLabel.layer.zPosition = 0
selectedCell?.accessoryView = checkmarkLabel
let rowsToReload = [lastSelectedIndexPath, selectedIndexPath]
.compactMap { $0 }
.filter {
$0.section < tableView.numberOfSections &&
$0.row >= 0 &&
$0.row < tableView.numberOfRows(inSection: $0.section)
}
if !rowsToReload.isEmpty {
tableView.reloadRows(at: rowsToReload, with: .none)
}
} }
profileViewRefresh() profileViewRefresh()
} }
@@ -491,4 +620,43 @@ final class OSCProfilesTableViewController: UIViewController, UITableViewDelegat
} }
return touch.view == view return touch.view == view
} }
@available(iOS 13.0, *)
func getNavigationElements() -> [ControllerNavigationElement] {
var elements: [ControllerNavigationElement] = []
elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadUp : .y, action: "previousItem"))
elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadDown : .a, action: "nextItem"))
elements.append(ControllerNavigationElement(control:ControllerNavigator.radialMenuButtonPosition == .left ? .dpadRight : .x, action: "exit"))
return elements
}
func navigateByController(forward: Bool) {
}
func navigateByController(downward: Bool) {
}
func persistControllerNavigationHighlight() {}
func restoreControllerNavigationHighlight() {}
func restoreControllerNavigationHighlightAfterSettingsModeSwitch() {}
func uiWidgetActionForControllerNavigator(forward: Bool, from navigation: ControllerNavigationElement) {
}
func uiButtonActionForControllerNavigator(pressed: Bool, from navigation: ControllerNavigationElement) {
guard pressed else { return }
PublicUtils.runOnMain { [weak self] in
switch navigation.action {
case "previousItem":
self?.moveSelectedProfile(by: -1)
case "nextItem":
self?.moveSelectedProfile(by: 1)
case "exit":
self?.dismiss(animated: false)
default:
break
}
}
}
} }
@@ -126,7 +126,6 @@ class HostCollectionViewController: UICollectionViewController, UICollectionView
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
NSLog("View did appear")
} }
override func didMove(toParent parent: UIViewController?) { override func didMove(toParent parent: UIViewController?) {
@@ -209,6 +208,7 @@ class HostCollectionViewController: UICollectionViewController, UICollectionView
if let host = items[indexPath.item] as? TemporaryHost { if let host = items[indexPath.item] as? TemporaryHost {
cell.configure(with: host) cell.configure(with: host)
} }
applyControllerNavigationHighlight(to: cell, highlighted: indexPath == controllerNavigationSelectedIndexPath)
return cell return cell
} }
@@ -13,32 +13,43 @@
#import "DiscoveryManager.h" #import "DiscoveryManager.h"
#import "PairManager.h" #import "PairManager.h"
#import "StreamConfiguration.h" #import "StreamConfiguration.h"
#import "UIAppView.h"
#import "AppAssetManager.h" #import "AppAssetManager.h"
#import "SWRevealViewController.h" #import "SWRevealViewController.h"
#import "VoidLink-Swift.h"
@class SettingsViewController;
@class TemporaryApp;
@class HostCollectionViewController;
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate, UITextFieldDelegate>
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate, HostCardActionDelegate, AppViewUpdateLoopDelegate, UITextFieldDelegate, ControllerNavigatorRadialMenuDelegate>
@property (nonatomic, strong) IBOutlet UIBarButtonItem *settingsButton; @property (nonatomic, strong) IBOutlet UIBarButtonItem *settingsButton;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *profilesButton; @property (weak, nonatomic) IBOutlet UIBarButtonItem *profilesButton;
@property (weak, nonatomic) SettingsViewController *settingsViewController;
@property (nonatomic, strong) HostCollectionViewController *hostCollectionVC;
@property (nonatomic, strong, readonly) NSArray<TemporaryApp *> *sortedAppList;
#if !TARGET_OS_TV #if !TARGET_OS_TV
@property (nonatomic, assign) bool settingsExpandedInStreamView; @property (nonatomic, assign) bool settingsExpandedInStreamView;
@property (nonatomic, strong) HostCollectionViewController *hostCollectionVC; @property (nonatomic, readonly) bool settingsViewExpanded;
@property (weak, nonatomic) IBOutlet UINavigationItem *navigationItem; @property (weak, nonatomic) IBOutlet UINavigationItem *navigationItem;
-(void)expandSettingsView; - (void)expandSettingsView;
- (void)closeSettingViewAnimated:(BOOL)anaimated; - (void)closeSettingViewAnimated:(BOOL)anaimated;
- (void)reloadStreamConfig; - (void)reloadStreamConfig;
- (bool)isIPhonePortrait; - (bool)isIPhonePortrait;
- (void)quitRunningApp; - (bool)isInAppView;
- (bool)isStreaming;
- (TemporaryApp*)findRunningApp:(TemporaryHost*)host;
- (void)quitApp:(TemporaryApp* )app;
- (void)quitLaunchedApp;
- (void)launchApp:(TemporaryApp *)app;
- (void)quitRunningAppAndStart:(TemporaryApp *)app;
- (NSInteger)requestForBitrate:(NSInteger)bitrateKbps; - (NSInteger)requestForBitrate:(NSInteger)bitrateKbps;
#endif #endif
- (void)fillResolutionTable:(CGSize*)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode; - (void)fillResolutionTable:(CGSize*)resolutionTable externalDisplayMode:(NSInteger)externalDisplayMode;
- (bool)isIPhone;
- (void)setNeedsUpdateAllowedOrientation; - (void)setNeedsUpdateAllowedOrientation;
@end @end
+320 -115
View File
@@ -16,7 +16,6 @@
#import "Connection.h" #import "Connection.h"
#import "StreamManager.h" #import "StreamManager.h"
#import "Utils.h" #import "Utils.h"
#import "UIAppView.h"
#import "DataManager.h" #import "DataManager.h"
#import "TemporarySettings.h" #import "TemporarySettings.h"
#import "WakeOnLanManager.h" #import "WakeOnLanManager.h"
@@ -44,8 +43,10 @@
#include <Limelight.h> #include <Limelight.h>
@interface MainFrameViewController() @interface MainFrameViewController() <AppCallback, HostCardActionDelegate, AppViewUpdateLoopDelegate, ControllerNavigatorRadialMenuDelegate, ControllerUtilDelegate>
@property (weak, nonatomic) LayoutOnScreenControlsViewController* gameProfileSelectorVC; @property (weak, nonatomic) LayoutOnScreenControlsViewController* gameProfileSelectorVC;
@property (nonatomic, strong, readwrite) NSArray<TemporaryApp *> *sortedAppList;
@property (nonatomic, assign) bool settingsViewExpanded;
@end @end
@implementation MainFrameViewController { @implementation MainFrameViewController {
@@ -67,14 +68,11 @@
UIAlertController* _pairAlert; UIAlertController* _pairAlert;
LoadingFrameViewController* _loadingFrame; LoadingFrameViewController* _loadingFrame;
FrontViewPosition currentPosition; FrontViewPosition currentPosition;
NSArray* _sortedAppList;
NSCache* _boxArtCache; NSCache* _boxArtCache;
bool _background; bool _background;
bool _enteredAppView; bool _enteredAppView;
bool _settingsViewExpanded;
UIView* menuSeparator; UIView* menuSeparator;
UIView* snapshot; UIView* snapshot;
SettingsViewController* settingsViewController;
__weak StreamFrameViewController* streamFrameViewController; __weak StreamFrameViewController* streamFrameViewController;
id navBarAppearanceStandard; id navBarAppearanceStandard;
bool _viewJustAppeared; bool _viewJustAppeared;
@@ -101,11 +99,13 @@ static NSMutableSet* hostList;
message:[LocalizationHelper localizedStringForKey:@"Enter_PIN_Msg", PIN] message:[LocalizationHelper localizedStringForKey:@"Enter_PIN_Msg", PIN]
preferredStyle:UIAlertControllerStyleAlert]; preferredStyle:UIAlertControllerStyleAlert];
[self->_pairAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { [self->_pairAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
self->_pairAlert = nil; dispatch_async(dispatch_get_main_queue(), ^{
[self->_discMan startDiscovery]; self->_pairAlert = nil;
[self hideLoadingFrame: ^{ [self->_discMan startDiscovery];
[self switchToHostView]; [self hideLoadingFrame: ^{
}]; [self switchToHostView];
}];
});
}]]; }]];
[[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil]; [[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil];
}); });
@@ -348,7 +348,7 @@ static NSMutableSet* hostList;
[_appManager stopRetrieving]; [_appManager stopRetrieving];
_showHiddenApps = NO; _showHiddenApps = NO;
_selectedHost = nil; _selectedHost = nil;
_sortedAppList = nil; self.sortedAppList = nil;
// [self.collectionView removeFromSuperview]; // necessary for new scroll host view reloading mechanism // [self.collectionView removeFromSuperview]; // necessary for new scroll host view reloading mechanism
self.hostCollectionVC.view.hidden = NO; self.hostCollectionVC.view.hidden = NO;
@@ -356,6 +356,15 @@ static NSMutableSet* hostList;
[self updateTitle]; [self updateTitle];
self.navigationItem.rightBarButtonItems = @[_helpButton, _addHostButton]; self.navigationItem.rightBarButtonItems = @[_helpButton, _addHostButton];
self.revealViewController.mainFrameIsInHostView = true; // to allow orientation change only in app view, tell top view controller the mainframe is not in host view self.revealViewController.mainFrameIsInHostView = true; // to allow orientation change only in app view, tell top view controller the mainframe is not in host view
if (@available(iOS 13.0, *)){
[GamepadNavigationIllustrationHud showInKeyWindow];
[ControllerNavigator setUINavigationDelegate:self.isInAppView ? self.hostCollectionVC : self.hostCollectionVC];
if(ControllerNavigator.radialMenuView.superview) [ControllerNavigator updateRadialMenu];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ControllerNavigator restoreUINavigationHighlight];
});
}
} }
- (void) receivedAssetForApp:(TemporaryApp*)app { - (void) receivedAssetForApp:(TemporaryApp*)app {
@@ -378,6 +387,10 @@ static NSMutableSet* hostList;
} }
- (void)switchToAppView{ - (void)switchToAppView{
if (@available(iOS 13.0, *)) {
[ControllerNavigator setUINavigationDelegate:self];
}
_enteredAppView = true; _enteredAppView = true;
//_appManager = [[AppAssetManager alloc] initWithCallback:self]; //_appManager = [[AppAssetManager alloc] initWithCallback:self];
[self.collectionView setCollectionViewLayout:self.collectionViewLayout]; [self.collectionView setCollectionViewLayout:self.collectionViewLayout];
@@ -399,6 +412,12 @@ static NSMutableSet* hostList;
// self.navigationController.navigationBar.backgroundColor = [ThemeManager appBackgroundColor]; // self.navigationController.navigationBar.backgroundColor = [ThemeManager appBackgroundColor];
// self.navigationController.navigationBar.translucent = NO; // self.navigationController.navigationBar.translucent = NO;
//[self applyNavBarAppearance:navBarAppearance]; //[self applyNavBarAppearance:navBarAppearance];
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
if(@available(iOS 13.0, *)) if(ControllerUtil.primaryGCController) [ControllerNavigator restoreUINavigationHighlight];
});
} }
- (void)appButtonTappedForHost:(TemporaryHost *)host{ - (void)appButtonTappedForHost:(TemporaryHost *)host{
@@ -411,12 +430,13 @@ static NSMutableSet* hostList;
} }
- (void)launchButtonTappedForHost:(TemporaryHost *)host { - (void)launchButtonTappedForHost:(TemporaryHost *)host {
if(self.revealViewController.isStreaming) return;
_selectedHost = host; _selectedHost = host;
if (host.state == StateOnline && host.pairState == PairStatePaired && host.appList.count > 0) { if (host.state == StateOnline && host.pairState == PairStatePaired && host.appList.count > 0) {
[self closeSettingViewAnimated:NO]; [self closeSettingViewAnimated:NO];
// [self switchToAppView]; // [self switchToAppView];
[self updateAppsForHost:_selectedHost]; [self updateAppsForHost:_selectedHost];
[self prepareToStreamApp:_sortedAppList.firstObject]; [self prepareToStreamApp:self.sortedAppList.firstObject];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil]; [self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
return; return;
} }
@@ -755,14 +775,16 @@ static NSMutableSet* hostList;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){ [self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
if (host != nil) { if (host != nil) {
dispatch_async(dispatch_get_main_queue(), ^{ [alertController dismissViewControllerAnimated:false completion:^{
[self hideLoadingFrame:^{ dispatch_async(dispatch_get_main_queue(), ^{
@synchronized(hostList) { [self hideLoadingFrame:^{
[hostList addObject:host]; @synchronized(hostList) {
} [hostList addObject:host];
[self updateHosts]; }
}]; [self updateHosts];
}); }];
});
}];
} else { } else {
unsigned int portTestResults = LiTestClientConnectivity([host.activeAddress UTF8String], 443, unsigned int portTestResults = LiTestClientConnectivity([host.activeAddress UTF8String], 443,
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989); ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
@@ -774,13 +796,15 @@ static NSMutableSet* hostList;
[Utils addHelpOptionToDialog:hostNotFoundAlert]; [Utils addHelpOptionToDialog:hostNotFoundAlert];
[hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]]; [hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingFrame:^{ [alertController dismissViewControllerAnimated:true completion:^{
if([error isEqualToString:[LocalizationHelper localizedStringForKey:@"Host information updated"]]){ [self hideLoadingFrame:^{
DataManager* dataMan = [[DataManager alloc] init]; if([error isEqualToString:[LocalizationHelper localizedStringForKey:@"Host information updated"]]){
[dataMan updateHost:host]; DataManager* dataMan = [[DataManager alloc] init];
} [dataMan updateHost:host];
[[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil]; }
}]; [self presentViewController:hostNotFoundAlert animated:YES completion:nil];
}];
}];
}); });
} }
}]; }];
@@ -990,10 +1014,11 @@ static NSMutableSet* hostList;
return quitResponse; return quitResponse;
} }
- (void)quitRunningApp{ - (void)quitApp:(TemporaryApp* )app{
if(!app) return;
[self showLoadingFrame: ^{ [self showLoadingFrame: ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpResponse* quitResponse = [self requestToQuitApp:self->launchedApp]; HttpResponse* quitResponse = [self requestToQuitApp:app];
// If it fails, display an error and stop the current operation // If it fails, display an error and stop the current operation
if (quitResponse.statusCode != 200) { if (quitResponse.statusCode != 200) {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Quitting App Failed"] UIAlertController* alert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Quitting App Failed"]
@@ -1001,7 +1026,7 @@ static NSMutableSet* hostList;
preferredStyle:UIAlertControllerStyleAlert]; preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]]; [alert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self updateAppsForHost:self->launchedApp.host]; [self updateAppsForHost:app.host];
[self hideLoadingFrame: ^{ [self hideLoadingFrame: ^{
[[self activeViewController] presentViewController:alert animated:YES completion:nil]; [[self activeViewController] presentViewController:alert animated:YES completion:nil];
}]; }];
@@ -1012,6 +1037,54 @@ static NSMutableSet* hostList;
}]; }];
} }
- (void)quitLaunchedApp {
[self quitApp:launchedApp];
}
- (void)quitRunningAppAndStart:(TemporaryApp *)app {
TemporaryApp* currentRunningApp = [self findRunningApp:app.host];
[self showLoadingFrame: ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpResponse* quitResponse = [self requestToQuitApp:app];
// If it fails, display an error and stop the current operation
if (quitResponse.statusCode != 200) {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Quitting App Failed"]
message:[LocalizationHelper localizedStringForKey:@"Failed to quit app. If this app was started by another device, you'll need to quit from that device."]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateAppsForHost:app.host];
[self hideLoadingFrame: ^{
[[self activeViewController] presentViewController:alert animated:YES completion:nil];
}];
});
}
else {
app.host.currentGame = @"0";
dispatch_async(dispatch_get_main_queue(), ^{
// If it succeeds and we're to start streaming, segue to the stream
if (![app.id isEqualToString:currentRunningApp.id]) {
[self prepareToStreamApp:app];
[self hideLoadingFrame: ^{
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}];
}
else {
// Otherwise, just hide the loading icon
[self hideLoadingFrame:nil];
}
});
}
});
}];
}
- (void)launchApp:(TemporaryApp *)app {
if(self.revealViewController.isStreaming) return;
[self prepareToStreamApp:app];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}
- (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view { - (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view {
Log(LOG_D, @"Long clicked app: %@", app.name); Log(LOG_D, @"Long clicked app: %@", app.name);
@@ -1188,15 +1261,15 @@ static NSMutableSet* hostList;
countdown:6 countdown:6
action:^{} action:^{}
completion:^{ completion:^{
[self openGameProfileSeletor:false]; [self openGameProfileSeletorWithAnimated:false];
}]; }];
} }
else [self openGameProfileSeletor:false]; else [self openGameProfileSeletorWithAnimated:false];
} }
- (void)openGameProfileSeletor:(bool)animated { - (void)openGameProfileSeletorWithAnimated:(bool)animated {
if(settingsViewController){ if(self.settingsViewController){
[settingsViewController mainFrameGameProfileButtonTapped:animated]; [self.settingsViewController mainFrameGameProfileButtonTapped:animated];
return; return;
} }
@@ -1235,11 +1308,17 @@ static NSMutableSet* hostList;
} }
- (void)revealController:(SWRevealViewController *)revealController willMoveToPosition:(FrontViewPosition)position { - (void)revealController:(SWRevealViewController *)revealController willMoveToPosition:(FrontViewPosition)position {
settingsViewController = (SettingsViewController*)[revealController rearViewController]; self.settingsViewController = (SettingsViewController*)[revealController rearViewController];
revealController.navBarMenuDelegate = settingsViewController; revealController.navBarMenuDelegate = self.settingsViewController;
_settingsViewExpanded = position != FrontViewPositionLeft; _settingsViewExpanded = position != FrontViewPositionLeft;
if (position == FrontViewPositionLeft) { if (position == FrontViewPositionLeft) {
if (@available(iOS 26.0, *)) { if (@available(iOS 13.0, *)){
self.settingsExpandedInStreamView = false;
if(!self.revealViewController.isStreaming) [ControllerNavigator setUINavigationDelegate: self.isInAppView ? self : self.hostCollectionVC];
else [ControllerNavigator setUINavigationDelegate: [StreamFrameViewController sharedInstance]];
}
if (@available(iOS 26.0, *)) {
_settingsButton.sharesBackground = false; _settingsButton.sharesBackground = false;
_profilesButton.sharesBackground = false; _profilesButton.sharesBackground = false;
} }
@@ -1255,11 +1334,13 @@ static NSMutableSet* hostList;
} }
} }
else { else {
if(self.revealViewController.isStreaming) self.settingsExpandedInStreamView = true; //notify mainFrameViewContorller that this is a setting expansion in stream view, some settings shall be disabled.
if (@available(iOS 13.0, *)) [ControllerNavigator setUINavigationDelegate:self.settingsViewController];
self.navigationItem.leftBarButtonItems = @[_profilesButton]; self.navigationItem.leftBarButtonItems = @[_profilesButton];
[settingsViewController updateTheme]; [self.settingsViewController updateTheme];
} }
settingsViewController.mainFrameViewController = self; self.settingsViewController.mainFrameViewController = self;
// enable / disable widgets acoordingly: in streamview, disable, outside of streamview, enable. // enable / disable widgets acoordingly: in streamview, disable, outside of streamview, enable.
if(self.settingsExpandedInStreamView) [revealController buttonsInStreaming]; if(self.settingsExpandedInStreamView) [revealController buttonsInStreaming];
else [revealController buttonsNotInStreaming]; else [revealController buttonsNotInStreaming];
@@ -1268,71 +1349,72 @@ static NSMutableSet* hostList;
// TemporarySettings* currentSettings = [dataMan getSettings]; // TemporarySettings* currentSettings = [dataMan getSettings];
[streamFrameViewController setUserInteractionEnabledForStreamView:!_settingsExpandedInStreamView || position == FrontViewPositionLeft]; [streamFrameViewController setUserInteractionEnabledForStreamView:!_settingsExpandedInStreamView || position == FrontViewPositionLeft];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.resolutionStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.resolutionStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.fpsStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.fpsStack];
// [settingsViewController widget:settingsViewController.bitrateSlider setEnabled:!self.settingsExpandedInStreamView]; // [self.settingsViewController widget:self.settingsViewController.bitrateSlider setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.optimizeGamesStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.optimizeGamesStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.audioOnPcStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.audioOnPcStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.sdrPerformanceWorkaroundStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.sdrPerformanceWorkaroundStack];
// [settingsViewController.touchModeSelector1 setEnabled:!_settingsExpandedInStreamView || !(settingsViewController.touchModeSelector1.selectedSegmentIndex == AbsoluteTouch && !settingsViewController.passthroughGesturesSwitch.isOn)]; // [self.settingsViewController.touchModeSelector1 setEnabled:!_settingsExpandedInStreamView || !(self.settingsViewController.touchModeSelector1.selectedSegmentIndex == AbsoluteTouch && !self.settingsViewController.passthroughGesturesSwitch.isOn)];
// [settingsViewController.touchModeSelector2 setEnabled:settingsViewController.touchModeSelector1.enabled]; // [self.settingsViewController.touchModeSelector2 setEnabled:self.settingsViewController.touchModeSelector1.enabled];
[settingsViewController.codecSelector setEnabled:!_settingsExpandedInStreamView]; [self.settingsViewController.codecSelector setEnabled:!_settingsExpandedInStreamView];
if(_settingsExpandedInStreamView){ if(_settingsExpandedInStreamView){
[settingsViewController.yuv444Switch setEnabled:NO]; [self.settingsViewController.yuv444Switch setEnabled:NO];
[settingsViewController.fullColorRangeSwitch setEnabled:NO]; [self.settingsViewController.fullColorRangeSwitch setEnabled:NO];
[settingsViewController.hdrSwitch setEnabled:NO]; [self.settingsViewController.hdrSwitch setEnabled:NO];
} }
else [settingsViewController updateCodecDependentSwitches]; else [self.settingsViewController updateCodecDependentSwitches];
[settingsViewController.gyroModeSelector setEnabled:!_settingsExpandedInStreamView || ![streamFrameViewController shallDisableGyroHotSwitch]]; [self.settingsViewController.gyroModeSelector setEnabled:!_settingsExpandedInStreamView || ![streamFrameViewController shallDisableGyroHotSwitch]];
[settingsViewController.emulatedControllerTypeSelector setEnabled:!_settingsExpandedInStreamView]; [self.settingsViewController.emulatedControllerTypeSelector setEnabled:!_settingsExpandedInStreamView];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.citrixX1MouseStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.citrixX1MouseStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.externalDisplayModeStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.externalDisplayModeStack];
if(settingsViewController.audioConfigSelector.numberOfSegments>2){ if(self.settingsViewController.audioConfigSelector.numberOfSegments>2){
if(_settingsExpandedInStreamView){ if(_settingsExpandedInStreamView){
if(settingsViewController.audioConfigSelector.selectedSegmentIndex>=2){ if(self.settingsViewController.audioConfigSelector.selectedSegmentIndex>=2){
[settingsViewController.audioConfigSelector setEnabled:false]; [self.settingsViewController.audioConfigSelector setEnabled:false];
} }
else { else {
[settingsViewController.audioConfigSelector setEnabled:false forSegmentAtIndex:2]; [self.settingsViewController.audioConfigSelector setEnabled:false forSegmentAtIndex:2];
[settingsViewController.audioConfigSelector setEnabled:false forSegmentAtIndex:3]; [self.settingsViewController.audioConfigSelector setEnabled:false forSegmentAtIndex:3];
} }
} }
else{ else{
[settingsViewController.audioConfigSelector setEnabled:true]; [self.settingsViewController.audioConfigSelector setEnabled:true];
[settingsViewController.audioConfigSelector setEnabled:true forSegmentAtIndex:2]; [self.settingsViewController.audioConfigSelector setEnabled:true forSegmentAtIndex:2];
[settingsViewController.audioConfigSelector setEnabled:true forSegmentAtIndex:3]; [self.settingsViewController.audioConfigSelector setEnabled:true forSegmentAtIndex:3];
} }
} }
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.duckOtherAppStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.duckOtherAppStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.pipStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.pipStack];
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.appThemeStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView forStack:self.settingsViewController.appThemeStack];
[settingsViewController.renderingBackendSelector setEnabled:!_settingsExpandedInStreamView]; [self.settingsViewController.renderingBackendSelector setEnabled:!_settingsExpandedInStreamView];
// Enable frame pacing mode selector only if not in stream view AND not in performance mode // Enable frame pacing mode selector only if not in stream view AND not in performance mode
BOOL shouldEnableFramePacingSelector = !_settingsExpandedInStreamView && (settingsViewController.renderingBackendSelector.selectedSegmentIndex != RENDER_METAL); BOOL shouldEnableFramePacingSelector = !_settingsExpandedInStreamView && (self.settingsViewController.renderingBackendSelector.selectedSegmentIndex != RENDER_METAL);
[settingsViewController.framePacingModeSelector setEnabled:shouldEnableFramePacingSelector]; [self.settingsViewController.framePacingModeSelector setEnabled:shouldEnableFramePacingSelector];
// [settingsViewController.frameTimebaseSwitch setEnabled:shouldEnableFramePacing]; // [self.settingsViewController.frameTimebaseSwitch setEnabled:shouldEnableFramePacing];
[settingsViewController.asyncFrameDequeueSwitch setEnabled:shouldEnableFramePacingSelector]; [self.settingsViewController.asyncFrameDequeueSwitch setEnabled:shouldEnableFramePacingSelector];
[settingsViewController setHidden:_settingsExpandedInStreamView || !(shouldEnableFramePacingSelector && settingsViewController.framePacingModeSelector.selectedSegmentIndex == FramePacingModeQueue) forStack:settingsViewController.frameQueueSizeStack]; [self.settingsViewController setHidden:_settingsExpandedInStreamView || !(shouldEnableFramePacingSelector && self.settingsViewController.framePacingModeSelector.selectedSegmentIndex == FramePacingModeQueue) forStack:self.settingsViewController.frameQueueSizeStack];
// Disable mic switch if sunshine does not support mic redirection // Disable mic switch if sunshine does not support mic redirection
[settingsViewController.redirectMicSwitch setEnabled:!_settingsExpandedInStreamView||streamFrameViewController.micStreamInitialized]; [self.settingsViewController.redirectMicSwitch setEnabled:!_settingsExpandedInStreamView||streamFrameViewController.micStreamInitialized];
if(_settingsExpandedInStreamView && !streamFrameViewController.micStreamInitialized) [settingsViewController.redirectMicSwitch setOn:false]; if(_settingsExpandedInStreamView && !streamFrameViewController.micStreamInitialized) [self.settingsViewController.redirectMicSwitch setOn:false];
[settingsViewController setHidden:!settingsViewController.redirectMicSwitch.isOn forStack:settingsViewController.useBuiltinMicStack]; [self.settingsViewController setHidden:!self.settingsViewController.redirectMicSwitch.isOn forStack:self.settingsViewController.useBuiltinMicStack];
[settingsViewController.useBuiltinMicSwitch setEnabled:!_settingsExpandedInStreamView]; [self.settingsViewController.useBuiltinMicSwitch setEnabled:!_settingsExpandedInStreamView];
// [settingsViewController.passthroughGesturesSwitch setEnabled:!_settingsExpandedInStreamView]; // [self.settingsViewController.passthroughGesturesSwitch setEnabled:!_settingsExpandedInStreamView];
} }
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position { - (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position {
// If we moved back to the center position, we should save the settings // If we moved back to the center position, we should save the settings
SettingsViewController* settingsViewController = (SettingsViewController*)[revealController rearViewController]; self.settingsViewController = (SettingsViewController*)[revealController rearViewController];
settingsViewController.mainFrameViewController = self; self.settingsViewController.mainFrameViewController = self;
if (position == FrontViewPositionLeft) { if (position == FrontViewPositionLeft) {
[settingsViewController saveSettings]; if (@available(iOS 13.0, *)) [ControllerNavigator persistUINavigationHighlight];
[self.settingsViewController saveSettings];
_settingsButton.enabled = YES; // make sure these 2 buttons are enabled after closing setting view. _settingsButton.enabled = YES; // make sure these 2 buttons are enabled after closing setting view.
_upButton.enabled = YES; // here is the select new host button _upButton.enabled = YES; // here is the select new host button
} }
@@ -1343,7 +1425,7 @@ static NSMutableSet* hostList;
#if TARGET_OS_TV #if TARGET_OS_TV
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[self appClicked:_sortedAppList[indexPath.row] view:nil]; [self appClicked:self.sortedAppList[indexPath.row] view:nil];
} }
#endif #endif
@@ -1446,10 +1528,6 @@ static NSMutableSet* hostList;
} }
- (bool)isIPhone{
return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
}
- (bool)isIPhonePortrait{ - (bool)isIPhonePortrait{
bool isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); bool isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]); CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
@@ -1492,7 +1570,9 @@ static NSMutableSet* hostList;
button.titleLabel.font = [UIFont systemFontOfSize:liquidGlassEnabled ? 16 : 16 weight:UIFontWeightMedium]; button.titleLabel.font = [UIFont systemFontOfSize:liquidGlassEnabled ? 16 : 16 weight:UIFontWeightMedium];
// 文字颜色设置为 tintColor 控制 // 文字颜色设置为 tintColor 控制
if(liquidGlassEnabled) button.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0.9, 0); if(liquidGlassEnabled) {
button.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0.9, 0);
}
button.tintColor = liquidGlassEnabled ? ThemeManager.appPrimaryColor : UIColor.whiteColor; button.tintColor = liquidGlassEnabled ? ThemeManager.appPrimaryColor : UIColor.whiteColor;
[button setTitleColor:button.tintColor forState:UIControlStateNormal]; [button setTitleColor:button.tintColor forState:UIControlStateNormal];
// button.tintColor = UIColor.whiteColor; // button.tintColor = UIColor.whiteColor;
@@ -1511,7 +1591,7 @@ static NSMutableSet* hostList;
// 创建 UIBarButtonItem // 创建 UIBarButtonItem
UIBarButtonItem *barItem = [[UIBarButtonItem alloc] initWithCustomView:button]; UIBarButtonItem *barItem = [[UIBarButtonItem alloc] initWithCustomView:button];
if (@available(iOS 26.0, *)) barItem.sharesBackground = true;
return barItem; return barItem;
} }
@@ -1548,6 +1628,9 @@ static NSMutableSet* hostList;
// 创建 UIBarButtonItem // 创建 UIBarButtonItem
UIBarButtonItem *barItem = [[UIBarButtonItem alloc] initWithCustomView:button]; UIBarButtonItem *barItem = [[UIBarButtonItem alloc] initWithCustomView:button];
if (@available(iOS 26.0, *)) barItem.sharesBackground = true;
return barItem; return barItem;
} }
@@ -1574,6 +1657,60 @@ static NSMutableSet* hostList;
} }
} }
- (void)applyThemeToNavigationButton:(UIBarButtonItem *)barButtonItem {
if (!barButtonItem) return;
barButtonItem.tintColor = ThemeManager.appPrimaryColor;
if (@available(iOS 13.0, *)) {
UIUserInterfaceStyle style = ThemeManager.userInterfaceStyle;
UIView *customView = barButtonItem.customView;
customView.overrideUserInterfaceStyle = style;
if ([customView isKindOfClass:UIButton.class]) {
UIButton *button = (UIButton *)customView;
button.tintColor = ThemeManager.appPrimaryColor;
[button setTitleColor:button.tintColor forState:UIControlStateNormal];
[button setTitleColor:ThemeManager.textColorGray forState:UIControlStateHighlighted];
}
}
if (barButtonItem == _addHostButton && [barButtonItem.customView isKindOfClass:UIButton.class]) {
UIButton *button = (UIButton *)barButtonItem.customView;
button.backgroundColor = PublicUtils.liquidGlassEnabled ? UIColor.clearColor : ThemeManager.appPrimaryColor;
button.tintColor = PublicUtils.liquidGlassEnabled ? ThemeManager.appPrimaryColor : UIColor.whiteColor;
[button setTitleColor:button.tintColor forState:UIControlStateNormal];
}
}
- (void)applyThemeToNavigationControls {
if (@available(iOS 13.0, *)) {
UIUserInterfaceStyle style = ThemeManager.userInterfaceStyle;
self.overrideUserInterfaceStyle = style;
self.view.overrideUserInterfaceStyle = style;
self.navigationController.overrideUserInterfaceStyle = style;
self.navigationController.view.overrideUserInterfaceStyle = style;
self.navigationController.navigationBar.overrideUserInterfaceStyle = style;
self.navigationItem.titleView.overrideUserInterfaceStyle = style;
}
NSMutableArray<UIBarButtonItem *> *barButtonItems = [NSMutableArray array];
if (self.navigationItem.leftBarButtonItems) [barButtonItems addObjectsFromArray:self.navigationItem.leftBarButtonItems];
if (self.navigationItem.rightBarButtonItems) [barButtonItems addObjectsFromArray:self.navigationItem.rightBarButtonItems];
if (_settingsButton) [barButtonItems addObject:_settingsButton];
if (_profilesButton) [barButtonItems addObject:_profilesButton];
if (_addHostButton) [barButtonItems addObject:_addHostButton];
if (_helpButton) [barButtonItems addObject:_helpButton];
if (_upButton) [barButtonItems addObject:_upButton];
for (UIBarButtonItem *barButtonItem in barButtonItems) {
[self applyThemeToNavigationButton:barButtonItem];
if (@available(iOS 26.0, *)) {
barButtonItem.sharesBackground = barButtonItem == _addHostButton || barButtonItem == _helpButton;
}
}
}
- (void)setupNavBar{ - (void)setupNavBar{
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {
UINavigationBarAppearance* appearance = [[UINavigationBarAppearance alloc] init]; UINavigationBarAppearance* appearance = [[UINavigationBarAppearance alloc] init];
@@ -1645,14 +1782,15 @@ static NSMutableSet* hostList;
_profilesButton.tintColor = ThemeManager.appPrimaryColor; _profilesButton.tintColor = ThemeManager.appPrimaryColor;
} }
} else { } else {
[_profilesButton setTitle:[LocalizationHelper localizedStringForKey:@"Game Profile"]]; [_profilesButton setTitle:[LocalizationHelper localizedStringForKey:@"Game Profile"]];
} }
if (@available(iOS 26.0, *)) { if (@available(iOS 26.0, *)) {
_settingsButton.sharesBackground = false; _settingsButton.sharesBackground = false;
_profilesButton.sharesBackground = false; _profilesButton.sharesBackground = false;
_addHostButton.sharesBackground = true;
_helpButton.sharesBackground = true;
} }
@@ -1692,15 +1830,20 @@ static NSMutableSet* hostList;
} }
_settingsButton.tintColor = ThemeManager.appPrimaryColor; _settingsButton.tintColor = ThemeManager.appPrimaryColor;
_profilesButton.tintColor = ThemeManager.appPrimaryColor;
_upButton.tintColor = ThemeManager.appPrimaryColor; _upButton.tintColor = ThemeManager.appPrimaryColor;
((UIButton*)_addHostButton.customView).backgroundColor = PublicUtils.liquidGlassEnabled ? UIColor.clearColor : ThemeManager.appPrimaryColor; ((UIButton*)_addHostButton.customView).backgroundColor = PublicUtils.liquidGlassEnabled ? UIColor.clearColor : ThemeManager.appPrimaryColor;
((UIButton*)_helpButton.customView).tintColor = ThemeManager.appPrimaryColor; ((UIButton*)_helpButton.customView).tintColor = ThemeManager.appPrimaryColor;
[self applyThemeToNavigationControls];
[self applyNavBarAppearance]; [self applyNavBarAppearance];
[self updateTitle]; [self updateTitle];
if (hostViewTitleLabel) { if (hostViewTitleLabel) {
hostViewTitleLabel.textColor = ThemeManager.textColor; hostViewTitleLabel.textColor = ThemeManager.textColor;
} }
if (@available(iOS 13.0, *)) [GamepadNavigationIllustrationHud updateCurrentTheme];
[self.hostCollectionVC updateTheme]; [self.hostCollectionVC updateTheme];
} }
@@ -1847,8 +1990,8 @@ static NSMutableSet* hostList;
//[self simulateSettingsButtonPress]; //force expand setting view if orientation changed since last quit from app. //[self simulateSettingsButtonPress]; //force expand setting view if orientation changed since last quit from app.
//[self updateResolutionAccordingly]; //[self updateResolutionAccordingly];
// SettingsViewController* settingsViewController = (SettingsViewController*)[self.revealViewController rearViewController]; // SettingsViewController* self.settingsViewController = (SettingsViewController*)[self.revealViewController rearViewController];
// [settingsViewController updateResolutionTable]; // [self.settingsViewController updateResolutionTable];
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMenuResize:)]; UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMenuResize:)];
longPress.delaysTouchesBegan = false; longPress.delaysTouchesBegan = false;
@@ -1856,7 +1999,7 @@ static NSMutableSet* hostList;
[self.view addGestureRecognizer:longPress]; [self.view addGestureRecognizer:longPress];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:[self isIPhone]?@"iPhone":@"iPad" bundle:nil]; UIStoryboard *storyboard = [UIStoryboard storyboardWithName:PublicUtils.isIPhone?@"iPhone":@"iPad" bundle:nil];
SettingsViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"settingsViewController"]; SettingsViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"settingsViewController"];
// 强制加载视图 // 强制加载视图
__unused UIView *view = viewController.view; __unused UIView *view = viewController.view;
@@ -1927,6 +2070,7 @@ static NSMutableSet* hostList;
-(void)viewDidLayoutSubviews{ -(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews]; [super viewDidLayoutSubviews];
[self updateHosts]; [self updateHosts];
// if (@available(iOS 13.0, *)) [GamepadNavigationIllustrationHud showInKeyWindow];
} }
// this will also be called back when device orientation changes // this will also be called back when device orientation changes
@@ -2007,7 +2151,7 @@ static NSMutableSet* hostList;
CGPoint point = [gestureRecognizer locationInView:self.collectionView]; CGPoint point = [gestureRecognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point]; NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
if (indexPath != nil) { if (indexPath != nil) {
[self appLongClicked:_sortedAppList[indexPath.row] view:nil]; [self appLongClicked:self.sortedAppList[indexPath.row] view:nil];
} }
} }
@@ -2099,7 +2243,8 @@ static NSMutableSet* hostList;
// [self setupHostViewTitle]; // [self setupHostViewTitle];
// [self reloadScrollHostView]; //remove this for proper test // [self reloadScrollHostView]; //remove this for proper test
[self attachWaterMark]; [self attachWaterMark];
if (@available(iOS 13.0, *)) [GamepadNavigationIllustrationHud showInKeyWindow];
#if !TARGET_OS_TV #if !TARGET_OS_TV
[[self revealViewController] setPrimaryViewController:self]; [[self revealViewController] setPrimaryViewController:self];
@@ -2133,13 +2278,26 @@ static NSMutableSet* hostList;
if([self needPopupAboutView])[self helpButtonTapped]; if([self needPopupAboutView])[self helpButtonTapped];
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {
[ControllerNavigator startWithSwapABXY:false]; ControllerUtil.delegate = self;
[ControllerUtil installControllerObserversIfNeeded];
[ControllerNavigator setRadialMenuDelegate:self]; [ControllerNavigator setRadialMenuDelegate:self];
DataManager* dataMan = [[DataManager alloc] init];
Settings* settings = [dataMan retrieveSettings];
ControllerNavigator.enabled = settings.enableControllerNavigation;
ControllerNavigator.localRadialMenuButton = (ControllerElement)settings.localRadialMenuButton.intValue;
ControllerNavigator.customPositionForLocalRadialMenuButton = (ControllerElementPosition)settings.customLocalRadialMenuButtonPosition.intValue;
ControllerNavigator.customPositionForStreamingRadialMenuButton = (ControllerElementPosition)settings.customStreamingRadialMenuButtonPosition.intValue;
ControllerNavigator.streamingRadialMenuButton = (ControllerElement)settings.streamingRadialMenuButton.intValue;
ControllerNavigator.streamingRadialMenuDelay = (NSTimeInterval)settings.streamingRadialMenuDelay.floatValue;
ControllerNavigator.controllerMouseStick = (ControllerElement)settings.controllerMouseStick.intValue;
ControllerNavigator.controllerMouseLeftButton = (ControllerElement)settings.controllerMouseLeftButton.intValue;
ControllerNavigator.controllerMouseRightButton = (ControllerElement)settings.controllerMouseRightButton.intValue;
ControllerNavigator.controllerMouseExpo = settings.controllerMouseExpo.floatValue;
if(ControllerNavigator.enabled) [ControllerNavigator start];
} }
} }
- (void)viewWillDisappear:(BOOL)animated{ - (void)viewWillDisappear:(BOOL)animated{
NSLog(@"willDisappear");
[super viewWillDisappear:animated]; [super viewWillDisappear:animated];
[_foregroundHostUpdateTimer invalidate]; [_foregroundHostUpdateTimer invalidate];
_foregroundHostUpdateTimer = nil; _foregroundHostUpdateTimer = nil;
@@ -2177,6 +2335,7 @@ static NSMutableSet* hostList;
[self.view addSubview:self.collectionView]; [self.view addSubview:self.collectionView];
[self initHostCollection]; [self initHostCollection];
if(!_enteredAppView) [self switchToHostView]; if(!_enteredAppView) [self switchToHostView];
else if (@available(iOS 13.0, *)) [ControllerNavigator setUINavigationDelegate:self];
[self updateTheme]; [self updateTheme];
} }
@@ -2343,17 +2502,17 @@ static NSMutableSet* hostList;
return; return;
} }
_sortedAppList = [host.appList allObjects]; self.sortedAppList = [host.appList allObjects];
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)]; self.sortedAppList = [self.sortedAppList sortedArrayUsingSelector:@selector(compareName:)];
if (!_showHiddenApps) { if (!_showHiddenApps) {
NSMutableArray* visibleAppList = [NSMutableArray array]; NSMutableArray* visibleAppList = [NSMutableArray array];
for (TemporaryApp* app in _sortedAppList) { for (TemporaryApp* app in self.sortedAppList) {
if (!app.hidden) { if (!app.hidden) {
[visibleAppList addObject:app]; [visibleAppList addObject:app];
} }
} }
_sortedAppList = visibleAppList; self.sortedAppList = visibleAppList;
} }
[self.collectionView reloadData]; [self.collectionView reloadData];
@@ -2370,7 +2529,7 @@ static NSMutableSet* hostList;
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AppCell" forIndexPath:indexPath]; UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AppCell" forIndexPath:indexPath];
TemporaryApp* app = _sortedAppList[indexPath.row]; TemporaryApp* app = self.sortedAppList[indexPath.row];
UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self]; UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self];
appView.updateLoopDelegate = (id<AppViewUpdateLoopDelegate>)self; appView.updateLoopDelegate = (id<AppViewUpdateLoopDelegate>)self;
@@ -2404,16 +2563,16 @@ static NSMutableSet* hostList;
layout:(UICollectionViewLayout *)collectionViewLayout layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath { sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGSize cellSize; CGSize cellSize;
if([self isIPhone]) cellSize.height = 0.365*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds])); if(PublicUtils.isIPhone) cellSize.height = 0.365*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds]));
else cellSize.height = 0.272*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds])); else cellSize.height = 0.272*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds]));
TemporaryApp* app = _sortedAppList[indexPath.row]; TemporaryApp* app = self.sortedAppList[indexPath.row];
UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self]; UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self];
cellSize.width = cellSize.height * (appView cellSize.width = cellSize.height * (appView
.bounds.size.width/appView .bounds.size.width/appView
.bounds.size.height); .bounds.size.height);
// cardSize.width = // cardSize.width =
return cellSize; return cellSize;
} }
@@ -2422,8 +2581,8 @@ static NSMutableSet* hostList;
} }
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (_selectedHost != nil && _sortedAppList != nil) { if (_selectedHost != nil && self.sortedAppList != nil) {
return _sortedAppList.count; return self.sortedAppList.count;
} }
else { else {
return 0; return 0;
@@ -2498,7 +2657,7 @@ static NSMutableSet* hostList;
double delayInSeconds = 0.02; double delayInSeconds = 0.02;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^{ dispatch_after(popTime, dispatch_get_main_queue(), ^{
[self->settingsViewController hideDynamicLabelsWhenOverlapped:self->settingsViewController.parentStack]; [self.settingsViewController hideDynamicLabelsWhenOverlapped:self.settingsViewController.parentStack];
}); });
} }
} }
@@ -2573,7 +2732,7 @@ static NSMutableSet* hostList;
- (CGSize)getHostCardSize{ - (CGSize)getHostCardSize{
CGSize cardSize; CGSize cardSize;
if([self isIPhone]) cardSize.height = 0.37*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds])); if(PublicUtils.isIPhone) cardSize.height = 0.37*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds]));
else cardSize.height = 0.25*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds])); else cardSize.height = 0.25*MIN(CGRectGetHeight([[UIScreen mainScreen] bounds]),CGRectGetWidth([[UIScreen mainScreen] bounds]));
TemporaryHost* dummyHost = [[TemporaryHost alloc] init]; TemporaryHost* dummyHost = [[TemporaryHost alloc] init];
HostCardView* dummyCard = [[HostCardView alloc] initWithHost:dummyHost]; HostCardView* dummyCard = [[HostCardView alloc] initWithHost:dummyHost];
@@ -2593,7 +2752,7 @@ static NSMutableSet* hostList;
if(self.hostCollectionVC.view.superview == nil){ if(self.hostCollectionVC.view.superview == nil){
[self.view addSubview:self.hostCollectionVC.view]; [self.view addSubview:self.hostCollectionVC.view];
CGFloat leftPadding = [self isIPhone] ? 30 : 0; CGFloat leftPadding = PublicUtils.isIPhone ? 30 : 0;
self.hostCollectionVC.view.translatesAutoresizingMaskIntoConstraints = NO; self.hostCollectionVC.view.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[self.hostCollectionVC.view.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:0], [self.hostCollectionVC.view.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:0],
@@ -2617,6 +2776,53 @@ static NSMutableSet* hostList;
} }
} }
- (void)controllerNavigatorDidSelectWithItem:(RadialMenuItem)item API_AVAILABLE(ios(13.0)){
dispatch_async(dispatch_get_main_queue(), ^{
switch (item) {
case RadialMenuItemSettings:
[[self revealViewController] revealToggleAnimated:YES];
break;
case RadialMenuItemAllSettings:
[self.revealViewController allSettingSelected];
break;
case RadialMenuItemFavoriteSettings:
[self.revealViewController favoriteSettingSelected];
break;
case RadialMenuItemGameProfiles:
if([GenericUtils isFirstTappingGameProfileSelectorFromMainFrame]){
DataManager* dataMan = [[DataManager alloc] init];
Settings* settings = [dataMan retrieveSettings];
NSString* edgeSide = settings.slideToSettingsScreenEdge.intValue != UIRectEdgeLeft ? [LocalizationHelper localizedStringForKey:@"left"] : [LocalizationHelper localizedStringForKey:@"right"];
NSString* slideDist = [NSString stringWithFormat:@"%d%%", (int)(settings.slideToSettingsDistance.floatValue*100)];
[AlertControllerUtil showAlertIn:self
title:[LocalizationHelper localizedStringForKey:@"Game Profile"]
message:[LocalizationHelper localizedStringForKey:@"gameProfileIntroduction", edgeSide, slideDist]
withCancel:NO
buttonTitle:[LocalizationHelper localizedStringForKey:@"Got it!"]
countdown:6
action:^{}
completion:^{
if(!self.gameProfileSelectorVC && !self.settingsViewController.layoutOnScreenControlsVC) [self openGameProfileSeletorWithAnimated:true];
}];
}
else if(!self.gameProfileSelectorVC && !self.settingsViewController.layoutOnScreenControlsVC) [self openGameProfileSeletorWithAnimated:true];
else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}];
else if (self.settingsViewController.layoutOnScreenControlsVC) [self.settingsViewController.layoutOnScreenControlsVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}];
break;
case RadialMenuItemHostView:
[self switchToHostView];
break;
case RadialMenuItemAddHost:
[self addHostTapped];
break;
case RadialMenuItemAboutView:
[self helpButtonTapped];
break;
default:
break;
}
});
}
- (void)controllerNavigatorDidSelectSettings { - (void)controllerNavigatorDidSelectSettings {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
@@ -2626,9 +2832,9 @@ static NSMutableSet* hostList;
- (void)controllerNavigatorDidSelectGameProfiles { - (void)controllerNavigatorDidSelectGameProfiles {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if(!self.gameProfileSelectorVC && !self->settingsViewController.layoutOnScreenControlsVC) [self openGameProfileSeletor:true]; if(!self.gameProfileSelectorVC && !self.settingsViewController.layoutOnScreenControlsVC) [self openGameProfileSeletorWithAnimated:true];
else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}]; else if (self.gameProfileSelectorVC) [self.gameProfileSelectorVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}];
else if (self->settingsViewController.layoutOnScreenControlsVC) [self->settingsViewController.layoutOnScreenControlsVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}]; else if (self.settingsViewController.layoutOnScreenControlsVC) [self.settingsViewController.layoutOnScreenControlsVC.oscProfilesTableViewController dismissViewControllerAnimated:true completion:^{}];
}); });
} }
@@ -2640,4 +2846,3 @@ static NSMutableSet* hostList;
} }
@end @end
@@ -243,6 +243,9 @@ typedef NS_ENUM(NSInteger, SWRevealToggleAnimationType)
// Thus, you can disable user interactions on your frontViewController view without affecting the tap recognizer. // Thus, you can disable user interactions on your frontViewController view without affecting the tap recognizer.
- (UITapGestureRecognizer*)tapGestureRecognizer; - (UITapGestureRecognizer*)tapGestureRecognizer;
- (void)favoriteSettingSelected;
- (void)allSettingSelected;
/* The following properties are provided for further customization, they are set to default values on initialization, /* The following properties are provided for further customization, they are set to default values on initialization,
you do not generally have to set them */ you do not generally have to set them */
@@ -10,21 +10,21 @@
// //
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "AppDelegate.h" #import "SWRevealViewController.h"
#import "MainFrameViewController.h"
#import "CustomEdgeSlideGestureRecognizer.h"
#import "VoidLink-Swift.h"
@class LayoutOnScreenControlsViewController; @class LayoutOnScreenControlsViewController;
@class MainFrameViewController;
@class MicHandler; @class MicHandler;
@class WidgetPickerViewController; @class WidgetPickerViewController;
@protocol WidgetPickerViewControllerDelegate;
@interface SettingsViewController : UIViewController <RearNavigationBarMenuDelegate, MenuSectionDelegate, UITextFieldDelegate, WidgetPickerViewControllerDelegate> @interface SettingsViewController : UIViewController <RearNavigationBarMenuDelegate, UITextFieldDelegate>
@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar; @property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar;
@property (strong, nonatomic) UIStackView *parentStack; @property (strong, nonatomic) UIStackView *parentStack;
@property (strong, nonatomic) IBOutlet UIStackView *resolutionStack; @property (strong, nonatomic) IBOutlet UIStackView *resolutionStack;
@property (strong, nonatomic) IBOutlet UIStackView *resolutionSelectorStack;
@property (weak, nonatomic) IBOutlet UIStackView *customResolutionStack;
@property (strong, nonatomic) IBOutlet UIStackView *fpsStack; @property (strong, nonatomic) IBOutlet UIStackView *fpsStack;
@property (strong, nonatomic) IBOutlet UIStackView *bitrateStack; @property (strong, nonatomic) IBOutlet UIStackView *bitrateStack;
@property (strong, nonatomic) IBOutlet UIStackView *touchModeStack; @property (strong, nonatomic) IBOutlet UIStackView *touchModeStack;
@@ -103,7 +103,8 @@
@property (strong, nonatomic) IBOutlet UILabel *mousePointerVelocityFactorUILabel; @property (strong, nonatomic) IBOutlet UILabel *mousePointerVelocityFactorUILabel;
@property (strong, nonatomic) IBOutlet UISegmentedControl *unlockDisplayOrientationSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *unlockDisplayOrientationSelector;
@property (weak, nonatomic) LayoutOnScreenControlsViewController *layoutOnScreenControlsVC; @property (weak, nonatomic) LayoutOnScreenControlsViewController *layoutOnScreenControlsVC;
@property (nonatomic, strong) MainFrameViewController *mainFrameViewController; @property (nonatomic, weak) MainFrameViewController *mainFrameViewController;
@property (strong, nonatomic) UIView *controllerNavigationHighlightOverlayView;
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector;
@@ -231,8 +232,11 @@
@property (weak, nonatomic) IBOutlet UIStackView *softKeyboardHeightStack; @property (weak, nonatomic) IBOutlet UIStackView *softKeyboardHeightStack;
@property (weak, nonatomic) IBOutlet UISwitch *softKeyboardHeightSwitch; @property (weak, nonatomic) IBOutlet UISwitch *softKeyboardHeightSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *controllerToMouseStack; @property (strong, nonatomic) IBOutlet UIStackView *controllerNavigationStack;
@property (strong, nonatomic) IBOutlet UISwitch *controllerToMouseSwitch; @property (strong, nonatomic) IBOutlet UISwitch *controllerNavigationSwitch;
@property (weak, nonatomic) IBOutlet UIStackView *streamingRadialMenuDelayStack;
@property (weak, nonatomic) IBOutlet UISlider *streamingRadialMenuDelaySlider;
@property (strong, nonatomic) IBOutlet UIStackView *controllerMouseVelocityStack; @property (strong, nonatomic) IBOutlet UIStackView *controllerMouseVelocityStack;
@property (strong, nonatomic) IBOutlet UISlider *controllerMouseVelocitySlider; @property (strong, nonatomic) IBOutlet UISlider *controllerMouseVelocitySlider;
@@ -307,11 +311,13 @@
// This is okay because it's just an enum and access uses @available checks // This is okay because it's just an enum and access uses @available checks
@property(nonatomic) UIUserInterfaceStyle overrideUserInterfaceStyle; @property(nonatomic) UIUserInterfaceStyle overrideUserInterfaceStyle;
@property(nonatomic, readonly) SettingsMenuMode currentSettingsMenuMode;
#pragma clang diagnostic pop #pragma clang diagnostic pop
- (bool)hdrSupported; - (bool)hdrSupported;
- (void)saveSettings; - (void)saveSettings;
- (void)saveFavoriteSettingStackIdentifiers;
+ (bool)isLandscapeNow; + (bool)isLandscapeNow;
- (void)updateResolutionTable; - (void)updateResolutionTable;
- (void)widget:(UIView*)widget setEnabled:(bool)enabled; - (void)widget:(UIView*)widget setEnabled:(bool)enabled;
@@ -320,5 +326,7 @@
- (void)setHidden:(BOOL)hidden forStack:(UIStackView* )stack; - (void)setHidden:(BOOL)hidden forStack:(UIStackView* )stack;
- (void)updateCodecDependentSwitches; - (void)updateCodecDependentSwitches;
- (void)mainFrameGameProfileButtonTapped:(bool)animated; - (void)mainFrameGameProfileButtonTapped:(bool)animated;
- (void)addSettingToFavorite:(UIStackView* )settingStack;
- (void)expandGamepadSection;
@end @end
+341 -130
View File
@@ -10,6 +10,8 @@
// //
#import "SettingsViewController.h" #import "SettingsViewController.h"
#import "MainFrameViewController.h"
#import "CustomEdgeSlideGestureRecognizer.h"
#import "TemporarySettings.h" #import "TemporarySettings.h"
#import "DataManager.h" #import "DataManager.h"
#import "VoidLink-Swift.h" #import "VoidLink-Swift.h"
@@ -20,6 +22,10 @@
#import "LocalizationHelper.h" #import "LocalizationHelper.h"
@interface SettingsViewController () <MenuSectionDelegate, WidgetPickerViewControllerDelegate>
@property(nonatomic, assign) SettingsMenuMode currentSettingsMenuMode;
@end
@implementation SettingsViewController { @implementation SettingsViewController {
TemporarySettings* tempSettings; TemporarySettings* tempSettings;
DataManager* dataMan; DataManager* dataMan;
@@ -41,7 +47,6 @@
NSMutableDictionary *_settingStackDict; NSMutableDictionary *_settingStackDict;
NSMutableArray *_favoriteSettingStackIdentifiers; NSMutableArray *_favoriteSettingStackIdentifiers;
bool settingStackWillBeRelocatedToLowestPosition; bool settingStackWillBeRelocatedToLowestPosition;
uint8_t currentSettingsMenuMode;
UIView *snapshot; UIView *snapshot;
UIStackView* capturedStack; UIStackView* capturedStack;
CADisplayLink *_autoScrollDisplayLink; CADisplayLink *_autoScrollDisplayLink;
@@ -54,11 +59,15 @@
MenuSectionView *otherSection; MenuSectionView *otherSection;
MenuSectionView *experimentalSection; MenuSectionView *experimentalSection;
NSMutableSet* hiddenStacks; NSMutableSet* hiddenStacks;
// ControllerNavigator UI navigation state
UIView *_controllerNavigationHighlightOverlayView;
GCController *capturedController; GCController *capturedController;
} }
@dynamic overrideUserInterfaceStyle; @dynamic overrideUserInterfaceStyle;
@synthesize controllerNavigationHighlightOverlayView = _controllerNavigationHighlightOverlayView;
//static NSString* bitrateFormat; //static NSString* bitrateFormat;
@@ -373,7 +382,7 @@ BOOL isCustomResolution(int resolutionSelected) {
self.controllerGyroSwitchButtonSetter.selectedSegmentIndex = oscProfile.controllerGyroSwitchMode; self.controllerGyroSwitchButtonSetter.selectedSegmentIndex = oscProfile.controllerGyroSwitchMode;
if(self.controllerGyroSwitchButtonSetter.selectedSegmentIndex != ControllerGyroSwitchDisabled){ if(self.controllerGyroSwitchButtonSetter.selectedSegmentIndex != ControllerGyroSwitchDisabled){
bool bothButtonsSet = oscProfile.controllerGyroSwitchHold != ControllerButtonNull && oscProfile.controllerGyroSwitchToggle != ControllerButtonNull; bool bothButtonsSet = oscProfile.controllerGyroSwitchHold != ControllerElementNull && oscProfile.controllerGyroSwitchToggle != ControllerElementNull;
[self findDynamicLabelFromStack:self.controllerGyroSwitchButtonStack].text = bothButtonsSet ? [LocalizationHelper localizedStringForKey:@" both set "] : @""; [self findDynamicLabelFromStack:self.controllerGyroSwitchButtonStack].text = bothButtonsSet ? [LocalizationHelper localizedStringForKey:@" both set "] : @"";
} }
[self setHidden:oscProfile.controllerGyroSwitchMode==ControllerGyroSwitchDisabled forStack:self.reverseHoldButtonStack]; [self setHidden:oscProfile.controllerGyroSwitchMode==ControllerGyroSwitchDisabled forStack:self.reverseHoldButtonStack];
@@ -556,7 +565,7 @@ BOOL isCustomResolution(int resolutionSelected) {
else dispatch_async(dispatch_get_main_queue(), ^{[self widget:self.bitrateSlider setEnabled:true];}); else dispatch_async(dispatch_get_main_queue(), ^{[self widget:self.bitrateSlider setEnabled:true];});
}); });
if(currentSettingsMenuMode == AllSettings && MenuSectionView.overridePersistedFoldState){ if(self.currentSettingsMenuMode == AllSettings && MenuSectionView.overridePersistedFoldState){
for(UIView *subview in _parentStack.arrangedSubviews){ for(UIView *subview in _parentStack.arrangedSubviews){
if([subview isKindOfClass:[MenuSectionView class]]){ if([subview isKindOfClass:[MenuSectionView class]]){
MenuSectionView* section = (MenuSectionView* )subview; MenuSectionView* section = (MenuSectionView* )subview;
@@ -619,11 +628,16 @@ BOOL isCustomResolution(int resolutionSelected) {
[self touchModeChanged:self.touchModeSelector1]; // a special fix for iOS 14 to set hidden for the "enableOswStack" [self touchModeChanged:self.touchModeSelector1]; // a special fix for iOS 14 to set hidden for the "enableOswStack"
if(!settingsViewAlreadyAppeared && ![self contentOffsetRestored]) _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, tempSettings.settingsMenuOffset.floatValue); if(!settingsViewAlreadyAppeared) {
if(![self contentOffsetRestored]) _scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, tempSettings.settingsMenuOffset.floatValue);
if(@available(iOS 13.0, *)) if(ControllerUtil.primaryGCController) [self restoreControllerNavigationHighlight];
}
_scrollView.hidden = false; _scrollView.hidden = false;
settingsViewJustExpanded = false; settingsViewJustExpanded = false;
settingsViewAlreadyAppeared = true; settingsViewAlreadyAppeared = true;
if (@available(iOS 13.0, *)) if(ControllerNavigator.radialMenuView.superview) [ControllerNavigator updateRadialMenu];
} }
- (void)viewWillDisappear:(BOOL)animated{ - (void)viewWillDisappear:(BOOL)animated{
@@ -637,11 +651,12 @@ BOOL isCustomResolution(int resolutionSelected) {
bool unlockDisplayOrientationFlipped = tempSettings.unlockDisplayOrientation != (_unlockDisplayOrientationSelector.selectedSegmentIndex == 1); bool unlockDisplayOrientationFlipped = tempSettings.unlockDisplayOrientation != (_unlockDisplayOrientationSelector.selectedSegmentIndex == 1);
if(unlockDisplayOrientationFlipped) [_mainFrameViewController setNeedsUpdateAllowedOrientation]; // handle allow portratit on & off if(unlockDisplayOrientationFlipped) [_mainFrameViewController setNeedsUpdateAllowedOrientation]; // handle allow portratit on & off
if (@available(iOS 13.0, *)) if(ControllerNavigator.radialMenuView.superview) [ControllerNavigator updateRadialMenu];
} }
- (SettingsMenuMode)getSettingsMenuMode{ - (SettingsMenuMode)getSettingsMenuMode{
return currentSettingsMenuMode; return self.currentSettingsMenuMode;
} }
- (void)edgeSwiped { - (void)edgeSwiped {
@@ -666,7 +681,7 @@ BOOL isCustomResolution(int resolutionSelected) {
if(!_parentStack.superview){ if(!_parentStack.superview){
[self.scrollView addSubview:_parentStack]; [self.scrollView addSubview:_parentStack];
[NSLayoutConstraint activateConstraints:@[ [NSLayoutConstraint activateConstraints:@[
[_parentStack.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor constant: currentSettingsMenuMode == AllSettings ? GenericUtils.settingsMenuNavigationBarHeight : GenericUtils.settingsMenuNavigationBarHeight+10], [_parentStack.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor constant: self.currentSettingsMenuMode == AllSettings ? GenericUtils.settingsMenuNavigationBarHeight : GenericUtils.settingsMenuNavigationBarHeight+10],
[_parentStack.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor constant:-20], [_parentStack.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor constant:-20],
]]; ]];
} }
@@ -841,7 +856,7 @@ BOOL isCustomResolution(int resolutionSelected) {
[videoSection setSectionWithIcon:[UIImage systemImageNamed:@"waveform"] size:13 sizeConstraint:-15]; [videoSection setSectionWithIcon:[UIImage systemImageNamed:@"waveform"] size:13 sizeConstraint:-15];
} }
self.resolutionStack.hasDynamicLabel = YES; [self addDynamicLabelForStack:self.resolutionSelectorStack];
[self addSetting:self.resolutionStack ofId:@"resolutionStack" to:videoSection]; [self addSetting:self.resolutionStack ofId:@"resolutionStack" to:videoSection];
[self addSetting:self.fpsStack ofId:@"fpsStack" to:videoSection]; [self addSetting:self.fpsStack ofId:@"fpsStack" to:videoSection];
@@ -933,6 +948,20 @@ BOOL isCustomResolution(int resolutionSelected) {
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {
[controllerSection setSectionWithIcon:[UIImage systemImageNamed:@"gamecontroller"] size:30 sizeConstraint:-10]; [controllerSection setSectionWithIcon:[UIImage systemImageNamed:@"gamecontroller"] size:30 sizeConstraint:-10];
} }
self.controllerNavigationStack.hasInfoTag = YES;
self.controllerNavigationStack.hasDynamicLabel = YES;
[self addSetting:self.controllerNavigationStack ofId:@"controllerNavigationStack" to:controllerSection];
self.streamingRadialMenuDelayStack.hasDynamicLabel = YES;
[self addSetting:self.streamingRadialMenuDelayStack ofId:@"streamingRadialMenuDelayStack" to:controllerSection];
self.controllerMouseVelocityStack.hasDynamicLabel = YES;
[self addSetting:self.controllerMouseVelocityStack ofId:@"controllerMouseVelocityStack" to:controllerSection];
self.controllerMouseExpoStack.hasInfoTag = YES;
self.controllerMouseExpoStack.hasDynamicLabel = YES;
[self addSetting:self.controllerMouseExpoStack ofId:@"controllerMouseExpoStack" to:controllerSection];
[self addSetting:self.swapAbxyStack ofId:@"swapAbaxyStack" to:controllerSection]; [self addSetting:self.swapAbxyStack ofId:@"swapAbaxyStack" to:controllerSection];
[self addSetting:self.hapticEngineStack ofId:@"hapticEngineStack" to:controllerSection]; [self addSetting:self.hapticEngineStack ofId:@"hapticEngineStack" to:controllerSection];
@@ -960,17 +989,6 @@ BOOL isCustomResolution(int resolutionSelected) {
self.rightStickMinOffsetStack.hasDynamicLabel = YES; self.rightStickMinOffsetStack.hasDynamicLabel = YES;
[self addSetting:self.rightStickMinOffsetStack ofId:@"rightStickMinOffsetStack" to:controllerSection]; [self addSetting:self.rightStickMinOffsetStack ofId:@"rightStickMinOffsetStack" to:controllerSection];
self.controllerToMouseStack.hasInfoTag = YES;
self.controllerToMouseStack.hasDynamicLabel = YES;
[self addSetting:self.controllerToMouseStack ofId:@"controllerToMouseStack" to:controllerSection];
self.controllerMouseVelocityStack.hasDynamicLabel = YES;
[self addSetting:self.controllerMouseVelocityStack ofId:@"controllerMouseVelocityStack" to:controllerSection];
self.controllerMouseExpoStack.hasInfoTag = YES;
self.controllerMouseExpoStack.hasDynamicLabel = YES;
[self addSetting:self.controllerMouseExpoStack ofId:@"controllerMouseExpoStack" to:controllerSection];
[controllerSection addToParentStack:_parentStack]; [controllerSection addToParentStack:_parentStack];
@@ -1203,7 +1221,6 @@ BOOL isCustomResolution(int resolutionSelected) {
// self.touchMoveEventIntervalStack.hasDynamicLabel = YES; // self.touchMoveEventIntervalStack.hasDynamicLabel = YES;
// [self addSetting:self.touchMoveEventIntervalStack ofId:@"touchMoveEventIntervalStack" to:experimentalSection]; // [self addSetting:self.touchMoveEventIntervalStack ofId:@"touchMoveEventIntervalStack" to:experimentalSection];
self.relativeTouchSlideThresholdStack.hasInfoTag = YES;
self.relativeTouchSlideThresholdStack.hasDynamicLabel = YES; self.relativeTouchSlideThresholdStack.hasDynamicLabel = YES;
[self addSetting:self.relativeTouchSlideThresholdStack ofId:@"relativeTouchSlideThresholdStack" to:experimentalSection]; [self addSetting:self.relativeTouchSlideThresholdStack ofId:@"relativeTouchSlideThresholdStack" to:experimentalSection];
@@ -1228,6 +1245,11 @@ BOOL isCustomResolution(int resolutionSelected) {
[experimentalSection addToParentStack:_parentStack]; [experimentalSection addToParentStack:_parentStack];
} }
- (void)expandGamepadSection {
if(_currentSettingsMenuMode == FavoriteSettings) [self switchToAllSettings];
[controllerSection setExpanded:true];
}
- (void)handleAutoScroll:(CGPoint)location{ - (void)handleAutoScroll:(CGPoint)location{
bool scrollDown = location.y > self.view.bounds.size.height - 100; bool scrollDown = location.y > self.view.bounds.size.height - 100;
bool scrollUp = location.y < 150; bool scrollUp = location.y < 150;
@@ -1432,7 +1454,7 @@ BOOL isCustomResolution(int resolutionSelected) {
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
CGPoint locationInParentStack = [gesture locationInView:_parentStack]; CGPoint locationInParentStack = [gesture locationInView:_parentStack];
CGPoint locationInRootView = [gesture locationInView:self.view.superview]; CGPoint locationInRootView = [gesture locationInView:self.view.superview];
if(currentSettingsMenuMode == AllSettings &&gesture.state == UIGestureRecognizerStateBegan) { if(self.currentSettingsMenuMode == AllSettings &&gesture.state == UIGestureRecognizerStateBegan) {
[self findCapturedStackByTouchLocation:locationInParentStack]; [self findCapturedStackByTouchLocation:locationInParentStack];
if(capturedStack == nil) return; if(capturedStack == nil) return;
[self highlightedBackgroundForView:capturedStack animateWithDuration:0 completion:nil]; [self highlightedBackgroundForView:capturedStack animateWithDuration:0 completion:nil];
@@ -1452,7 +1474,7 @@ BOOL isCustomResolution(int resolutionSelected) {
_autoScrollDisplayLink = nil; _autoScrollDisplayLink = nil;
} }
if(currentSettingsMenuMode == FavoriteSettings){ if(self.currentSettingsMenuMode == FavoriteSettings){
switch (gesture.state) { switch (gesture.state) {
case UIGestureRecognizerStateBegan: case UIGestureRecognizerStateBegan:
// 创建快照视图 // 创建快照视图
@@ -1517,7 +1539,9 @@ BOOL isCustomResolution(int resolutionSelected) {
if([_favoriteSettingStackIdentifiers containsObject:settingStack.accessibilityIdentifier]) return; if([_favoriteSettingStackIdentifiers containsObject:settingStack.accessibilityIdentifier]) return;
[_favoriteSettingStackIdentifiers addObject:settingStack.accessibilityIdentifier]; if(settingStack.accessibilityIdentifier) [_favoriteSettingStackIdentifiers addObject:settingStack.accessibilityIdentifier];
else if(settingStack.superview.accessibilityIdentifier) [_favoriteSettingStackIdentifiers addObject:settingStack.superview.accessibilityIdentifier];
for(NSString *identifier in _favoriteSettingStackIdentifiers){ for(NSString *identifier in _favoriteSettingStackIdentifiers){
NSLog(@"favorite setting: %@", identifier); NSLog(@"favorite setting: %@", identifier);
} }
@@ -1590,6 +1614,7 @@ BOOL isCustomResolution(int resolutionSelected) {
NSString* tipText = @""; NSString* tipText = @"";
NSString* onlineDocLink = @""; NSString* onlineDocLink = @"";
bool showOnlineDocAction = false; bool showOnlineDocAction = false;
tipText = @""; tipText = @"";
if([sender.superview.accessibilityIdentifier isEqualToString: @"bitrateStack"]){ if([sender.superview.accessibilityIdentifier isEqualToString: @"bitrateStack"]){
@@ -1722,9 +1747,9 @@ BOOL isCustomResolution(int resolutionSelected) {
tipText = [LocalizationHelper localizedStringForKey:@"controllerGyroSwitchButtonStackTip", [ControllerUtil stringFor:oscProfile.controllerGyroSwitchToggle], [ControllerUtil stringFor:oscProfile.controllerGyroSwitchHold]]; tipText = [LocalizationHelper localizedStringForKey:@"controllerGyroSwitchButtonStackTip", [ControllerUtil stringFor:oscProfile.controllerGyroSwitchToggle], [ControllerUtil stringFor:oscProfile.controllerGyroSwitchHold]];
showOnlineDocAction = false; showOnlineDocAction = false;
} }
if([sender.superview.accessibilityIdentifier isEqualToString: @"controllerToMouseStack"]){ if([sender.superview.accessibilityIdentifier isEqualToString: @"controllerNavigationStack"]){
tempSettings = [dataMan getSettings]; tempSettings = [dataMan getSettings];
tipText = [LocalizationHelper localizedStringForKey:@"controllerToMouseStackTip", [ControllerUtil stringFor:tempSettings.controllerMouseSwitch.intValue], [LocalizationHelper localizedStringForKey:tempSettings.controllerMouseStick.intValue == LeftStickToMouse ? @"Left stick" : @"Right stick"], [ControllerUtil stringFor:tempSettings.controllerMouseLeftButton.intValue], [ControllerUtil stringFor:tempSettings.controllerMouseRightButton.intValue]]; tipText = [LocalizationHelper localizedStringForKey:@"controllerNavigationStackTip", [ControllerUtil stringFor:tempSettings.localRadialMenuButton.intValue], [ControllerUtil stringFor:tempSettings.streamingRadialMenuButton.intValue], [ControllerUtil stringFor:tempSettings.controllerMouseStick.intValue], [ControllerUtil stringFor:tempSettings.controllerMouseLeftButton.intValue], [ControllerUtil stringFor:tempSettings.controllerMouseRightButton.intValue]];
showOnlineDocAction = false; showOnlineDocAction = false;
} }
if([sender.superview.accessibilityIdentifier isEqualToString: @"reverseHoldButtonStack"]){ if([sender.superview.accessibilityIdentifier isEqualToString: @"reverseHoldButtonStack"]){
@@ -1846,7 +1871,7 @@ BOOL isCustomResolution(int resolutionSelected) {
//switchToAll/Favorite 调用此方法时,这些hiddenStack已身处新的superView中, 可以正常执行hidden = YES //switchToAll/Favorite 调用此方法时,这些hiddenStack已身处新的superView中, 可以正常执行hidden = YES
for(UIStackView* stack in hiddenStacks) stack.hidden = YES; for(UIStackView* stack in hiddenStacks) stack.hidden = YES;
if(currentSettingsMenuMode == AllSettings){ if(self.currentSettingsMenuMode == AllSettings){
for(MenuSectionView* section in _parentStack.arrangedSubviews) [section updateViewForFoldState]; for(MenuSectionView* section in _parentStack.arrangedSubviews) [section updateViewForFoldState];
} }
[self hideDynamicLabelsWhenOverlapped:self.parentStack]; [self hideDynamicLabelsWhenOverlapped:self.parentStack];
@@ -1857,7 +1882,7 @@ BOOL isCustomResolution(int resolutionSelected) {
for(UIStackView* stack in hiddenStacks) { for(UIStackView* stack in hiddenStacks) {
stack.hidden = NO; stack.hidden = NO;
} }
if(currentSettingsMenuMode == AllSettings){ if(self.currentSettingsMenuMode == AllSettings){
for(UIView* view in _parentStack.arrangedSubviews){ for(UIView* view in _parentStack.arrangedSubviews){
if([view isKindOfClass:[MenuSectionView class]]){ if([view isKindOfClass:[MenuSectionView class]]){
MenuSectionView* section = (MenuSectionView* )view; MenuSectionView* section = (MenuSectionView* )view;
@@ -1876,20 +1901,16 @@ BOOL isCustomResolution(int resolutionSelected) {
- (void)switchToFavoriteSettings{ - (void)switchToFavoriteSettings{
[self forceRestoreHeightTemporarilyForSettingStackParentView]; [self forceRestoreHeightTemporarilyForSettingStackParentView];
[_parentStack removeFromSuperview]; [_parentStack removeFromSuperview];
currentSettingsMenuMode = FavoriteSettings; self.currentSettingsMenuMode = FavoriteSettings;
[self initParentStack]; [self initParentStack];
[self updateTheme]; [self updateTheme];
Settings *currentSettings = [dataMan retrieveSettings]; Settings *currentSettings = [dataMan retrieveSettings];
currentSettings.settingsMenuMode = [NSNumber numberWithInteger:currentSettingsMenuMode]; currentSettings.settingsMenuMode = [NSNumber numberWithInteger:self.currentSettingsMenuMode];
[dataMan saveData]; [dataMan saveData];
_parentStack.spacing = [self isIPhone] ? 10 : 12; _parentStack.spacing = [self isIPhone] ? 10 : 12;
[self loadFavoriteSettingStackIdentifiers]; [self loadFavoriteSettingStackIdentifiers];
for(NSString* settingIdentifier in _favoriteSettingStackIdentifiers){
[_parentStack addArrangedSubview:_settingStackDict[settingIdentifier]];
}
for(NSString* settingIdentifier in _favoriteSettingStackIdentifiers){ for(NSString* settingIdentifier in _favoriteSettingStackIdentifiers){
[_parentStack addArrangedSubview:_settingStackDict[settingIdentifier]]; [_parentStack addArrangedSubview:_settingStackDict[settingIdentifier]];
} }
@@ -1903,11 +1924,20 @@ BOOL isCustomResolution(int resolutionSelected) {
[self hideDynamicLabelsWhenOverlapped:self.parentStack]; [self hideDynamicLabelsWhenOverlapped:self.parentStack];
[self layoutSettingsView]; [self layoutSettingsView];
dispatch_async(dispatch_get_main_queue(), ^{
if (@available(iOS 13.0, *)) {
if(ControllerNavigator.radialMenuView.superview) [ControllerNavigator updateRadialMenu];
[GamepadNavigationIllustrationHud updateHud];
if(ControllerUtil.primaryGCController){
[ControllerNavigator restoreSettingsModeSwitchHighlight];
};
}
});
} }
- (void)switchToAllSettings{ - (void)switchToAllSettings{
[self forceRestoreHeightTemporarilyForSettingStackParentView]; [self forceRestoreHeightTemporarilyForSettingStackParentView];
currentSettingsMenuMode = AllSettings; self.currentSettingsMenuMode = AllSettings;
[_parentStack removeFromSuperview]; [_parentStack removeFromSuperview];
[self initParentStack]; [self initParentStack];
[self layoutSections]; [self layoutSections];
@@ -1915,13 +1945,22 @@ BOOL isCustomResolution(int resolutionSelected) {
[self updateTheme]; [self updateTheme];
//[self doneRemoveSettingItem]; //[self doneRemoveSettingItem];
Settings *currentSettings = [dataMan retrieveSettings]; Settings *currentSettings = [dataMan retrieveSettings];
currentSettings.settingsMenuMode = [NSNumber numberWithInteger:currentSettingsMenuMode]; currentSettings.settingsMenuMode = [NSNumber numberWithInteger:self.currentSettingsMenuMode];
[dataMan saveData]; [dataMan saveData];
[self layoutSettingsView]; [self layoutSettingsView];
dispatch_async(dispatch_get_main_queue(), ^{
if (@available(iOS 13.0, *)){
if(ControllerNavigator.radialMenuView.superview) [ControllerNavigator updateRadialMenu];
[GamepadNavigationIllustrationHud updateHud];
if(ControllerUtil.primaryGCController){
[ControllerNavigator restoreSettingsModeSwitchHighlight];
};
}
});
} }
- (void)enterRemoveSettingItemMode{ - (void)enterRemoveSettingItemMode{
currentSettingsMenuMode = RemoveSettingItem; self.currentSettingsMenuMode = RemoveSettingItem;
for(UIStackView* stack in _parentStack.arrangedSubviews){ for(UIStackView* stack in _parentStack.arrangedSubviews){
for(UIView* view in stack.subviews){ for(UIView* view in stack.subviews){
if([view.accessibilityIdentifier isEqualToString:@"infoButton"]) view.hidden = YES; if([view.accessibilityIdentifier isEqualToString:@"infoButton"]) view.hidden = YES;
@@ -1933,7 +1972,7 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
- (void)doneRemoveSettingItem{ - (void)doneRemoveSettingItem{
currentSettingsMenuMode = FavoriteSettings; self.currentSettingsMenuMode = FavoriteSettings;
for(UIStackView* stack in _parentStack.arrangedSubviews){ for(UIStackView* stack in _parentStack.arrangedSubviews){
//stack.userInteractionEnabled = true; //stack.userInteractionEnabled = true;
for(UIView* view in stack.subviews){ for(UIView* view in stack.subviews){
@@ -1960,7 +1999,7 @@ BOOL isCustomResolution(int resolutionSelected) {
- (void)saveFavoriteSettingStackIdentifiers { - (void)saveFavoriteSettingStackIdentifiers {
if(currentSettingsMenuMode != AllSettings){ if(self.currentSettingsMenuMode != AllSettings){
[_favoriteSettingStackIdentifiers removeAllObjects]; [_favoriteSettingStackIdentifiers removeAllObjects];
//for(NSInteger i = 0; i < parentStack.arrangedSubviews.count; i++){ //for(NSInteger i = 0; i < parentStack.arrangedSubviews.count; i++){
for(NSInteger i = 0; i < _parentStack.arrangedSubviews.count; i++){ for(NSInteger i = 0; i < _parentStack.arrangedSubviews.count; i++){
@@ -2061,7 +2100,7 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
self->currentSettingsMenuMode = self->tempSettings.settingsMenuMode.intValue; self.currentSettingsMenuMode = self->tempSettings.settingsMenuMode.intValue;
[self loadFavoriteSettingStackIdentifiers]; [self loadFavoriteSettingStackIdentifiers];
if(self->tempSettings.settingsMenuMode.intValue == FavoriteSettings) [self switchToFavoriteSettings]; if(self->tempSettings.settingsMenuMode.intValue == FavoriteSettings) [self switchToFavoriteSettings];
@@ -2074,6 +2113,9 @@ BOOL isCustomResolution(int resolutionSelected) {
// CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale; // CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
// CGFloat fullScreenWidth = window.frame.size.width * screenScale; // CGFloat fullScreenWidth = window.frame.size.width * screenScale;
// CGFloat fullScreenHeight = window.frame.size.height * screenScale; // CGFloat fullScreenHeight = window.frame.size.height * screenScale;
self.resolutionSelectorStack.accessibilityIdentifier = @"resolutionSelectorStack";
self.customResolutionStack.accessibilityIdentifier = @"customResolutionStack";
[self.resolutionSelector removeSegmentAtIndex:0 animated:NO]; // remove 360p [self.resolutionSelector removeSegmentAtIndex:0 animated:NO]; // remove 360p
[self.resolutionSelector removeSegmentAtIndex:5 animated:NO]; // remove custom segment [self.resolutionSelector removeSegmentAtIndex:5 animated:NO]; // remove custom segment
@@ -2418,15 +2460,15 @@ BOOL isCustomResolution(int resolutionSelected) {
[self.onScreenWidgetSelector sendActionsForControlEvents:UIControlEventValueChanged]; [self.onScreenWidgetSelector sendActionsForControlEvents:UIControlEventValueChanged];
/* /*
// touch move event interval for native-touch.
[self.touchMoveEventIntervalSlider setValue:self->tempSettings.touchMoveEventInterval.intValue animated:NO]; // Load old setting. [self.touchMoveEventIntervalSlider setValue:self->tempSettings.touchMoveEventInterval.intValue animated:NO]; // Load old setting.
[self.touchMoveEventIntervalSlider addTarget:self action:@selector(touchMoveEventIntervalSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. [self.touchMoveEventIntervalSlider addTarget:self action:@selector(touchMoveEventIntervalSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
[self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider]; [self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider];
*/
// touch move event interval for native-touch.
[self.leftClickDelaySlider setValue:self->tempSettings.leftClickDelayMs.intValue animated:NO]; // Load old setting. [self.leftClickDelaySlider setValue:self->tempSettings.leftClickDelayMs.intValue animated:NO]; // Load old setting.
[self.leftClickDelaySlider addTarget:self action:@selector(leftClickDelaySliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. [self.leftClickDelaySlider addTarget:self action:@selector(leftClickDelaySliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
[self leftClickDelaySliderMoved:self.leftClickDelaySlider]; [self.leftClickDelaySlider sendActionsForControlEvents:UIControlEventValueChanged];
*/
// touchMode refactored to game profile system // touchMode refactored to game profile system
[self.touchModeSelector1 addTarget:self action:@selector(touchMode1Changed:) forControlEvents:UIControlEventValueChanged]; [self.touchModeSelector1 addTarget:self action:@selector(touchMode1Changed:) forControlEvents:UIControlEventValueChanged];
@@ -2437,12 +2479,16 @@ BOOL isCustomResolution(int resolutionSelected) {
[self.sendDummyEventSwitch setOn:self->tempSettings.sendDummyEvent];// Load old setting [self.sendDummyEventSwitch setOn:self->tempSettings.sendDummyEvent];// Load old setting
[self.controllerToMouseSwitch setOn:self->tempSettings.mapControllerToMouse]; [self.controllerNavigationSwitch setOn:self->tempSettings.enableControllerNavigation];
[self.controllerToMouseSwitch addTarget:self action:@selector(controllerToMouseSwitchFlipped:) forControlEvents:UIControlEventValueChanged]; [self.controllerNavigationSwitch addTarget:self action:@selector(controllerNavigationSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged]; [self.controllerNavigationSwitch sendActionsForControlEvents:UIControlEventValueChanged];
[self.controllerGyroSwitchButtonSetter addTarget:self action:@selector(controllerGyroSwitchModeChanged:) forControlEvents:UIControlEventValueChanged]; [self.controllerGyroSwitchButtonSetter addTarget:self action:@selector(controllerGyroSwitchModeChanged:) forControlEvents:UIControlEventValueChanged];
[self.streamingRadialMenuDelaySlider setValue:self->tempSettings.streamingRadialMenuDelay.floatValue];
[self.streamingRadialMenuDelaySlider addTarget:self action:@selector(streamingRadialMenuDelaySliderMoved:) forControlEvents:UIControlEventValueChanged];
[self.streamingRadialMenuDelaySlider sendActionsForControlEvents:UIControlEventValueChanged];
[self.controllerMouseVelocitySlider setValue:self->tempSettings.controllerMousePointerVelocity.floatValue]; [self.controllerMouseVelocitySlider setValue:self->tempSettings.controllerMousePointerVelocity.floatValue];
[self.controllerMouseVelocitySlider addTarget:self action:@selector(controllerMouseVelocitySliderMoved:) forControlEvents:UIControlEventValueChanged]; [self.controllerMouseVelocitySlider addTarget:self action:@selector(controllerMouseVelocitySliderMoved:) forControlEvents:UIControlEventValueChanged];
[self.controllerMouseVelocitySlider sendActionsForControlEvents:UIControlEventValueChanged]; [self.controllerMouseVelocitySlider sendActionsForControlEvents:UIControlEventValueChanged];
@@ -2511,10 +2557,13 @@ BOOL isCustomResolution(int resolutionSelected) {
NSLog(@"OK button tapped with no change"); NSLog(@"OK button tapped with no change");
} */ } */
// Continue execution after the alert is dismissed [alertController dismissViewControllerAnimated:true completion:^{
if (!self->_mainFrameViewController.settingsExpandedInStreamView) { // Continue execution after the alert is dismissed
[self invokeOscLayout]; // Don't open osc layout tool immediately during streaming if (!self->_mainFrameViewController.settingsExpandedInStreamView) {
} [self invokeOscLayout]; // Don't open osc layout tool immediately during streaming
}
}];
/* /*
[self findDynamicLabelFromStack:self.onScreenWidgetStack].text = [self isCustomOswEnabled] ? [LocalizationHelper localizedStringForKey:@"%d finger tap", self->oswLayoutFingers] : @""; [self findDynamicLabelFromStack:self.onScreenWidgetStack].text = [self isCustomOswEnabled] ? [LocalizationHelper localizedStringForKey:@"%d finger tap", self->oswLayoutFingers] : @"";
[self handleOswGestureChange]; */ [self handleOswGestureChange]; */
@@ -2725,16 +2774,18 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
[self->dataMan saveData]; [self->dataMan saveData];
}]; }];
[AlertControllerUtil showAlertIn:self [AlertControllerUtil.alertController dismissViewControllerAnimated:true completion:^{
title:[LocalizationHelper localizedStringForKey:@"Drift Correction"] [AlertControllerUtil showAlertIn:self
message:[LocalizationHelper localizedStringForKey:@"Calibrating..."] title:[LocalizationHelper localizedStringForKey:@"Drift Correction"]
withCancel:NO message:[LocalizationHelper localizedStringForKey:@"Calibrating..."]
buttonTitle:[LocalizationHelper localizedStringForKey:@"Finished!"] withCancel:NO
countdown:6 buttonTitle:[LocalizationHelper localizedStringForKey:@"Finished!"]
action:^{} countdown:6
completion:^{ action:^{}
self.mapGyroToSelector.selectedSegmentIndex = self->oscProfile.mapGyroTo; completion:^{
[self.mapGyroToSelector sendActionsForControlEvents:UIControlEventValueChanged]; self.mapGyroToSelector.selectedSegmentIndex = self->oscProfile.mapGyroTo;
[self.mapGyroToSelector sendActionsForControlEvents:UIControlEventValueChanged];
}];
}]; }];
} }
}]; }];
@@ -2766,20 +2817,32 @@ BOOL isCustomResolution(int resolutionSelected) {
if(self->capturedController){ if(self->capturedController){
__weak typeof(self) weakSelf = self; __weak typeof(self) weakSelf = self;
[ControllerUtil listenWithController:self->capturedController swapABXY:false handler:^(NSDictionary * buttonDict, GCExtendedGamepad * gamepad, GCControllerElement * element) { [ControllerUtil listenWithController:self->capturedController swapABXY:false handler:^(NSDictionary * elementDict, GCExtendedGamepad * gamepad, GCControllerElement * element) {
__strong typeof(weakSelf) self = weakSelf; __strong typeof(weakSelf) self = weakSelf;
if (!self) return; if (!self) return;
for(NSNumber* buttonFlagId in buttonDict){ for(NSNumber* elementEnumInstance in elementDict){
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId]; GCControllerElement * element = (GCControllerButtonInput *)elementDict[elementEnumInstance];
if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
GCControllerButtonInput * button = (GCControllerButtonInput *)element;
if(button.isPressed){ if(button.isPressed){
if(sender.selectedSegmentIndex == ControllerGyroSwitchPressToToggle) self->oscProfile.controllerGyroSwitchToggle = buttonFlagId.intValue; if(sender.selectedSegmentIndex == ControllerGyroSwitchPressToToggle) self->oscProfile.controllerGyroSwitchToggle = elementEnumInstance.intValue;
if(sender.selectedSegmentIndex == ControllerGyroSwitchHoldDown) self->oscProfile.controllerGyroSwitchHold = buttonFlagId.intValue; if(sender.selectedSegmentIndex == ControllerGyroSwitchHoldDown) self->oscProfile.controllerGyroSwitchHold = elementEnumInstance.intValue;
switchButtonCaptured = true; switchButtonCaptured = true;
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Finished"]; AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Finished"];
for(UIAlertAction* action in AlertControllerUtil.alertController.actions) {
action.enabled = false;
}
gamepad.valueChangedHandler = nil; gamepad.valueChangedHandler = nil;
if(confirmAction) [confirmAction setValue:[LocalizationHelper localizedStringForKey:@"OK"] forKey:@"title"]; if(confirmAction) [confirmAction setValue:[LocalizationHelper localizedStringForKey:@"OK"] forKey:@"title"];
[self->oscProfileMan replaceSelectedProfileWith:self->oscProfile overwriteDefault:true]; [self->oscProfileMan replaceSelectedProfileWith:self->oscProfile overwriteDefault:true];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[AlertControllerUtil.alertController dismissViewControllerAnimated:NO completion:nil];
if(AlertControllerUtil.completion) AlertControllerUtil.completion();
});
} }
} }
}]; }];
@@ -2807,14 +2870,14 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
completion:^{ completion:^{
if(!switchButtonCaptured){ if(!switchButtonCaptured){
if(sender.selectedSegmentIndex == ControllerGyroSwitchPressToToggle) self->oscProfile.controllerGyroSwitchToggle = ControllerButtonNull; if(sender.selectedSegmentIndex == ControllerGyroSwitchPressToToggle) self->oscProfile.controllerGyroSwitchToggle = ControllerElementNull;
if(sender.selectedSegmentIndex == ControllerGyroSwitchHoldDown) self->oscProfile.controllerGyroSwitchHold = ControllerButtonNull; if(sender.selectedSegmentIndex == ControllerGyroSwitchHoldDown) self->oscProfile.controllerGyroSwitchHold = ControllerElementNull;
[self->oscProfileMan replaceSelectedProfileWith:self->oscProfile overwriteDefault:true]; [self->oscProfileMan replaceSelectedProfileWith:self->oscProfile overwriteDefault:true];
} }
bool noSwitchButtonSet = self->oscProfile.controllerGyroSwitchHold == ControllerButtonNull && self->oscProfile.controllerGyroSwitchToggle == ControllerButtonNull; bool noSwitchButtonSet = self->oscProfile.controllerGyroSwitchHold == ControllerElementNull && self->oscProfile.controllerGyroSwitchToggle == ControllerElementNull;
bool duplicatedButtons = self->oscProfile.controllerGyroSwitchHold == self->oscProfile.controllerGyroSwitchToggle && self->oscProfile.controllerGyroSwitchToggle != ControllerButtonNull; bool duplicatedButtons = self->oscProfile.controllerGyroSwitchHold == self->oscProfile.controllerGyroSwitchToggle && self->oscProfile.controllerGyroSwitchToggle != ControllerElementNull;
bool bothButtonsSet = self->oscProfile.controllerGyroSwitchHold != ControllerButtonNull && self->oscProfile.controllerGyroSwitchToggle != ControllerButtonNull && !duplicatedButtons; bool bothButtonsSet = self->oscProfile.controllerGyroSwitchHold != ControllerElementNull && self->oscProfile.controllerGyroSwitchToggle != ControllerElementNull && !duplicatedButtons;
tipLabel.text = @""; tipLabel.text = @"";
if(bothButtonsSet) tipLabel.text = [LocalizationHelper localizedStringForKey:@" both set "]; if(bothButtonsSet) tipLabel.text = [LocalizationHelper localizedStringForKey:@" both set "];
if(duplicatedButtons){ if(duplicatedButtons){
@@ -2824,7 +2887,7 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
if(noSwitchButtonSet) sender.selectedSegmentIndex = ControllerGyroSwitchDisabled; if(noSwitchButtonSet) sender.selectedSegmentIndex = ControllerGyroSwitchDisabled;
if(!noSwitchButtonSet && !bothButtonsSet && !duplicatedButtons){ if(!noSwitchButtonSet && !bothButtonsSet && !duplicatedButtons){
sender.selectedSegmentIndex = self->oscProfile.controllerGyroSwitchToggle != ControllerButtonNull ? ControllerGyroSwitchPressToToggle : ControllerGyroSwitchHoldDown; sender.selectedSegmentIndex = self->oscProfile.controllerGyroSwitchToggle != ControllerElementNull ? ControllerGyroSwitchPressToToggle : ControllerGyroSwitchHoldDown;
} }
[self setHidden:sender.selectedSegmentIndex==ControllerGyroSwitchDisabled forStack:self.reverseHoldButtonStack]; [self setHidden:sender.selectedSegmentIndex==ControllerGyroSwitchDisabled forStack:self.reverseHoldButtonStack];
@@ -2834,21 +2897,26 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
} }
- (void)controllerToMouseSwitchFlipped:(UISwitch* )sender{ - (void)controllerNavigationSwitchFlipped:(UISwitch* )sender{
[self setHidden:!sender.isOn forStack:_streamingRadialMenuDelayStack];
[self setHidden:!sender.isOn forStack:_controllerMouseVelocityStack]; [self setHidden:!sender.isOn forStack:_controllerMouseVelocityStack];
[self setHidden:!sender.isOn forStack:_controllerMouseExpoStack]; [self setHidden:!sender.isOn forStack:_controllerMouseExpoStack];
if(!sender.isOn || settingsViewJustLoaded) return; if(!sender.isOn || settingsViewJustLoaded) return;
[self.swapAbxySwitch setOn:false]; // swapAbxy can be enabled after mouse to controller is set [self.swapAbxySwitch setOn:false]; // swapAbxy can be enabled after mouse to controller is set
__block bool switchButtonCaptured = false; __block bool localRadialMenuButtonCaptured = false;
__block bool streamingRadialMenuButtonCaptured = false;
__block bool previousTriggerPressed = false;
__block bool stickCaptured = false; __block bool stickCaptured = false;
__block bool leftButtonCaptured = false; __block bool leftButtonCaptured = false;
__block bool rightButtonCaptured = false; __block bool rightButtonCaptured = false;
__block bool metRadialButtonWithUndefinedPosition = false;
__block NSMutableSet *capturedButtons = [NSMutableSet new]; __block NSMutableSet *capturedButtons = [NSMutableSet new];
Settings* currentSettings = [dataMan retrieveSettings]; Settings* currentSettings = [dataMan retrieveSettings];
[AlertControllerUtil showAlertIn:self [AlertControllerUtil showAlertIn:self
title:[LocalizationHelper localizedStringForKey:@"Map controller to mouse"] title:[LocalizationHelper localizedStringForKey:@"Controller Navigation"]
message:[LocalizationHelper localizedStringForKey:@"mouseModeSwitchButtonTip"] message:[LocalizationHelper localizedStringForKey:@"radialMenuButtonTip"]
withCancel:YES withCancel:NO
buttonTitle:@"" buttonTitle:@""
countdown:0 countdown:0
action:^{ action:^{
@@ -2861,42 +2929,152 @@ BOOL isCustomResolution(int resolutionSelected) {
element.preferredSystemGestureState = GCSystemGestureStateDisabled; element.preferredSystemGestureState = GCSystemGestureStateDisabled;
} }
} }
[ControllerUtil stopListeningPrimaryControllerWithStopListenToRadialMenuButton:true];
[ControllerUtil listenWithController:self->capturedController swapABXY:false handler:^(NSDictionary * buttonDict, GCExtendedGamepad * gamepad, GCControllerElement * element) { [ControllerUtil listenWithController:self->capturedController swapABXY:false handler:^(NSDictionary * elementDict, GCExtendedGamepad * gamepad, GCControllerElement * element) {
__strong typeof(weakSelf) self = weakSelf; __strong typeof(weakSelf) self = weakSelf;
if (!self) return; if (!self) return;
if(!switchButtonCaptured){ if(!localRadialMenuButtonCaptured){
for(NSNumber* buttonFlagId in buttonDict){ for(NSNumber* elementEnumInstance in elementDict){
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId]; if (@available(iOS 13.0, *)) if(![ControllerNavigator.radialMenuButtonPool containsObject:elementEnumInstance]) continue;
if(button.isPressed && ![capturedButtons containsObject:buttonFlagId]){ GCControllerElement * element = (GCControllerButtonInput *)elementDict[elementEnumInstance];
currentSettings.controllerMouseSwitch = buttonFlagId; if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
[capturedButtons addObject:buttonFlagId]; GCControllerButtonInput * button = (GCControllerButtonInput *)element;
switchButtonCaptured = true; if(button.isPressed){
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Move the stick you wish to use for mouse control. Another stick will be used for vertical & horizontal scroll."]; currentSettings.localRadialMenuButton = elementEnumInstance;
[capturedButtons addObject:elementEnumInstance];
ControllerElementPosition position = [ControllerUtil positionFor:(ControllerElement)currentSettings.localRadialMenuButton.intValue];
metRadialButtonWithUndefinedPosition = position == ControllerElementPositionUndefined || position == ControllerElementPositionMiddle;
localRadialMenuButtonCaptured = true;
if(button == gamepad.leftTrigger || button == gamepad.rightTrigger) previousTriggerPressed = true;
if(metRadialButtonWithUndefinedPosition) {
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Which side of the controller is this button on?"];
if (@available(iOS 13.0, *)) [ControllerNavigator updateHudForCustomRadialMenuButtonPosition];
return;
}
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Press the same or a different button for controller navigation during streaming."];
return;
}
else if(button == gamepad.leftTrigger || button == gamepad.rightTrigger) previousTriggerPressed = false;
}
}
if(metRadialButtonWithUndefinedPosition && localRadialMenuButtonCaptured && !streamingRadialMenuButtonCaptured){
for(NSNumber* elementEnumInstance in elementDict){
GCControllerElement * element = (GCControllerButtonInput *)elementDict[elementEnumInstance];
if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
GCControllerButtonInput * button = (GCControllerButtonInput *)element;
if(button.isPressed){
switch (elementEnumInstance.intValue) {
case ControllerElementDpadLeft:
currentSettings.customLocalRadialMenuButtonPosition = @(ControllerElementPositionLeft);
metRadialButtonWithUndefinedPosition = false;
break;
case ControllerElementDpadRight:
currentSettings.customLocalRadialMenuButtonPosition = @(ControllerElementPositionRight);
metRadialButtonWithUndefinedPosition = false;
break;
case ControllerElementDpadUp:
currentSettings.customLocalRadialMenuButtonPosition = @(ControllerElementPositionMiddle);
metRadialButtonWithUndefinedPosition = false;
break;
default:
break;
}
if(!metRadialButtonWithUndefinedPosition){
if (@available(iOS 13.0, *)) [GamepadNavigationIllustrationHud clearHud];
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Press the same or a different button for controller navigation during streaming."];
return;
}
}
}
}
if(!streamingRadialMenuButtonCaptured && localRadialMenuButtonCaptured && !metRadialButtonWithUndefinedPosition){
for(NSNumber* elementEnumInstance in elementDict){
if (@available(iOS 13.0, *)) if(![ControllerNavigator.radialMenuButtonPool containsObject:elementEnumInstance]) continue;
GCControllerElement * element = (GCControllerButtonInput *)elementDict[elementEnumInstance];
if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
GCControllerButtonInput * button = (GCControllerButtonInput *)element;
if(button == gamepad.leftTrigger || button == gamepad.rightTrigger) {
if(button.isPressed == previousTriggerPressed) continue;
}
if(button.isPressed){
currentSettings.streamingRadialMenuButton = elementEnumInstance;
[capturedButtons addObject:elementEnumInstance];
streamingRadialMenuButtonCaptured = true;
ControllerElementPosition position = [ControllerUtil positionFor:(ControllerElement)currentSettings.streamingRadialMenuButton.intValue];
metRadialButtonWithUndefinedPosition = currentSettings.streamingRadialMenuButton.intValue != currentSettings.localRadialMenuButton.intValue && (position == ControllerElementPositionUndefined || position == ControllerElementPositionMiddle);
if(currentSettings.streamingRadialMenuButton.intValue == currentSettings.localRadialMenuButton.intValue) currentSettings.customStreamingRadialMenuButtonPosition = currentSettings.customLocalRadialMenuButtonPosition;
if(metRadialButtonWithUndefinedPosition) {
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Which side of the controller is this button on?"];
if (@available(iOS 13.0, *)) [ControllerNavigator updateHudForCustomRadialMenuButtonPosition];
return;
}
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Move the stick you want to use for mouse control. The other stick will be used for vertical and horizontal scrolling"];
return; return;
} }
} }
} }
if(!stickCaptured && switchButtonCaptured){ if(metRadialButtonWithUndefinedPosition && streamingRadialMenuButtonCaptured && localRadialMenuButtonCaptured){
for(NSNumber* elementEnumInstance in elementDict){
GCControllerElement * element = (GCControllerButtonInput *)elementDict[elementEnumInstance];
if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
GCControllerButtonInput * button = (GCControllerButtonInput *)element;
if(button.isPressed){
switch (elementEnumInstance.intValue) {
case ControllerElementDpadLeft:
currentSettings.customStreamingRadialMenuButtonPosition = @(ControllerElementPositionLeft);
metRadialButtonWithUndefinedPosition = false;
break;
case ControllerElementDpadRight:
currentSettings.customStreamingRadialMenuButtonPosition = @(ControllerElementPositionRight);
metRadialButtonWithUndefinedPosition = false;
break;
case ControllerElementDpadUp:
currentSettings.customStreamingRadialMenuButtonPosition = @(ControllerElementPositionMiddle);
metRadialButtonWithUndefinedPosition = false;
break;
default:
break;
}
if(!metRadialButtonWithUndefinedPosition){
if (@available(iOS 13.0, *)) [GamepadNavigationIllustrationHud clearHud];
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Move the stick you want to use for mouse control. The other stick will be used for vertical and horizontal scrolling"];
return;
}
}
}
}
if(!stickCaptured && streamingRadialMenuButtonCaptured && !metRadialButtonWithUndefinedPosition){
float leftStickOffset = hypotf(gamepad.leftThumbstick.xAxis.value, gamepad.leftThumbstick.yAxis.value); float leftStickOffset = hypotf(gamepad.leftThumbstick.xAxis.value, gamepad.leftThumbstick.yAxis.value);
float rightStickOffset = hypotf(gamepad.rightThumbstick.xAxis.value, gamepad.rightThumbstick.yAxis.value); float rightStickOffset = hypotf(gamepad.rightThumbstick.xAxis.value, gamepad.rightThumbstick.yAxis.value);
if(leftStickOffset>0.1||rightStickOffset>0.1){ if(leftStickOffset>0.1||rightStickOffset>0.1){
ControllerMouseStick stick = leftStickOffset>rightStickOffset ? LeftStickToMouse : RightStickToMouse; ControllerElement stick = leftStickOffset>rightStickOffset ? ControllerElementLeftStick : ControllerElementRightStick;
currentSettings.controllerMouseStick = @(stick); currentSettings.controllerMouseStick = @(stick);
stickCaptured = true; stickCaptured = true;
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Press the button for mouse left button"]; AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Press the button for mouse left button"];
return;
} }
return;
} }
if(!leftButtonCaptured && stickCaptured){ if(!leftButtonCaptured && stickCaptured){
for(NSNumber* buttonFlagId in buttonDict){ for(NSNumber* elementEnumInstance in elementDict){
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId]; GCControllerElement * element = (GCControllerButtonInput *)elementDict[elementEnumInstance];
if(button.isPressed && ![capturedButtons containsObject:buttonFlagId]){ if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
currentSettings.controllerMouseLeftButton = buttonFlagId; GCControllerButtonInput * button = (GCControllerButtonInput *)elementDict[elementEnumInstance];
[capturedButtons addObject:buttonFlagId]; if(button.isPressed && ![capturedButtons containsObject:elementEnumInstance]){
currentSettings.controllerMouseLeftButton = elementEnumInstance;
[capturedButtons addObject:elementEnumInstance];
leftButtonCaptured = true; leftButtonCaptured = true;
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Press the button for mouse right button"]; AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Press the button for mouse right button"];
return; return;
@@ -2905,17 +3083,42 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
if(!rightButtonCaptured && leftButtonCaptured){ if(!rightButtonCaptured && leftButtonCaptured){
for(NSNumber* buttonFlagId in buttonDict){ for(NSNumber* elementEnumInstance in elementDict){
GCControllerButtonInput * button = (GCControllerButtonInput *)buttonDict[buttonFlagId]; GCControllerElement * element = (GCControllerButtonInput *)elementDict[elementEnumInstance];
if(button.isPressed && ![capturedButtons containsObject:buttonFlagId]){ if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
currentSettings.controllerMouseRightButton = buttonFlagId; GCControllerButtonInput * button = (GCControllerButtonInput *)elementDict[elementEnumInstance];
[capturedButtons addObject:buttonFlagId]; if(button.isPressed && ![capturedButtons containsObject:elementEnumInstance]){
currentSettings.controllerMouseRightButton = elementEnumInstance;
[capturedButtons addObject:elementEnumInstance];
rightButtonCaptured = true; rightButtonCaptured = true;
if (@available(iOS 13.0, *)) {
NSLog(@"custom posistion local %d , stream: %d", currentSettings.customLocalRadialMenuButtonPosition.intValue, currentSettings.customStreamingRadialMenuButtonPosition.intValue);
ControllerNavigator.localRadialMenuButton = (ControllerElement)currentSettings.localRadialMenuButton.intValue;
ControllerNavigator.customPositionForLocalRadialMenuButton = (ControllerElementPosition)currentSettings.customLocalRadialMenuButtonPosition.intValue;
ControllerNavigator.streamingRadialMenuButton = (ControllerElement)currentSettings.streamingRadialMenuButton.intValue;
ControllerNavigator.customPositionForStreamingRadialMenuButton = (ControllerElementPosition)currentSettings.customStreamingRadialMenuButtonPosition.intValue;
ControllerNavigator.streamingRadialMenuDelay = (NSTimeInterval)currentSettings.streamingRadialMenuDelay.floatValue;
ControllerNavigator.enabled = true;
[ControllerNavigator setUINavigationDelegate:self];
// ControllerNavigator.controllerMouseStick = (ControllerElement)currentSettings.controllerMouseStick.intValue;
// ControllerNavigator.controllerMouseLeftButton = (ControllerElement)currentSettings.controllerMouseLeftButton.intValue;
// ControllerNavigator.controllerMouseRightButton = (ControllerElement)currentSettings.controllerMouseRightButton.intValue;
// ControllerNavigator.controllerMouseExpo = currentSettings.controllerMouseExpo.floatValue;
}
AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Finished"]; AlertControllerUtil.alertController.message = [LocalizationHelper localizedStringForKey:@"Finished"];
gamepad.valueChangedHandler = nil; gamepad.valueChangedHandler = nil;
// deprecated:
UIAlertAction* cancelAction = AlertControllerUtil.alertController.actions.firstObject; UIAlertAction* cancelAction = AlertControllerUtil.alertController.actions.firstObject;
if(cancelAction) [cancelAction setValue:[LocalizationHelper localizedStringForKey:@"OK"] forKey:@"title"]; if(cancelAction) [cancelAction setValue:[LocalizationHelper localizedStringForKey:@"OK"] forKey:@"title"];
//
[self->dataMan saveData]; [self->dataMan saveData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
if (@available(iOS 13.0, *)) [ControllerNavigator restartListening];
[AlertControllerUtil.alertController dismissViewControllerAnimated:NO completion:nil];
});
} }
} }
} }
@@ -2932,12 +3135,12 @@ BOOL isCustomResolution(int resolutionSelected) {
action:^{} action:^{}
completion:^{ completion:^{
if(AlertControllerUtil.actionCancelled){ if(AlertControllerUtil.actionCancelled){
[self.controllerToMouseSwitch setOn:NO]; [self.controllerNavigationSwitch setOn:NO];
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged]; [self.controllerNavigationSwitch sendActionsForControlEvents:UIControlEventValueChanged];
return; return;
} }
[AlertControllerUtil.alertController dismissViewControllerAnimated:NO completion:^{}]; [AlertControllerUtil.alertController dismissViewControllerAnimated:NO completion:^{}];
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged]; [self.controllerNavigationSwitch sendActionsForControlEvents:UIControlEventValueChanged];
}]; }];
}]; }];
} }
@@ -2945,8 +3148,8 @@ BOOL isCustomResolution(int resolutionSelected) {
completion:^{ completion:^{
if(AlertControllerUtil.actionCancelled){ if(AlertControllerUtil.actionCancelled){
if(!rightButtonCaptured){ if(!rightButtonCaptured){
[self.controllerToMouseSwitch setOn:rightButtonCaptured]; [self.controllerNavigationSwitch setOn:rightButtonCaptured];
[self.controllerToMouseSwitch sendActionsForControlEvents:UIControlEventValueChanged]; [self.controllerNavigationSwitch sendActionsForControlEvents:UIControlEventValueChanged];
} }
if(self->capturedController && self->capturedController.extendedGamepad) self->capturedController.extendedGamepad.valueChangedHandler = nil; if(self->capturedController && self->capturedController.extendedGamepad) self->capturedController.extendedGamepad.valueChangedHandler = nil;
} }
@@ -2972,25 +3175,25 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
- (void)rollSensitivitySliderMoved:(UISlider* )sender{ - (void)rollSensitivitySliderMoved:(UISlider* )sender{
[self findDynamicLabelFromStack:_rollSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (int16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (int16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]];
} }
- (void)gyroMinStickOffsetSliderMoved:(UISlider* )sender{ - (void)gyroMinStickOffsetSliderMoved:(UISlider* )sender{
[self findDynamicLabelFromStack:_gyroToStickMinOffsetStack].text = [NSString stringWithFormat:@" %d ", (int16_t)sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d ", (int16_t)sender.value];
if(settingsViewJustExpanded) return; if(settingsViewJustExpanded) return;
LiSendControllerEvent(0, 0, 0, _rollToLeftStickSwitch.isOn?sender.value:0, 0, _yawPitchToRightStickSwitch.isOn?sender.value:0, 0); LiSendControllerEvent(0, 0, 0, _rollToLeftStickSwitch.isOn?sender.value:0, 0, _yawPitchToRightStickSwitch.isOn?sender.value:0, 0);
OnScreenWidgetView.gamepadArrivalReported = true; OnScreenWidgetView.gamepadArrivalReported = true;
} }
- (void)leftStickMinOffsetSliderMoved:(UISlider* )sender{ - (void)leftStickMinOffsetSliderMoved:(UISlider* )sender{
[self findDynamicLabelFromStack:_leftStickMinOffsetStack].text = [NSString stringWithFormat:@" %d ", (int16_t)sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d ", (int16_t)sender.value];
if(settingsViewJustExpanded) return; if(settingsViewJustExpanded) return;
LiSendControllerEvent(0, 0, 0, sender.value, 0, 0, 0); LiSendControllerEvent(0, 0, 0, sender.value, 0, 0, 0);
OnScreenWidgetView.gamepadArrivalReported = true; OnScreenWidgetView.gamepadArrivalReported = true;
} }
- (void)rightStickMinOffsetSliderMoved:(UISlider* )sender{ - (void)rightStickMinOffsetSliderMoved:(UISlider* )sender{
[self findDynamicLabelFromStack:_rightStickMinOffsetStack].text = [NSString stringWithFormat:@" %d ", (int16_t)sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d ", (int16_t)sender.value];
if(settingsViewJustExpanded) return; if(settingsViewJustExpanded) return;
LiSendControllerEvent(0, 0, 0, 0, 0, sender.value, 0); LiSendControllerEvent(0, 0, 0, 0, 0, sender.value, 0);
OnScreenWidgetView.gamepadArrivalReported = true; OnScreenWidgetView.gamepadArrivalReported = true;
@@ -3056,24 +3259,24 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
- (void) pointerVelocityModeDividerSliderMoved:(UISlider* )sender { - (void) pointerVelocityModeDividerSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:self.pointerVelocityDividerStack].text = [NSString stringWithFormat:@" | %d%% | %d%% | ", (uint8_t)sender.value, 100-(uint8_t)sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" | %d%% | %d%% | ", (uint8_t)sender.value, 100-(uint8_t)sender.value];
} }
- (void) touchPointerVelocityFactorSliderMoved:(UISlider* )sender { - (void) touchPointerVelocityFactorSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:self.pointerVelocityFactorStack].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; // Update label display [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; // Update label display
} }
- (void) gyroSensitivitySliderMoved:(UISlider* )sender { - (void) gyroSensitivitySliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:self.gyroSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)sender.value]; // Update label display [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)sender.value]; // Update label display
} }
- (void) localVolumeSliderMoved:(UISlider* )sender { - (void) localVolumeSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:self.localVolumeStack].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)sender.value]; // Update label display [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)sender.value]; // Update label display
if(_mainFrameViewController.settingsExpandedInStreamView) [Connection setVolume:sender.value/100]; if(_mainFrameViewController.settingsExpandedInStreamView) [Connection setVolume:sender.value/100];
} }
- (void) micVolumeSliderMoved:(UISlider* )sender { - (void) micVolumeSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:self.micVolumeStack].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)sender.value]; // Update label display [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)sender.value]; // Update label display
if(_mainFrameViewController.settingsExpandedInStreamView) [MicHandler setVolume:sender.value/100]; if(_mainFrameViewController.settingsExpandedInStreamView) [MicHandler setVolume:sender.value/100];
} }
@@ -3082,7 +3285,7 @@ BOOL isCustomResolution(int resolutionSelected) {
labelString = [LocalizationHelper localizedStringForKey:@" keep %d min ", (uint16_t)sender.value]; labelString = [LocalizationHelper localizedStringForKey:@" keep %d min ", (uint16_t)sender.value];
if(sender.value == 0) labelString = [LocalizationHelper localizedStringForKey:@" disconnect "]; if(sender.value == 0) labelString = [LocalizationHelper localizedStringForKey:@" disconnect "];
if(sender.value == sender.maximumValue) labelString = [LocalizationHelper localizedStringForKey:@" keep alive "]; if(sender.value == sender.maximumValue) labelString = [LocalizationHelper localizedStringForKey:@" keep alive "];
[self findDynamicLabelFromStack:self.backgroundSessionTimerStack].text = labelString; // Update label display [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = labelString; // Update label display
} }
- (void)appThemeChanged:(UISegmentedControl* )sender{ - (void)appThemeChanged:(UISegmentedControl* )sender{
@@ -3110,31 +3313,35 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
- (void) mousePointerVelocityFactorSliderMoved:(UISlider* )sender { - (void) mousePointerVelocityFactorSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:_mousePointerVelocityStack].text = [NSString stringWithFormat:@" %d%% ",(uint16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ",(uint16_t)[self map_velocFactorDisplay_fromSliderValue:sender.value]];
} }
- (void) singleTapSensitivitySliderMoved:(UISlider* )sender { - (void) singleTapSensitivitySliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:_singleTapSensitivityStack].text = [NSString stringWithFormat:@" %.1f ",sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %.1f ",sender.value];
} }
- (void) scrollSensitivitySliderMoved:(UISlider* )sender { - (void) scrollSensitivitySliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:_scrollSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)(sender.value*100)]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)(sender.value*100)];
} }
- (void) pinchSensitivitySliderMoved:(UISlider* )sender { - (void) pinchSensitivitySliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:_pinchSensitivityStack].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)(sender.value*100)]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d%% ", (uint16_t)(sender.value*100)];
} }
- (void) relativeTouchSlideThresholdSliderMoved:(UISlider* )sender { - (void) relativeTouchSlideThresholdSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:_relativeTouchSlideThresholdStack].text = [NSString stringWithFormat:@" %.1f ",sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %.1f ",sender.value];
} }
- (void) controllerMouseVelocitySliderMoved:(UISlider* )sender { - (void) controllerMouseVelocitySliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:_controllerMouseVelocityStack].text = [NSString stringWithFormat:@" %.1f ", sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %.1f ", sender.value];
}
- (void) streamingRadialMenuDelaySliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %.1f s ", sender.value];
} }
- (void) controllerMouseExpoSliderMoved:(UISlider* )sender { - (void) controllerMouseExpoSliderMoved:(UISlider* )sender {
[self findDynamicLabelFromStack:_controllerMouseExpoStack].text = [NSString stringWithFormat:@" %.1f ", sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %.1f ", sender.value];
} }
- (uint32_t) getScreenEdgeFromSelector { - (uint32_t) getScreenEdgeFromSelector {
@@ -3580,27 +3787,27 @@ BOOL isCustomResolution(int resolutionSelected) {
NSInteger width = [self getChosenStreamWidth]; NSInteger width = [self getChosenStreamWidth];
NSInteger height = [self getChosenStreamHeight]; NSInteger height = [self getChosenStreamHeight];
[self findDynamicLabelFromStack:_resolutionStack].text = [NSString stringWithFormat:@"%ld × %ld", (long)width, (long)height]; [self findDynamicLabelFromStack:_resolutionSelectorStack].text = [NSString stringWithFormat:@"%ld × %ld", (long)width, (long)height];
} }
- (void) touchMoveEventIntervalSliderMoved:(UISlider* )sender{ - (void) touchMoveEventIntervalSliderMoved:(UISlider* )sender{
[self findDynamicLabelFromStack:self.touchMoveEventIntervalStack].text = sender.enabled ? [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = sender.enabled ?
[NSString stringWithFormat:@" %d μs ", (uint16_t)self.touchMoveEventIntervalSlider.value] : @""; [NSString stringWithFormat:@" %d μs ", (uint16_t)self.touchMoveEventIntervalSlider.value] : @"";
} }
- (void) leftClickDelaySliderMoved:(UISlider* )sender{ - (void) leftClickDelaySliderMoved:(UISlider* )sender{
[self findDynamicLabelFromStack:self.leftClickDelayStack].text = [NSString stringWithFormat:@" %d ms ", (uint16_t)sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d ms ", (uint16_t)sender.value];
} }
- (void) slideToMenuDistanceSliderMoved:(UISlider* )sender{ - (void) slideToMenuDistanceSliderMoved:(UISlider* )sender{
UILabel* displayLabel = [self findDynamicLabelFromStack:_slideToSettingsDistanceStack]; UILabel* displayLabel = [self findDynamicLabelFromStack:(UIStackView*)sender.superview];
// displayLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightMedium]; // displayLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightMedium];
NSString* labelText = [LocalizationHelper localizedStringForKey:@"%d%% screen width", (uint8_t)(sender.value * 100)]; NSString* labelText = [LocalizationHelper localizedStringForKey:@"%d%% screen width", (uint8_t)(sender.value * 100)];
displayLabel.text = [NSString stringWithFormat:@" %@ ", labelText]; displayLabel.text = [NSString stringWithFormat:@" %@ ", labelText];
} }
- (void) edgeSlidingSensitivitySliderMoved:(UISlider* )sender{ - (void) edgeSlidingSensitivitySliderMoved:(UISlider* )sender{
UILabel* displayLabel = [self findDynamicLabelFromStack:_edgeSlidingSensitivityStack]; UILabel* displayLabel = [self findDynamicLabelFromStack:(UIStackView*)sender.superview];
// displayLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightMedium]; // displayLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightMedium];
NSString* labelText = [LocalizationHelper localizedStringForKey:@" %d ", (uint8_t)sender.value]; NSString* labelText = [LocalizationHelper localizedStringForKey:@" %d ", (uint8_t)sender.value];
displayLabel.text = [NSString stringWithFormat:@" %@ ", labelText]; displayLabel.text = [NSString stringWithFormat:@" %@ ", labelText];
@@ -3782,7 +3989,7 @@ BOOL isCustomResolution(int resolutionSelected) {
- (void) frameQueueSizeSliderMoved:(UISlider* )sender { - (void) frameQueueSizeSliderMoved:(UISlider* )sender {
assert(self.frameQueueSizeSlider.value >= 0 && self.frameQueueSizeSlider.value <= 5); assert(self.frameQueueSizeSlider.value >= 0 && self.frameQueueSizeSlider.value <= 5);
int queueSize = self.frameQueueSizeSlider.value; int queueSize = self.frameQueueSizeSlider.value;
[self findDynamicLabelFromStack:_frameQueueSizeStack].text = queueSize==0 ? [LocalizationHelper localizedStringForKey:@"lowest latency"] : [NSString stringWithFormat: @" %d ", queueSize]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = queueSize==0 ? [LocalizationHelper localizedStringForKey:@"lowest latency"] : [NSString stringWithFormat: @" %d ", queueSize];
} }
- (void) enableGraphsChanged:(UISwitch* )sender { - (void) enableGraphsChanged:(UISwitch* )sender {
@@ -3811,6 +4018,8 @@ BOOL isCustomResolution(int resolutionSelected) {
if(![MicHandler permissionGranted]) [self.redirectMicSwitch setOn:false]; if(![MicHandler permissionGranted]) [self.redirectMicSwitch setOn:false];
[self saveGameProfileConfigs]; [self saveGameProfileConfigs];
if (@available(iOS 13.0, *)) if(!self.controllerNavigationSwitch.isOn) ControllerNavigator.enabled = false;
} }
- (void)pencilTickModeChanged:(UISegmentedControl* )sender{ - (void)pencilTickModeChanged:(UISegmentedControl* )sender{
@@ -3863,7 +4072,7 @@ BOOL isCustomResolution(int resolutionSelected) {
} }
- (void)pencilTickIntervalSliderMoved:(UISlider* )sender{ - (void)pencilTickIntervalSliderMoved:(UISlider* )sender{
[self findDynamicLabelFromStack:self.pencilTickIntervalStack].text = [NSString stringWithFormat:@" %d μs ", (uint16_t)sender.value]; [self findDynamicLabelFromStack:(UIStackView*)sender.superview].text = [NSString stringWithFormat:@" %d μs ", (uint16_t)sender.value];
} }
- (void)pressureCurveSwitchFlipped:(UISwitch* )sender{ - (void)pressureCurveSwitchFlipped:(UISwitch* )sender{
@@ -3964,7 +4173,7 @@ BOOL isCustomResolution(int resolutionSelected) {
self.pencilTickSelector.selectedSegmentIndex = tempSettings.pencilTickMode.intValue; self.pencilTickSelector.selectedSegmentIndex = tempSettings.pencilTickMode.intValue;
[self.pencilTickSelector addTarget:self action:@selector(pencilTickModeChanged:) forControlEvents:UIControlEventValueChanged]; [self.pencilTickSelector addTarget:self action:@selector(pencilTickModeChanged:) forControlEvents:UIControlEventValueChanged];
[self.pencilTickSelector sendActionsForControlEvents:UIControlEventValueChanged]; [self.pencilTickSelector sendActionsForControlEvents:UIControlEventValueChanged];
[self.pencilTickIntervalSlider setValue:tempSettings.pencilTickIntervalUs.floatValue]; [self.pencilTickIntervalSlider setValue:tempSettings.pencilTickIntervalUs.floatValue];
[self.pencilTickIntervalSlider addTarget:self action:@selector(pencilTickIntervalSliderMoved:) forControlEvents:UIControlEventValueChanged]; [self.pencilTickIntervalSlider addTarget:self action:@selector(pencilTickIntervalSliderMoved:) forControlEvents:UIControlEventValueChanged];
[self.pencilTickIntervalSlider sendActionsForControlEvents:UIControlEventValueChanged]; [self.pencilTickIntervalSlider sendActionsForControlEvents:UIControlEventValueChanged];
@@ -4079,7 +4288,7 @@ BOOL isCustomResolution(int resolutionSelected) {
BOOL ctrlDownForPinch = self.ctrlDownForPinchSwitch.isOn; BOOL ctrlDownForPinch = self.ctrlDownForPinchSwitch.isOn;
CGFloat leftClickDelayMs = self.leftClickDelaySlider.value; CGFloat leftClickDelayMs = self.leftClickDelaySlider.value;
BOOL passthroughGestures = self.passthroughGesturesSwitch.isOn; BOOL passthroughGestures = self.passthroughGesturesSwitch.isOn;
BOOL mapControllerToMouse = self.controllerToMouseSwitch.isOn; BOOL enableControllerNavigation = self.controllerNavigationSwitch.isOn;
CGFloat controllerMousePointerVelocity = self.controllerMouseVelocitySlider.value; CGFloat controllerMousePointerVelocity = self.controllerMouseVelocitySlider.value;
CGFloat controllerMouseExpo = self.controllerMouseExpoSlider.value; CGFloat controllerMouseExpo = self.controllerMouseExpoSlider.value;
NSInteger controllerGyroSwitchMode = self.controllerGyroSwitchButtonSetter.selectedSegmentIndex; NSInteger controllerGyroSwitchMode = self.controllerGyroSwitchButtonSetter.selectedSegmentIndex;
@@ -4087,6 +4296,7 @@ BOOL isCustomResolution(int resolutionSelected) {
BOOL asyncFrameDequeue = self.asyncFrameDequeueSwitch.isOn; BOOL asyncFrameDequeue = self.asyncFrameDequeueSwitch.isOn;
CGFloat softKeyboardHeight = self.softKeyboardHeightSwitch.isOn ? self->softKeyboardHeight : 0; CGFloat softKeyboardHeight = self.softKeyboardHeightSwitch.isOn ? self->softKeyboardHeight : 0;
BOOL globeAsEscape = self.globeAsEscapeSwitch.isOn; BOOL globeAsEscape = self.globeAsEscapeSwitch.isOn;
CGFloat streamingRadialMenuDelay = self.streamingRadialMenuDelaySlider.value;
NSInteger backgroundSessionTimer = self.backgroundSessionTimerSlider.value == self.backgroundSessionTimerSlider.maximumValue ? (uint32_t) INT16_MAX : (uint32_t)self.backgroundSessionTimerSlider.value; NSInteger backgroundSessionTimer = self.backgroundSessionTimerSlider.value == self.backgroundSessionTimerSlider.maximumValue ? (uint32_t) INT16_MAX : (uint32_t)self.backgroundSessionTimerSlider.value;
[dataMan saveSettings:currentSettings [dataMan saveSettings:currentSettings
@@ -4156,7 +4366,7 @@ BOOL isCustomResolution(int resolutionSelected) {
leftClickDelayMs:leftClickDelayMs leftClickDelayMs:leftClickDelayMs
settingsMenuOffset:settingsMenuOffset settingsMenuOffset:settingsMenuOffset
passthroughGestures:passthroughGestures passthroughGestures:passthroughGestures
mapControllerToMouse:mapControllerToMouse enableControllerNavigation:enableControllerNavigation
controllerMousePointerVelocity:controllerMousePointerVelocity controllerMousePointerVelocity:controllerMousePointerVelocity
controllerMouseExpo:controllerMouseExpo controllerMouseExpo:controllerMouseExpo
controllerGyroSwitchMode:controllerGyroSwitchMode controllerGyroSwitchMode:controllerGyroSwitchMode
@@ -4164,7 +4374,8 @@ BOOL isCustomResolution(int resolutionSelected) {
asyncFrameDequeue:asyncFrameDequeue asyncFrameDequeue:asyncFrameDequeue
sdrPerformanceWorkaround:sdrPerformanceWorkaround sdrPerformanceWorkaround:sdrPerformanceWorkaround
softKeyboardHeight:softKeyboardHeight softKeyboardHeight:softKeyboardHeight
globeAsEscape:globeAsEscape globeAsEscape:globeAsEscape
streamingRadialMenuDelay:streamingRadialMenuDelay
backgroundSessionTimer:backgroundSessionTimer]; backgroundSessionTimer:backgroundSessionTimer];
} }
@@ -28,7 +28,7 @@
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, AVPictureInPictureControllerDelegate> @interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, AVPictureInPictureControllerDelegate>
#else #else
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, ToolboxSpecialEntryDelegate, AVPictureInPictureControllerDelegate, OnScreenFunctionalWidgetDelegate, AbstractGamepadOverlayCloseButtonDelegate> @interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, AVPictureInPictureControllerDelegate>
#endif #endif
@property (nonatomic, strong) StreamManager* streamMan; @property (nonatomic, strong) StreamManager* streamMan;
@@ -48,14 +48,19 @@
@property (nonatomic, assign) CGFloat streamViewMagnifierZoomScale; @property (nonatomic, assign) CGFloat streamViewMagnifierZoomScale;
+ (StreamFrameViewController *)sharedInstance;
- (void)updatePreferredDisplayMode:(BOOL)streamActive; - (void)updatePreferredDisplayMode:(BOOL)streamActive;
- (void)setUserInteractionEnabledForStreamView:(bool)enabled; - (void)setUserInteractionEnabledForStreamView:(bool)enabled;
- (bool)shallDisableGyroHotSwitch; - (bool)shallDisableGyroHotSwitch;
- (void)loadGameProfileConfigs:(OSCProfile* )profile; - (void)loadGameProfileConfigs:(OSCProfile* )profile;
- (void)openWidgetProfileTableWithPickProfile:(BOOL)pickProfile;
- (void)toggleGamepadOverlayWithOverlayEnabled:(BOOL)overlayEnabled API_AVAILABLE(ios(13.0)); - (void)toggleGamepadOverlayWithOverlayEnabled:(BOOL)overlayEnabled API_AVAILABLE(ios(13.0));
- (void)loadAbstractGamepadOverlayIfNeeded API_AVAILABLE(ios(13.0)); - (void)loadAbstractGamepadOverlayIfNeeded API_AVAILABLE(ios(13.0));
- (void)restorePersistedStreamViewOffsetAndScaleWithProfile:(OSCProfile* )profile; - (void)restorePersistedStreamViewOffsetAndScaleWithProfile:(OSCProfile* )profile;
- (void)updateMagnifierViewportMetrics; - (void)updateMagnifierViewportMetrics;
- (void)setMagnifierViewportInteractionEnabled:(BOOL)enabled;
- (void)resetMagnifierStreamViewWithAnimated:(BOOL)animated;
- (void)returnToMainFrame;
@end @end
@@ -76,6 +76,11 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
} }
@interface StreamFrameViewController () <ToolboxSpecialEntryDelegate, OnScreenFunctionalWidgetDelegate, AbstractGamepadOverlayCloseButtonDelegate>
@end
static __weak StreamFrameViewController *VLSharedStreamFrameViewController = nil;
@implementation StreamFrameViewController { @implementation StreamFrameViewController {
ControllerSupport *_controllerSupport; ControllerSupport *_controllerSupport;
TemporarySettings *_settings; TemporarySettings *_settings;
@@ -130,6 +135,10 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
} }
+ (StreamFrameViewController *)sharedInstance {
return VLSharedStreamFrameViewController;
}
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
_streamView.hidden = YES; _streamView.hidden = YES;
if (self.imguiView) { if (self.imguiView) {
@@ -660,6 +669,12 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
[self setMagnifierViewportInteractionEnabled:_oscProfile.touchMode == AbsoluteTouch && !_settings.passthroughGestures]; [self setMagnifierViewportInteractionEnabled:_oscProfile.touchMode == AbsoluteTouch && !_settings.passthroughGestures];
GenericUtils.globeAsEscape = _settings.globeAsEscape; GenericUtils.globeAsEscape = _settings.globeAsEscape;
if (@available(iOS 13.0, *)) {
ControllerNavigator.localRadialMenuButton = (ControllerElement)_settings.localRadialMenuButton.intValue;
ControllerNavigator.streamingRadialMenuButton = (ControllerElement)_settings.streamingRadialMenuButton.intValue;
ControllerNavigator.streamingRadialMenuDelay = (NSTimeInterval)_settings.streamingRadialMenuDelay.floatValue;
}
NSLog(@"frameview gestures: %d", (uint32_t)[self.view.gestureRecognizers count]); NSLog(@"frameview gestures: %d", (uint32_t)[self.view.gestureRecognizers count]);
NSLog(@"streamview gestures: %d", (uint32_t)[_streamView.gestureRecognizers count]); NSLog(@"streamview gestures: %d", (uint32_t)[_streamView.gestureRecognizers count]);
@@ -828,6 +843,7 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
- (void)viewDidLoad - (void)viewDidLoad
{ {
VLSharedStreamFrameViewController = self;
_viewJustLoaded = true; _viewJustLoaded = true;
viewIsBeingResized = false; viewIsBeingResized = false;
_magnifierViewportInteractionActive = false; _magnifierViewportInteractionActive = false;
@@ -1005,8 +1021,6 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OnScreenWidgetView.enableFolderAnimation = true; OnScreenWidgetView.enableFolderAnimation = true;
}); });
[ControllerNavigator stop];
} }
- (void)keyboardWillShow:(NSNotification *)notification{ - (void)keyboardWillShow:(NSNotification *)notification{
@@ -1118,10 +1132,12 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
if(!profile){ if(!profile){
profile = [OSCProfilesManager sharedManager:CGRectZero].getSelectedProfile; profile = [OSCProfilesManager sharedManager:CGRectZero].getSelectedProfile;
} }
[self setMagnifierViewportInteractionEnabled:true]; dispatch_async(dispatch_get_main_queue(), ^{
CGPoint streamViewOffset = CGPointMake(profile.normalizedStreamViewOffset.x*self.view.bounds.size.width, profile.normalizedStreamViewOffset.y*self.view.bounds.size.height); [self setMagnifierViewportInteractionEnabled:true];
[self restoreMagnifierStreamViewWithOffset:streamViewOffset scale:profile.streamViewScale animated:YES]; CGPoint streamViewOffset = CGPointMake(profile.normalizedStreamViewOffset.x*self.view.bounds.size.width, profile.normalizedStreamViewOffset.y*self.view.bounds.size.height);
[self setMagnifierViewportInteractionEnabled:profile.touchMode == AbsoluteTouch && !_settings.passthroughGestures]; [self restoreMagnifierStreamViewWithOffset:streamViewOffset scale:profile.streamViewScale animated:YES];
[self setMagnifierViewportInteractionEnabled:profile.touchMode == AbsoluteTouch && !self->_settings.passthroughGestures];
});
} }
- (void)restoreMagnifierStreamViewWithOffset:(CGPoint)offset scale:(CGFloat)scale { - (void)restoreMagnifierStreamViewWithOffset:(CGPoint)offset scale:(CGFloat)scale {
@@ -1289,10 +1305,10 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
- (void)setupOverlayView{ - (void)setupOverlayView{
if (_overlayView == nil) { if (_overlayView == nil) {
_overlayView = [[PaddedLabel alloc] initWithFrame:CGRectZero]; _overlayView = [[PaddedLabel alloc] initWithFrame:CGRectZero];
[_overlayView setTextInsets:UIEdgeInsetsMake([_mainFrameViewcontroller isIPhone]?4:6, 12, [_mainFrameViewcontroller isIPhone]?4:6, 12)]; [_overlayView setTextInsets:UIEdgeInsetsMake(PublicUtils.isIPhone?4:6, 12, PublicUtils.isIPhone?4:6, 12)];
[_overlayView setUserInteractionEnabled:NO]; [_overlayView setUserInteractionEnabled:NO];
[_overlayView setNumberOfLines:100]; [_overlayView setNumberOfLines:100];
[_overlayView.layer setCornerRadius:[_mainFrameViewcontroller isIPhone]?7:10]; [_overlayView.layer setCornerRadius:PublicUtils.isIPhone?7:10];
[_overlayView.layer setMasksToBounds:YES]; [_overlayView.layer setMasksToBounds:YES];
// HACK: If not using stats overlay, center the text // HACK: If not using stats overlay, center the text
@@ -1305,13 +1321,13 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
#if TARGET_OS_TV #if TARGET_OS_TV
[_overlayView setFont:[UIFont systemFontOfSize:24 weight:UIFontWeightMedium]]; [_overlayView setFont:[UIFont systemFontOfSize:24 weight:UIFontWeightMedium]];
#else #else
[_overlayView setFont:[UIFont systemFontOfSize: [_mainFrameViewcontroller isIPhone]?10:12 weight:UIFontWeightMedium]]; [_overlayView setFont:[UIFont systemFontOfSize: PublicUtils.isIPhone?10:12 weight:UIFontWeightMedium]];
#endif #endif
[_overlayView setAlpha:(float)[_settings.graphOpacity intValue]/ 100.0]; [_overlayView setAlpha:(float)[_settings.graphOpacity intValue]/ 100.0];
[self.view addSubview:_overlayView]; [self.view addSubview:_overlayView];
} }
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {
if(overlayLevel == 1) _overlayView.font = [UIFont monospacedSystemFontOfSize:[_mainFrameViewcontroller isIPhone]?10:12 weight:UIFontWeightMedium]; if(overlayLevel == 1) _overlayView.font = [UIFont monospacedSystemFontOfSize:PublicUtils.isIPhone?10:12 weight:UIFontWeightMedium];
} }
[_overlayView setHidden:YES]; [_overlayView setHidden:YES];
@@ -1336,7 +1352,18 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
} }
} }
- (void) returnToMainFrame { - (void)returnToMainFrame {
if (@available(iOS 13.0, *)) {
// [ControllerNavigator setUINavigationDelegate:[_mainFrameViewcontroller isInAppView] ? _mainFrameViewcontroller : _mainFrameViewcontroller.hostCollectionVC];
[ControllerNavigator restorePreviousUINavigationDelegateWithIfCurrentDelegateIs:self];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if(ControllerNavigator.radialMenuView) [ControllerNavigator updateRadialMenu];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[ControllerNavigator restoreUINavigationHighlight];
});
}
[_streamView saveStreamingGameProfileChanges]; [_streamView saveStreamingGameProfileChanges];
[_streamView clearOnScreenWidgets]; [_streamView clearOnScreenWidgets];
if(micHandler) [micHandler clean]; if(micHandler) [micHandler clean];
@@ -1347,11 +1374,11 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
if (@available(iOS 13.0, *)) { if (@available(iOS 13.0, *)) {
[SceneDelegate clearExternalDisplayRenderView]; [SceneDelegate clearExternalDisplayRenderView];
} }
if (_settings.enablePIP) { if (_settings.enablePIP) {
[self cleanupPiPController]; [self cleanupPiPController];
} }
[_statsUpdateTimer invalidate]; [_statsUpdateTimer invalidate];
_statsUpdateTimer = nil; _statsUpdateTimer = nil;
@@ -1360,8 +1387,10 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
_extWindow = nil; _extWindow = nil;
if(_streamConfig.redirectMic) [micHandler stopTappingWithStopEngine:true]; if(_streamConfig.redirectMic) [micHandler stopTappingWithStopEngine:true];
self.mainFrameViewcontroller.settingsExpandedInStreamView = false; // reset this flag to false self.mainFrameViewcontroller.settingsExpandedInStreamView = false; // reset this flag to false
[ControllerUtil disableSysGestures: ControllerUtil.primaryGCController];
} }
// External Screen connected // External Screen connected
@@ -1569,7 +1598,6 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
} }
- (void)expandSettingsView{ - (void)expandSettingsView{
self.mainFrameViewcontroller.settingsExpandedInStreamView = true; //notify mainFrameViewContorller that this is a setting expansion in stream view, some settings shall be disabled.
[_streamView saveStreamingGameProfileChanges]; [_streamView saveStreamingGameProfileChanges];
[self.mainFrameViewcontroller expandSettingsView]; [self.mainFrameViewcontroller expandSettingsView];
} }
@@ -1593,7 +1621,7 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1.5); sleep(1.5);
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[self.mainFrameViewcontroller quitRunningApp]; [self.mainFrameViewcontroller quitLaunchedApp];
}); });
}); });
} }
@@ -2258,6 +2286,9 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
- (void)dealloc { - (void)dealloc {
if (VLSharedStreamFrameViewController == self) {
VLSharedStreamFrameViewController = nil;
}
NSLog(@"dealloc StreamFrameViewController %f", CACurrentMediaTime()); NSLog(@"dealloc StreamFrameViewController %f", CACurrentMediaTime());
} }
@@ -2271,4 +2302,5 @@ static NSString* VLTerminationHintForErrorCode(int errorCode) {
}]; }];
} }
@end @end
@@ -463,7 +463,7 @@ import UIKit
private func sendKeyboardCommand(_ cmd: RemoteCommand) { private func sendKeyboardCommand(_ cmd: RemoteCommand) {
print("Sending key-value") // print("Sending key-value")
let keyboardCmdStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: cmd.cmdString) let keyboardCmdStrings = CommandManager.shared.extractAutoReleaseButtonStrings(from: cmd.cmdString)
CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: keyboardCmdStrings!) CommandManager.shared.sendAutoReleaseComboCommand(cmdStrings: keyboardCmdStrings!)
} }
+5
View File
@@ -3,9 +3,14 @@
// //
#include <Limelight.h> #include <Limelight.h>
#import "SettingsViewController.h"
#import "MainFrameViewController.h"
#import "StreamFrameViewController.h"
#import "ControllerSupport.h"
#import "OnScreenControls.h" #import "OnScreenControls.h"
#import "OnScreenButtonState.h" #import "OnScreenButtonState.h"
#import "DataManager.h" #import "DataManager.h"
#import "AppAssetManager.h"
#import "LocalizationHelper.h" #import "LocalizationHelper.h"
#import "OSCProfile.h" #import "OSCProfile.h"
#import "LayoutOnScreenControls.h" #import "LayoutOnScreenControls.h"
+42 -12
View File
@@ -733,6 +733,36 @@
} }
} }
}, },
"9d1-RN-lqW.text" : {
"comment" : "Class = \"UILabel\"; text = \"Streaming Radial Menu Delay\"; ObjectID = \"9d1-RN-lqW\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Streaming Radial Menu Delay"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "串流手柄菜单延迟"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "串流手把選單延遲"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "串流手把選單延遲"
}
}
}
},
"9pX-93-bC2.text" : { "9pX-93-bC2.text" : {
"comment" : "Class = \"UILabel\"; text = \"Background Session\"; ObjectID = \"9pX-93-bC2\";", "comment" : "Class = \"UILabel\"; text = \"Background Session\"; ObjectID = \"9pX-93-bC2\";",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
@@ -5313,31 +5343,31 @@
} }
}, },
"P1g-rW-Mgp.text" : { "P1g-rW-Mgp.text" : {
"comment" : "Class = \"UILabel\"; text = \"Controller → Mouse\"; ObjectID = \"P1g-rW-Mgp\";", "comment" : "Class = \"UILabel\"; text = \"Controller Navigation\"; ObjectID = \"P1g-rW-Mgp\";",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Controller → Mouse" "value" : "Controller Navigation"
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "手柄映射为鼠标" "value" : "手柄导航"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "手把映射為滑鼠" "value" : "手把導覽"
} }
}, },
"zh-HK" : { "zh-HK" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "手把映射為滑鼠" "value" : "手把導覽"
} }
} }
} }
@@ -6959,31 +6989,31 @@
} }
}, },
"Xnd-C4-8N9.text" : { "Xnd-C4-8N9.text" : {
"comment" : "Class = \"UILabel\"; text = \"Pointer Velocity\"; ObjectID = \"Xnd-C4-8N9\";", "comment" : "Class = \"UILabel\"; text = \"Controller Mouse Velocity\"; ObjectID = \"Xnd-C4-8N9\";",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Pointer Velocity" "value" : "Controller Mouse Velocity"
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "指针速度" "value" : "手柄鼠标速度"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "指針速度" "value" : "手把滑鼠速度"
} }
}, },
"zh-HK" : { "zh-HK" : {
"stringUnit" : { "stringUnit" : {
"state" : "needs_review", "state" : "translated",
"value" : "指針速度" "value" : "手把滑鼠速度"
} }
} }
} }
+40 -10
View File
@@ -1621,6 +1621,36 @@
} }
} }
}, },
"bPX-Kf-kdP.text" : {
"comment" : "Class = \"UILabel\"; text = \"Streaming Radial Menu Delay\"; ObjectID = \"bPX-Kf-kdP\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Streaming Radial Menu Delay"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "串流手柄菜单延迟"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "串流手把選單延遲"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "串流手把選單延遲"
}
}
}
},
"BQ1-w6-Opg.text" : { "BQ1-w6-Opg.text" : {
"comment" : "Class = \"UILabel\"; text = \"Touch Point Tracking\"; ObjectID = \"BQ1-w6-Opg\";", "comment" : "Class = \"UILabel\"; text = \"Touch Point Tracking\"; ObjectID = \"BQ1-w6-Opg\";",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
@@ -2402,31 +2432,31 @@
} }
}, },
"F0O-Ht-fW5.text" : { "F0O-Ht-fW5.text" : {
"comment" : "Class = \"UILabel\"; text = \"Controller → Mouse\"; ObjectID = \"F0O-Ht-fW5\";", "comment" : "Class = \"UILabel\"; text = \"Controller Navigation\"; ObjectID = \"F0O-Ht-fW5\";",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Controller → Mouse" "value" : "Controller Navigation"
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "手柄映射为鼠标" "value" : "手柄导航"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "手把映射為滑鼠" "value" : "手把導覽"
} }
}, },
"zh-HK" : { "zh-HK" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "手把映射為滑鼠" "value" : "手把導覽"
} }
} }
} }
@@ -2738,31 +2768,31 @@
} }
}, },
"H3s-ud-yqh.text" : { "H3s-ud-yqh.text" : {
"comment" : "Class = \"UILabel\"; text = \"Pointer Velocity\"; ObjectID = \"H3s-ud-yqh\";", "comment" : "Class = \"UILabel\"; text = \"Controller Mouse Velocity\"; ObjectID = \"H3s-ud-yqh\";",
"extractionState" : "extracted_with_value", "extractionState" : "extracted_with_value",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "new", "state" : "new",
"value" : "Pointer Velocity" "value" : "Controller Mouse Velocity"
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "指针速度" "value" : "手柄鼠标速度"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "指針速度" "value" : "手把滑鼠速度"
} }
}, },
"zh-HK" : { "zh-HK" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "指針速度" "value" : "手把滑鼠速度"
} }
} }
} }
+3 -3
View File
@@ -518,7 +518,7 @@ struct AbstractGamepadView: View {
applyTriggerSelection(widget) applyTriggerSelection(widget)
applyDS4TouchSelection(widget) applyDS4TouchSelection(widget)
print("selectedCmd =", AbstractGamepadView.selectedCmd) // print("selectedCmd =", AbstractGamepadView.selectedCmd)
return true return true
} }
@@ -733,8 +733,8 @@ struct AbstractGamepadView: View {
if liveSnapshot.pressedButtons.contains(.b) { result.insert(.b) } if liveSnapshot.pressedButtons.contains(.b) { result.insert(.b) }
if liveSnapshot.pressedButtons.contains(.x) { result.insert(.x) } if liveSnapshot.pressedButtons.contains(.x) { result.insert(.x) }
if liveSnapshot.pressedButtons.contains(.y) { result.insert(.y) } if liveSnapshot.pressedButtons.contains(.y) { result.insert(.y) }
if liveSnapshot.pressedButtons.contains(.menu) { result.insert(.start) } if liveSnapshot.pressedButtons.contains(.start) { result.insert(.start) }
if liveSnapshot.pressedButtons.contains(.back) { result.insert(.select) } if liveSnapshot.pressedButtons.contains(.select) { result.insert(.select) }
if liveSnapshot.pressedButtons.contains(.special) { result.insert(.home) } if liveSnapshot.pressedButtons.contains(.special) { result.insert(.home) }
if liveSnapshot.pressedButtons.contains(.leftShoulder) { result.insert(.leftShoulder) } if liveSnapshot.pressedButtons.contains(.leftShoulder) { result.insert(.leftShoulder) }
if liveSnapshot.pressedButtons.contains(.rightShoulder) { result.insert(.rightShoulder) } if liveSnapshot.pressedButtons.contains(.rightShoulder) { result.insert(.rightShoulder) }
+235 -24
View File
@@ -10,25 +10,42 @@ import SwiftUI
import Combine import Combine
import UIKit import UIKit
public enum RadialMenuItem { @objc public enum RadialMenuItem:Int {
case settings case settings
case favoriteSettings
case allSettings
case addHost
case aboutView
case hostView case hostView
case gameProfiles case gameProfiles
case disconnect
case mouse
case quitApp
case theme
case more
case navigationSettings
case exit case exit
} }
@objc public enum RadialMenuState:Int {
case main
case mouseModeEnabled
case disconnectAndQuit
case moreOptions
}
@available(iOS 13.0, *) @available(iOS 13.0, *)
struct RadialMenuSector: Identifiable, Equatable { struct RadialMenuSector: Identifiable, Equatable {
let id = UUID() let id = UUID()
let title: String let title: String
let subtitle: String let subtitle: String
let systemImageName: String let symbolName: String
let item: RadialMenuItem let item: RadialMenuItem
init(title: String, subtitle: String, systemImageName: String, item: RadialMenuItem) { init(title: String, subtitle: String, symbol: String, item: RadialMenuItem) {
self.title = title self.title = title
self.subtitle = subtitle self.subtitle = subtitle
self.systemImageName = systemImageName self.symbolName = symbol
self.item = item self.item = item
} }
} }
@@ -38,7 +55,7 @@ struct RadialMenuStyle: Equatable {
var ringColor = Color(white: 0.86) var ringColor = Color(white: 0.86)
var selectedRingColor = Color(red: 0.19, green: 0.72, blue: 0.96) var selectedRingColor = Color(red: 0.19, green: 0.72, blue: 0.96)
var ringStrokeColor = Color.white.opacity(0.72) var ringStrokeColor = Color.white.opacity(0.72)
var centerFillColor = Color.white var centerFillColor = Color(ThemeManager.hostViewBackgroundColor.withAlphaComponent(0.5))
var iconColor = Color(white: 0.23) var iconColor = Color(white: 0.23)
var selectedIconColor = Color(white: 0.16) var selectedIconColor = Color(white: 0.16)
var titleColor = Color(white: 0.24) var titleColor = Color(white: 0.24)
@@ -46,35 +63,97 @@ struct RadialMenuStyle: Equatable {
var shadowColor = Color.black.opacity(0.18) var shadowColor = Color.black.opacity(0.18)
var ringWidthRatio: CGFloat = 0.41 var ringWidthRatio: CGFloat = 0.41
var segmentGapWidth: CGFloat = 1 var segmentGapWidth: CGFloat = 1
var centerIconScale: CGFloat = 0.16 var centerIconScale: CGFloat = 0.13
var segmentIconScale: CGFloat = 0.085 var segmentIconScale: CGFloat = 0.085
static func themed(for userInterfaceStyle: UIUserInterfaceStyle, accentColor: UIColor) -> RadialMenuStyle {
switch userInterfaceStyle {
case .dark:
return RadialMenuStyle(
ringColor: Color(UIColor(red: 44.0 / 255.0, green: 44.0 / 255.0, blue: 46.0 / 255.0, alpha: 0.88)),
selectedRingColor: Color(accentColor.withAlphaComponent(0.52)),
ringStrokeColor: Color.white.opacity(0.26),
// centerFillColor: Color(UIColor(red: 28.0 / 255.0, green: 28.0 / 255.0, blue: 30.0 / 255.0, alpha: 0.96)),
centerFillColor: Color(ThemeManager.hostViewBackgroundColor.withAlphaComponent(0.5)),
iconColor: Color(UIColor(white: 0.86, alpha: 1)),
selectedIconColor: Color.white,
titleColor: Color(UIColor(white: 0.92, alpha: 1)),
subtitleColor: Color(UIColor(white: 0.70, alpha: 1)),
shadowColor: Color(accentColor.withAlphaComponent(0)),
ringWidthRatio: 0.41,
segmentGapWidth: 1,
centerIconScale: 0.16,
segmentIconScale: 0.085
)
default:
return RadialMenuStyle(
selectedRingColor: Color(accentColor.withAlphaComponent(0.86))
)
}
}
}
@available(iOS 13.0, *)
enum RadialMenuSelectionChangeReason {
case began
case moved
case released
case unchanged
} }
@available(iOS 13.0, *) @available(iOS 13.0, *)
final class RadialMenuSelectionState: ObservableObject { final class RadialMenuSelectionState: ObservableObject {
@Published private(set) var selectedIndex: Int? @Published private(set) var selectedIndex: Int?
@Published private(set) var hasReceivedJoystickInput = false @Published private(set) var hasReceivedJoystickInput = false
@Published private(set) var lastChangeReason: RadialMenuSelectionChangeReason = .unchanged
private var itemCount = 0 private var itemCount = 0
func updateSelection(xOffset: Float, yOffset: Float) { func updateSelection(xOffset: CGFloat, yOffset: CGFloat) {
updateSelection(xOffset: xOffset, yOffset: yOffset, deadZone: 0.1) updateSelection(xOffset: xOffset, yOffset: yOffset, deadZone: 0.1)
} }
func updateSelection(xOffset: Float, yOffset: Float, deadZone: Float) { func updateSelection(xOffset: CGFloat, yOffset: CGFloat, deadZone: CGFloat) {
let x = max(min(xOffset, 1), -1)
let y = max(min(yOffset, 1), -1)
let distance = hypot(x, y)
if distance < deadZone {
guard !hasReceivedJoystickInput || selectedIndex != nil else {
lastChangeReason = .unchanged
return
}
hasReceivedJoystickInput = true
lastChangeReason = selectedIndex == nil ? .unchanged : .released
selectedIndex = nil
return
}
// Near center, tiny return-to-center offsets have unstable angles. Keep the
// last high-confidence sector until the stick fully enters the dead zone.
let selectionUpdateDeadZone = max(deadZone, 0.35)
guard distance >= selectionUpdateDeadZone else {
hasReceivedJoystickInput = true
lastChangeReason = .unchanged
return
}
let nextSelectedIndex = Self.selectedIndex( let nextSelectedIndex = Self.selectedIndex(
xOffset: xOffset, xOffset: x,
yOffset: yOffset, yOffset: y,
itemCount: itemCount, itemCount: itemCount,
deadZone: deadZone deadZone: deadZone
) )
guard !hasReceivedJoystickInput || nextSelectedIndex != selectedIndex else { guard !hasReceivedJoystickInput || nextSelectedIndex != selectedIndex else {
lastChangeReason = .unchanged
return return
} }
let previousSelectedIndex = selectedIndex
hasReceivedJoystickInput = true hasReceivedJoystickInput = true
selectedIndex = nextSelectedIndex selectedIndex = nextSelectedIndex
lastChangeReason = Self.changeReason(from: previousSelectedIndex, to: nextSelectedIndex)
} }
fileprivate func configureItemCount(_ itemCount: Int) { fileprivate func configureItemCount(_ itemCount: Int) {
@@ -84,7 +163,7 @@ final class RadialMenuSelectionState: ObservableObject {
} }
} }
static func selectedIndex(xOffset: Float, yOffset: Float, itemCount: Int, deadZone: Float = 0.1) -> Int? { static func selectedIndex(xOffset: CGFloat, yOffset: CGFloat, itemCount: Int, deadZone: CGFloat = 0.1) -> Int? {
guard itemCount > 0 else { return nil } guard itemCount > 0 else { return nil }
let x = max(min(xOffset, 1), -1) let x = max(min(xOffset, 1), -1)
@@ -104,6 +183,19 @@ final class RadialMenuSelectionState: ObservableObject {
let remainder = value.truncatingRemainder(dividingBy: divisor) let remainder = value.truncatingRemainder(dividingBy: divisor)
return remainder >= 0 ? remainder : remainder + divisor return remainder >= 0 ? remainder : remainder + divisor
} }
private static func changeReason(from previousIndex: Int?, to nextIndex: Int?) -> RadialMenuSelectionChangeReason {
switch (previousIndex, nextIndex) {
case (nil, .some):
return .began
case (.some(let previous), .some(let next)) where previous != next:
return .moved
case (.some, nil):
return .released
default:
return .unchanged
}
}
} }
@available(iOS 13.0, *) @available(iOS 13.0, *)
@@ -151,7 +243,7 @@ struct RadialMenuView: View {
self.joystickSelectionState.configureItemCount(sectors.count) self.joystickSelectionState.configureItemCount(sectors.count)
} }
func updateSelection(xOffset: Float, yOffset: Float) { func updateSelection(xOffset: CGFloat, yOffset: CGFloat) {
joystickSelectionState.configureItemCount(sectors.count) joystickSelectionState.configureItemCount(sectors.count)
joystickSelectionState.updateSelection(xOffset: xOffset, yOffset: yOffset) joystickSelectionState.updateSelection(xOffset: xOffset, yOffset: yOffset)
} }
@@ -192,9 +284,13 @@ struct RadialMenuView: View {
ForEach(sectors.indices, id: \.self) { index in ForEach(sectors.indices, id: \.self) { index in
let item = sectors[index] let item = sectors[index]
Image(systemName: item.systemImageName) RadialMenuIconView(
.font(.system(size: max(diameter * style.segmentIconScale, 18), weight: .medium)) symbolName: item.symbolName,
.foregroundColor(index == effectiveSelectedIndex ? style.selectedIconColor : style.iconColor) fallbackSystemName: "circle",
size: max(diameter * style.segmentIconScale, 18),
weight: .medium,
color: index == effectiveSelectedIndex ? style.selectedIconColor : style.iconColor
)
.scaleEffect(index == effectiveSelectedIndex ? 1.18 : 1) .scaleEffect(index == effectiveSelectedIndex ? 1.18 : 1)
.position(iconPosition(index: index, count: sectors.count, center: center, radius: (outerRadius + innerRadius) / 2)) .position(iconPosition(index: index, count: sectors.count, center: center, radius: (outerRadius + innerRadius) / 2))
.animation(.spring(response: 0.24, dampingFraction: 0.72), value: effectiveSelectedIndex) .animation(.spring(response: 0.24, dampingFraction: 0.72), value: effectiveSelectedIndex)
@@ -203,7 +299,7 @@ struct RadialMenuView: View {
RadialMenuCenterView(item: selectedItem, style: style, diameter: diameter) RadialMenuCenterView(item: selectedItem, style: style, diameter: diameter)
.frame(width: innerRadius * 1.72, height: innerRadius * 1.72) .frame(width: innerRadius * 1.72, height: innerRadius * 1.72)
.position(center) .position(center)
.animation(.easeInOut(duration: 0.16), value: effectiveSelectedIndex) .animation(.easeInOut(duration: selectedItem?.item == .gameProfiles ? 0 : 0.16), value: effectiveSelectedIndex)
} }
.frame(width: proxy.size.width, height: proxy.size.height) .frame(width: proxy.size.width, height: proxy.size.height)
.contentShape(Circle()) .contentShape(Circle())
@@ -266,18 +362,25 @@ private struct RadialMenuCenterView: View {
let diameter: CGFloat let diameter: CGFloat
var body: some View { var body: some View {
VStack(spacing: max(diameter * 0.018, 4)) { VStack(spacing: 0) {
Image(systemName: item?.systemImageName ?? "circle.grid.cross") RadialMenuIconView(
.font(.system(size: max(diameter * style.centerIconScale, 28), weight: .medium)) symbolName: item?.symbolName ?? "circle.grid.cross",
.foregroundColor(style.iconColor) fallbackSystemName: "circle.grid.cross",
size: centerIconSize,
weight: .medium,
color: style.iconColor
)
.id(item?.id) .id(item?.id)
.transition(.opacity) .transition(.opacity)
.frame(width: centerIconSize * 1.35, height: centerIconSize * 1.2)
.padding(.bottom, centerIconTitleSpacing)
Text(item?.title ?? "") Text(item?.title ?? "")
.font(.system(size: max(diameter * 0.047, 13), weight: .medium)) .font(.system(size: max(diameter * 0.047, 13), weight: .medium))
.foregroundColor(style.titleColor) .foregroundColor(style.titleColor)
.lineLimit(1) .lineLimit(1)
.minimumScaleFactor(0.62) .minimumScaleFactor(0.62)
.padding(.bottom, item?.subtitle.isEmpty == false ? centerTextSpacing : 0)
if item?.subtitle.isEmpty == false { if item?.subtitle.isEmpty == false {
Text(item?.subtitle ?? "") Text(item?.subtitle ?? "")
@@ -290,6 +393,114 @@ private struct RadialMenuCenterView: View {
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, diameter * 0.035) .padding(.horizontal, diameter * 0.035)
} }
private var centerIconSize: CGFloat {
max(diameter * style.centerIconScale, 28)
}
private var centerTextSpacing: CGFloat {
max(diameter * 0.018, 4)
}
private var centerIconTitleSpacing: CGFloat {
if #available(iOS 14.0, *) {
return centerTextSpacing
}
return max(diameter * 0.034, 8)
}
}
@available(iOS 13.0, *)
private struct RadialMenuIconView: View {
let symbolName: String
let fallbackSystemName: String
let size: CGFloat
let weight: Font.Weight
let color: Color
var body: some View {
Group {
if UIImage(systemName: symbolName) != nil {
Image(systemName: symbolName)
.font(.system(size: size, weight: weight))
} else if let assetImage = RadialMenuIconImageRenderer.templateImage(named: symbolName, pointSize: size) {
Image(uiImage: assetImage)
.renderingMode(.template)
} else {
Image(systemName: fallbackSystemName)
.font(.system(size: size, weight: weight))
}
}
.foregroundColor(color)
}
}
@available(iOS 13.0, *)
private enum RadialMenuIconImageRenderer {
private static let cache = NSCache<NSString, UIImage>()
private static let assetCanvasScale: CGFloat = 1.05
private static let assetVisualScale: CGFloat = 1
static func templateImage(named name: String, pointSize: CGFloat) -> UIImage? {
let scale = UIScreen.main.scale
let normalizedPointSize = max(ceil(pointSize * 2) / 2, 1)
let cacheKey = "\(name)-\(normalizedPointSize)-\(scale)-\(assetCanvasScale)-\(assetVisualScale)" as NSString
if let cachedImage = cache.object(forKey: cacheKey) {
return cachedImage
}
guard let sourceImage = UIImage(named: name) else {
return nil
}
let canvasSize = CGSize(
width: normalizedPointSize * assetCanvasScale,
height: normalizedPointSize * assetCanvasScale
)
let format = UIGraphicsImageRendererFormat()
format.scale = scale
format.opaque = false
let renderedImage = UIGraphicsImageRenderer(size: canvasSize, format: format).image { _ in
let drawBounds = scaledRect(
CGRect(origin: .zero, size: canvasSize),
by: assetVisualScale
)
sourceImage.draw(in: aspectFitRect(for: sourceImage.size, in: drawBounds))
}.withRenderingMode(.alwaysTemplate)
cache.setObject(renderedImage, forKey: cacheKey)
return renderedImage
}
private static func scaledRect(_ rect: CGRect, by scale: CGFloat) -> CGRect {
let width = rect.width * scale
let height = rect.height * scale
return CGRect(
x: rect.midX - width / 2,
y: rect.midY - height / 2,
width: width,
height: height
)
}
private static func aspectFitRect(for sourceSize: CGSize, in bounds: CGRect) -> CGRect {
guard sourceSize.width > 0, sourceSize.height > 0 else {
return bounds
}
let scale = min(bounds.width / sourceSize.width, bounds.height / sourceSize.height)
let width = sourceSize.width * scale
let height = sourceSize.height * scale
return CGRect(
x: bounds.midX - width / 2,
y: bounds.midY - height / 2,
width: width,
height: height
)
}
} }
@available(iOS 13.0, *) @available(iOS 13.0, *)
@@ -387,10 +598,10 @@ struct RadialMenuDemoView: View {
@SwiftUI.State private var isTouchSelectionEnabled = true @SwiftUI.State private var isTouchSelectionEnabled = true
static let menuItems = [ static let menuItems = [
RadialMenuSector(title: "Settings Menu".localized, subtitle: "", systemImageName: "sidebar.left", item: .settings), RadialMenuSector(title: "Settings Menu".localized, subtitle: "", symbol: "sidebar.left", item: .settings),
RadialMenuSector(title: "Host View".localized, subtitle: "", systemImageName: PublicUtils.liquidGlassEnabled ? "macwindow.on.rectangle" : "tv", item: .hostView), RadialMenuSector(title: "Host View".localized, subtitle: "", symbol: PublicUtils.liquidGlassEnabled ? "macwindow.on.rectangle" : "tv", item: .hostView),
RadialMenuSector(title: "Game Profiles".localized, subtitle: "", systemImageName: "gamecontroller.circle", item: .gameProfiles), RadialMenuSector(title: "Game Profiles".localized, subtitle: "", symbol: "gamecontroller.circle", item: .gameProfiles),
RadialMenuSector(title: "Exit/Disconnect".localized, subtitle: "", systemImageName: "personalhotspot.slash", item: .gameProfiles), RadialMenuSector(title: "Exit/Disconnect".localized, subtitle: "", symbol: "personalhotspot.slash", item: .gameProfiles),
] ]
private var visibleItems: [RadialMenuSector] { private var visibleItems: [RadialMenuSector] {
+1 -1
View File
@@ -1568,7 +1568,7 @@ struct KeyView: View {
.onEnded { _ in .onEnded { _ in
isPressed = false isPressed = false
action?() action?()
print("cmdString \(String(describing: cmdString)) code \(String(describing: keyCode))") // print("cmdString \(String(describing: cmdString)) code \(String(describing: keyCode))")
} }
) )
} }
+1 -1
View File
@@ -777,7 +777,7 @@ struct WidgetPickerView: View {
FunctionalButtonOption( FunctionalButtonOption(
localizationKey: "=disableTilt", localizationKey: "=disableTilt",
cmd: "DISABLETILT", cmd: "DISABLETILT",
tip: LocalizationHelper.localizedString(forKey: "Disable tilt for apple pencil"), tip: "Disable tilt for apple pencil".localized,
allowsKeyboardCombination: false, allowsKeyboardCombination: false,
allowsGamepadCombination: false, allowsGamepadCombination: false,
allowsSkillCombo: false, allowsSkillCombo: false,