gamepad live feedback overlay

This commit is contained in:
True Zhuanjia
2026-04-06 01:19:44 +08:00
parent 7f4108c4e3
commit 5af39e8fb6
19 changed files with 1583 additions and 382 deletions
+14 -9
View File
@@ -34,6 +34,7 @@
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D7E2F050240008C326F /* PressureCurve.swift */; };
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; };
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */; };
2A4B8D922F8149E200497966 /* CommandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDEB2C4F67D300547904 /* CommandManager.swift */; };
2A7113FD2F5A6C65001D852D /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7113FC2F5A6C65001D852D /* ThemeManager.swift */; };
2AAA4E822F7666C5001E20DC /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
2AAA4E842F7666EE001E20DC /* SwiftLocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1CDF92C4FE83400547904 /* SwiftLocalizationHelper.swift */; };
@@ -98,6 +99,7 @@
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AB2E841CAD46840089BB98 /* libmoonlight-common.a */; };
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; };
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */; };
A1B2C3D42F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */; };
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; };
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9B2C1A705600224667 /* OpenSSL */; };
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
@@ -200,7 +202,7 @@
18D43D7E2F050240008C326F /* PressureCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureCurve.swift; sourceTree = "<group>"; };
18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = "<group>"; };
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = "<group>"; };
2A039E562F72892B00546768 /* VoidLinkSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoidLinkSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; };
2A039E562F72892B00546768 /* VoidLinkPreview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoidLinkPreview.app; sourceTree = BUILT_PRODUCTS_DIR; };
2A7113FC2F5A6C65001D852D /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
2AC1E3822F507D80004F76C0 /* GenericUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericUtils.swift; sourceTree = "<group>"; };
3A4A3A102F0C000100000001 /* OSCProfilesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCProfilesTableViewController.swift; sourceTree = "<group>"; };
@@ -314,6 +316,7 @@
98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryHost.m; path = Database/TemporaryHost.m; sourceTree = "<group>"; };
98D5856E1C0ED0E800F6CC00 /* TemporarySettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporarySettings.h; path = Database/TemporarySettings.h; sourceTree = "<group>"; };
98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporarySettings.m; path = Database/TemporarySettings.m; sourceTree = "<group>"; };
A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractGamepadOverlayWidgetView.swift; sourceTree = "<group>"; };
DC1F5A05206436B10037755F /* ConnectionHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConnectionHelper.h; sourceTree = "<group>"; };
DC1F5A06206436B20037755F /* ConnectionHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionHelper.m; sourceTree = "<group>"; };
FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = "<group>"; };
@@ -418,7 +421,7 @@
membershipExceptions = (
WidgetPickerViewController.swift,
);
target = 2A039E552F72892B00546768 /* VoidLinkSwiftUI */;
target = 2A039E552F72892B00546768 /* VoidLinkPreview */;
};
2AAA4E862F76BDC6001E20DC /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
@@ -622,7 +625,7 @@
isa = PBXGroup;
children = (
FB290CEE19B2C406004C83CF /* VoidLink.app */,
2A039E562F72892B00546768 /* VoidLinkSwiftUI.app */,
2A039E562F72892B00546768 /* VoidLinkPreview.app */,
);
name = Products;
sourceTree = "<group>";
@@ -770,6 +773,7 @@
18E4BCE92E891895000A8657 /* MotionHandler.swift */,
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
A1B2C3D32F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift */,
1817B61D2E6FD04D000D938D /* SafeTimer.swift */,
);
path = Input;
@@ -962,9 +966,9 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
2A039E552F72892B00546768 /* VoidLinkSwiftUI */ = {
2A039E552F72892B00546768 /* VoidLinkPreview */ = {
isa = PBXNativeTarget;
buildConfigurationList = 2A039E5E2F72892C00546768 /* Build configuration list for PBXNativeTarget "VoidLinkSwiftUI" */;
buildConfigurationList = 2A039E5E2F72892C00546768 /* Build configuration list for PBXNativeTarget "VoidLinkPreview" */;
buildPhases = (
2A039E522F72892B00546768 /* Sources */,
2A039E532F72892B00546768 /* Frameworks */,
@@ -977,11 +981,11 @@
fileSystemSynchronizedGroups = (
2A039E572F72892B00546768 /* VoidLinkSwiftUI */,
);
name = VoidLinkSwiftUI;
name = VoidLinkPreview;
packageProductDependencies = (
);
productName = VoidLinkSwiftUI;
productReference = 2A039E562F72892B00546768 /* VoidLinkSwiftUI.app */;
productReference = 2A039E562F72892B00546768 /* VoidLinkPreview.app */;
productType = "com.apple.product-type.application";
};
FB290CED19B2C406004C83CF /* VoidLink */ = {
@@ -1051,7 +1055,7 @@
projectRoot = "";
targets = (
FB290CED19B2C406004C83CF /* VoidLink */,
2A039E552F72892B00546768 /* VoidLinkSwiftUI */,
2A039E552F72892B00546768 /* VoidLinkPreview */,
);
};
/* End PBXProject section */
@@ -1155,6 +1159,7 @@
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */,
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
A1B2C3D42F90000100ABCDEF /* AbstractGamepadOverlayWidgetView.swift in Sources */,
2AC1E3832F507D80004F76C0 /* GenericUtils.swift in Sources */,
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
9777AAF32E2CF7A400D7FBAF /* Shaders-Linear.metal in Sources */,
@@ -1612,7 +1617,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
2A039E5E2F72892C00546768 /* Build configuration list for PBXNativeTarget "VoidLinkSwiftUI" */ = {
2A039E5E2F72892C00546768 /* Build configuration list for PBXNativeTarget "VoidLinkPreview" */ = {
isa = XCConfigurationList;
buildConfigurations = (
2A039E5F2F72892C00546768 /* Debug */,
@@ -0,0 +1,402 @@
import UIKit
import SwiftUI
private enum AbstractGamepadOverlayPersistence {
private static let defaults = UserDefaults.standard
private enum Key {
static let overlayCenterXRatio = "AbstractGamepadOverlay.overlay.centerXRatio"
static let overlayCenterYRatio = "AbstractGamepadOverlay.overlay.centerYRatio"
static let overlayWidth = "AbstractGamepadOverlay.overlay.width"
}
struct PersistedState {
let centerRatio: CGPoint
let size: CGSize
}
static func loadOverlayState() -> PersistedState? {
loadState(
centerXKey: Key.overlayCenterXRatio,
centerYKey: Key.overlayCenterYRatio,
widthKey: Key.overlayWidth
)
}
static func saveOverlayState(centerRatio: CGPoint, size: CGSize) {
saveState(
centerRatio: centerRatio,
size: size,
centerXKey: Key.overlayCenterXRatio,
centerYKey: Key.overlayCenterYRatio,
widthKey: Key.overlayWidth
)
}
static func centerRatio(for view: UIView, in superview: UIView) -> CGPoint {
let safeWidth = max(superview.bounds.width, 1)
let safeHeight = max(superview.bounds.height, 1)
return CGPoint(x: view.center.x / safeWidth, y: view.center.y / safeHeight)
}
static func center(in superview: UIView, from ratio: CGPoint) -> CGPoint {
CGPoint(
x: min(max(ratio.x, 0), 1) * superview.bounds.width,
y: min(max(ratio.y, 0), 1) * superview.bounds.height
)
}
private static func loadState(
centerXKey: String,
centerYKey: String,
widthKey: String
) -> PersistedState? {
guard defaults.object(forKey: centerXKey) != nil,
defaults.object(forKey: centerYKey) != nil,
defaults.object(forKey: widthKey) != nil else {
return nil
}
let width = defaults.double(forKey: widthKey)
guard width > 0 else { return nil }
return PersistedState(
centerRatio: CGPoint(
x: defaults.double(forKey: centerXKey),
y: defaults.double(forKey: centerYKey)
),
size: CGSize(width: width, height: 0)
)
}
private static func saveState(
centerRatio: CGPoint,
size: CGSize,
centerXKey: String,
centerYKey: String,
widthKey: String
) {
defaults.set(centerRatio.x, forKey: centerXKey)
defaults.set(centerRatio.y, forKey: centerYKey)
defaults.set(size.width, forKey: widthKey)
}
}
@available(iOS 13.0, *)
private struct AbstractGamepadOverlayBridgeView: View {
let usesPlayStationFaceButtons: Bool
private static let overlayClusterGapAdjustmentRatio: CGFloat = 0.13
private static let overlayUpperPrimaryClusterVerticalAdjustmentRatio: CGFloat = -0.05
private static let overlayCenterCompressionRatio: CGFloat = 0
private static let overlayPanelHorizontalInsetRatio: CGFloat = 0.083
private static let overlayTriggerStubSpacingAdjustmentRatio: CGFloat = 0.17
private static let overlayRightShoulderHorizontalOffsetRatio: CGFloat = 0.006
private static let overlayThumbPurpleStrength: CGFloat = 1.15
private static let overlayTriggerPurpleStrength: CGFloat = 2.0
@ObservedObject var stateCenter = GamepadOverlayStateCenter.shared
var body: some View {
AbstractGamepadView(
gamepadType: usesPlayStationFaceButtons ? GamepadType.ps : GamepadType.xbox,
metricsProfile: GamepadMetricsProfile.overlay,
clusterGapAdjustmentRatio: Self.overlayClusterGapAdjustmentRatio,
upperPrimaryClusterVerticalAdjustmentRatio: Self.overlayUpperPrimaryClusterVerticalAdjustmentRatio,
centerCompressionRatio: Self.overlayCenterCompressionRatio,
panelHorizontalInsetRatio: Self.overlayPanelHorizontalInsetRatio,
triggerStubSpacingAdjustmentRatio: Self.overlayTriggerStubSpacingAdjustmentRatio,
rightShoulderHorizontalOffsetRatio: Self.overlayRightShoulderHorizontalOffsetRatio,
thumbPurpleStrength: Self.overlayThumbPurpleStrength,
triggerPurpleStrength: Self.overlayTriggerPurpleStrength,
liveSnapshot: stateCenter.snapshot
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.allowsHitTesting(false)
.background(Color.clear)
}
}
@available(iOS 13.0, *)
@objc
final class AbstractGamepadOverlayView: UIView {
private enum OverlayMaskMetrics {
static let widthRatio: CGFloat = 0.85
static let heightRatio: CGFloat = 1.03
static let cornerRadiusRatio: CGFloat = 0.326
static let verticalOffsetRatio: CGFloat = -0.006
static let topRectWidthRatio: CGFloat = 0.6
static let topRectHeightRatio: CGFloat = 0.15
static let topRectCornerRadiusRatio: CGFloat = 0.22
static let topRectVerticalOffsetRatio: CGFloat = -0.5
}
private var hostingControllerBox: AnyObject?
private weak var hostedView: UIView?
private let visualMaskLayer = CAShapeLayer()
private let usesPlayStationFaceButtons: Bool
private var pinchBaseTransform: CGAffineTransform = .identity
private var panStartCenter: CGPoint = .zero
private var activeGestureCount = 0
private var didRestorePersistedState = false
private let minScale: CGFloat = 0.1
private let maxScale: CGFloat = 2.4
private let doubleTapWidths: [CGFloat] = [130, 200]
@objc init(frame: CGRect, usesPlayStationFaceButtons: Bool) {
self.usesPlayStationFaceButtons = usesPlayStationFaceButtons
super.init(frame: frame)
setupHostedViewIfNeeded()
}
override init(frame: CGRect) {
self.usesPlayStationFaceButtons = false
super.init(frame: frame)
setupHostedViewIfNeeded()
}
required init?(coder: NSCoder) {
self.usesPlayStationFaceButtons = false
super.init(coder: coder)
setupHostedViewIfNeeded()
}
private func setupHostedViewIfNeeded() {
guard hostedView == nil else { return }
let hostingController = UIHostingController(rootView: AbstractGamepadOverlayBridgeView(usesPlayStationFaceButtons: usesPlayStationFaceButtons))
hostingController.view.backgroundColor = .clear
hostingController.view.isUserInteractionEnabled = false
hostingController.view.translatesAutoresizingMaskIntoConstraints = true
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostingController.view.frame = bounds
hostingControllerBox = hostingController
hostedView = hostingController.view
backgroundColor = .clear
clipsToBounds = false
isMultipleTouchEnabled = true
layer.mask = visualMaskLayer
addSubview(hostingController.view)
installGesturesIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
hostedView?.frame = bounds
updateVisualMask()
restorePersistedStateIfNeeded()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
didRestorePersistedState = false
restorePersistedStateIfNeeded()
}
private func installGesturesIfNeeded() {
guard gestureRecognizers?.isEmpty != false else { return }
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
pan.minimumNumberOfTouches = 1
pan.maximumNumberOfTouches = 1
pan.cancelsTouchesInView = true
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
pinch.cancelsTouchesInView = true
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
doubleTap.numberOfTapsRequired = 2
doubleTap.cancelsTouchesInView = true
pan.delegate = self
pinch.delegate = self
doubleTap.delegate = self
pan.require(toFail: doubleTap)
addGestureRecognizer(pan)
addGestureRecognizer(pinch)
addGestureRecognizer(doubleTap)
}
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
guard let superview else { return }
switch recognizer.state {
case .began:
presentFirstTouchTipIfNeeded()
beginGestureSession()
panStartCenter = center
case .changed, .ended:
let translation = recognizer.translation(in: superview)
center = CGPoint(x: panStartCenter.x + translation.x, y: panStartCenter.y + translation.y)
if recognizer.state == .ended {
endGestureSession()
}
case .cancelled, .failed:
endGestureSession()
default:
break
}
}
@objc private func handlePinch(_ recognizer: UIPinchGestureRecognizer) {
switch recognizer.state {
case .began:
presentFirstTouchTipIfNeeded()
beginGestureSession()
pinchBaseTransform = transform
case .changed, .ended:
let baseScale = currentScale(from: pinchBaseTransform)
let nextScale = max(minScale, min(maxScale, baseScale * recognizer.scale))
transform = CGAffineTransform(scaleX: nextScale, y: nextScale)
if recognizer.state == .ended {
endGestureSession()
}
case .cancelled, .failed:
endGestureSession()
default:
break
}
}
@objc private func handleDoubleTap(_ recognizer: UITapGestureRecognizer) {
guard recognizer.state == .ended else { return }
presentFirstTouchTipIfNeeded()
applyScale(forTargetWidth: nextDoubleTapWidth())
persistState()
}
private func currentScale(from transform: CGAffineTransform) -> CGFloat {
sqrt((transform.a * transform.a) + (transform.c * transform.c))
}
private func updateVisualMask() {
guard bounds.width > 1, bounds.height > 1 else {
visualMaskLayer.path = nil
return
}
let maskWidth = bounds.width * OverlayMaskMetrics.widthRatio
let maskHeight = bounds.height * OverlayMaskMetrics.heightRatio
let maskRect = CGRect(
x: (bounds.width - maskWidth) * 0.5,
y: (bounds.height - maskHeight) * 0.5 + bounds.height * OverlayMaskMetrics.verticalOffsetRatio,
width: maskWidth,
height: maskHeight
)
let maskCornerRadius = min(maskRect.width, maskRect.height) * OverlayMaskMetrics.cornerRadiusRatio
let topRectWidth = bounds.width * OverlayMaskMetrics.topRectWidthRatio
let topRectHeight = bounds.height * OverlayMaskMetrics.topRectHeightRatio
let topRect = CGRect(
x: (bounds.width - topRectWidth) * 0.5,
y: (bounds.height - topRectHeight) * 0.5 + bounds.height * OverlayMaskMetrics.topRectVerticalOffsetRatio,
width: topRectWidth,
height: topRectHeight
)
let topRectCornerRadius = min(topRect.width, topRect.height) * OverlayMaskMetrics.topRectCornerRadiusRatio
visualMaskLayer.frame = bounds
let path = UIBezierPath(
roundedRect: maskRect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: maskCornerRadius, height: maskCornerRadius)
)
path.append(
UIBezierPath(
roundedRect: topRect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: topRectCornerRadius, height: topRectCornerRadius)
)
)
visualMaskLayer.path = path.cgPath
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
bounds.contains(point)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}
private func restorePersistedStateIfNeeded() {
guard !didRestorePersistedState,
let superview,
bounds.width > 0,
bounds.height > 0,
superview.bounds.width > 0,
superview.bounds.height > 0 else { return }
defer { didRestorePersistedState = true }
guard let state = AbstractGamepadOverlayPersistence.loadOverlayState() else { return }
let restoredScale = max(minScale, min(maxScale, state.size.width / bounds.width))
transform = CGAffineTransform(scaleX: restoredScale, y: restoredScale)
center = AbstractGamepadOverlayPersistence.center(in: superview, from: state.centerRatio)
}
private func beginGestureSession() {
activeGestureCount += 1
}
private func presentFirstTouchTipIfNeeded() {
guard GenericUtils.isFirstLaunchGamepadOverlayFeature() else { return }
guard let presentingViewController = nearestViewController() else { return }
let alertController = UIAlertController(
title: GenericUtils.gamepadOverlayFeatureTipTitle(),
message: GenericUtils.gamepadOverlayFeatureTipMessage(),
preferredStyle: .alert
)
alertController.addAction(
UIAlertAction(
title: GenericUtils.gamepadOverlayFeatureTipButtonTitle(),
style: .default
)
)
presentingViewController.present(alertController, animated: true)
}
private func nearestViewController() -> UIViewController? {
var responder: UIResponder? = self
while let current = responder {
if let viewController = current as? UIViewController {
return viewController
}
responder = current.next
}
return nil
}
private func endGestureSession() {
activeGestureCount = max(0, activeGestureCount - 1)
guard activeGestureCount == 0 else { return }
persistState()
}
private func persistState() {
guard let superview,
superview.bounds.width > 0,
superview.bounds.height > 0 else { return }
AbstractGamepadOverlayPersistence.saveOverlayState(
centerRatio: AbstractGamepadOverlayPersistence.centerRatio(for: self, in: superview),
size: frame.size
)
}
private func nextDoubleTapWidth() -> CGFloat {
let currentWidth = frame.width
return abs(currentWidth - doubleTapWidths[0]) <= abs(currentWidth - doubleTapWidths[1])
? doubleTapWidths[1]
: doubleTapWidths[0]
}
private func applyScale(forTargetWidth targetWidth: CGFloat) {
guard bounds.width > 0 else { return }
let targetScale = max(minScale, min(maxScale, targetWidth / bounds.width))
transform = CGAffineTransform(scaleX: targetScale, y: targetScale)
}
}
@available(iOS 13.0, *)
extension AbstractGamepadOverlayView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
(gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPinchGestureRecognizer)
|| (gestureRecognizer is UIPinchGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer)
}
}
+2
View File
@@ -133,6 +133,7 @@ import UIKit
"SETTINGS",
"TOOLBOX",
"WIDGETTOOL",
"ABSGAMEPAD",
"WIDGETPROFILES",
"PROFILES",
"PICKPROFILE",
@@ -146,6 +147,7 @@ import UIKit
"ERASER",
"NOSINGLETOUCH",
"DISABLETOUCH",
"GAMEPADOVERLAY",
]
@objc public static let pencilProButtonCmds: [String] = [
"PENCILHOVER",
+128 -125
View File
@@ -1139,148 +1139,151 @@ double rc_expo(double x, double expo) {
[ControllerUtil listenWithController:controller swapABXY:self->_swapABXYButtons handler:^(NSDictionary * buttonDict, GCExtendedGamepad * gamepad, GCControllerElement * element) {
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){
if(button.pressed){
if(buttonFlagId.intValue == self->_controllerMouseSwitch){
self->_mouseSwitchButtonPressed = true;
self->mouseSwitchDownTimestamp = CACurrentMediaTime();
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){
if(button.pressed){
if(buttonFlagId.intValue == self->_controllerMouseSwitch){
self->_mouseSwitchButtonPressed = true;
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{
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 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 switchGyroOnOffByControllerButton];
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 switchGyroOnOffByControllerButton];
}
if (buttonFlagId.intValue == self->_controllerGyroSwitchHold
&& !self->_controllerGyroSwitchHoldPressed) {
self->_controllerGyroSwitchHoldPressed = true;
self->_gyroEnabledFlag = !self->_reverseHoldButton;
[self switchGyroOnOffByControllerButton];
}
}
if (buttonFlagId.intValue == self->_controllerGyroSwitchHold
&& !self->_controllerGyroSwitchHoldPressed) {
self->_controllerGyroSwitchHoldPressed = true;
self->_gyroEnabledFlag = !self->_reverseHoldButton;
[self switchGyroOnOffByControllerButton];
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 switchGyroOnOffByControllerButton];
}
}
}
else{
if (buttonFlagId.intValue == self->_controllerGyroSwitchToggle
&& self->_controllerGyroSwitchTogglePressed) {
self->_controllerGyroSwitchTogglePressed = false;
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);
}
if (buttonFlagId.intValue == self->_controllerGyroSwitchHold
&& self->_controllerGyroSwitchHoldPressed) {
self->_controllerGyroSwitchHoldPressed = false;
self->_gyroEnabledFlag = self->_reverseHoldButton;
[self switchGyroOnOffByControllerButton];
}
}
}
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);
}
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];
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;
}
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) {
[self handleControllerTouchpad:voidController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]
index:1];
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];
}
}
});
}];
/*
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
controller.extendedGamepad.valueChangedHandler =
^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
VoidController* voidController = [self->_voidControllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
short leftStickX, leftStickY;
short rightStickX, rightStickY;
+75
View File
@@ -8,6 +8,9 @@
import GameController
import Combine
import CoreGraphics
import Foundation
@objc enum ControllerButton: Int {
// Face buttons
@@ -99,6 +102,11 @@ import GameController
}
}
handler(buttonDict, gamepad, element)
if #available(iOS 13.0, *) {
if controller.playerIndex == .index1 {
GamepadOverlayStateCenter.shared.publish(snapshot: GamepadOverlaySnapshot(gamepad: gamepad))
}
}
}
}
@@ -247,3 +255,70 @@ import GameController
return CGVector(dx: circulatedX, dy: circulatedY)
}
}
@available(iOS 13.0, *)
struct GamepadOverlaySnapshot: Equatable {
var pressedButtons: Set<ControllerButton> = []
var dpadHighlight: Int = 0
var leftStick: CGPoint = .zero
var rightStick: CGPoint = .zero
var leftTrigger: CGFloat = 0
var rightTrigger: CGFloat = 0
static let idle = GamepadOverlaySnapshot()
init() {}
init(gamepad: GCExtendedGamepad) {
var pressedButtons = Set<ControllerButton>()
if gamepad.buttonA.isPressed { pressedButtons.insert(.a) }
if gamepad.buttonB.isPressed { pressedButtons.insert(.b) }
if gamepad.buttonX.isPressed { pressedButtons.insert(.x) }
if gamepad.buttonY.isPressed { pressedButtons.insert(.y) }
if gamepad.leftShoulder.isPressed { pressedButtons.insert(.leftShoulder) }
if gamepad.rightShoulder.isPressed { pressedButtons.insert(.rightShoulder) }
if gamepad.buttonMenu.isPressed { pressedButtons.insert(.menu) }
if gamepad.buttonOptions?.isPressed == true { pressedButtons.insert(.back) }
if #available(iOS 14.0, *), gamepad.buttonHome?.isPressed == true { pressedButtons.insert(.special) }
if gamepad.leftThumbstickButton?.isPressed == true { pressedButtons.insert(.leftStickButton) }
if gamepad.rightThumbstickButton?.isPressed == true { pressedButtons.insert(.rightStickButton) }
var dpadHighlight = 0
if gamepad.dpad.up.isPressed { dpadHighlight |= DPadHighlight.up.rawValue }
if gamepad.dpad.down.isPressed { dpadHighlight |= DPadHighlight.down.rawValue }
if gamepad.dpad.left.isPressed { dpadHighlight |= DPadHighlight.left.rawValue }
if gamepad.dpad.right.isPressed { dpadHighlight |= DPadHighlight.right.rawValue }
self.pressedButtons = pressedButtons
self.dpadHighlight = dpadHighlight
self.leftStick = CGPoint(
x: CGFloat(max(-1, min(1, gamepad.leftThumbstick.xAxis.value))),
y: CGFloat(max(-1, min(1, gamepad.leftThumbstick.yAxis.value)))
)
self.rightStick = CGPoint(
x: CGFloat(max(-1, min(1, gamepad.rightThumbstick.xAxis.value))),
y: CGFloat(max(-1, min(1, gamepad.rightThumbstick.yAxis.value)))
)
self.leftTrigger = CGFloat(max(0, min(1, gamepad.leftTrigger.value)))
self.rightTrigger = CGFloat(max(0, min(1, gamepad.rightTrigger.value)))
}
}
@available(iOS 13.0, *)
@objc(GamepadOverlayStateCenter)
final class GamepadOverlayStateCenter: NSObject, ObservableObject {
static let shared = GamepadOverlayStateCenter()
@Published private(set) var snapshot: GamepadOverlaySnapshot = .idle
func publish(snapshot: GamepadOverlaySnapshot) {
DispatchQueue.main.async {
self.snapshot = snapshot
}
}
@objc static func clearSharedState() {
shared.publish(snapshot: .idle)
}
}
+3
View File
@@ -58,6 +58,9 @@ typedef NS_ENUM(NSInteger, PencilHoverMode) {
@property (nonatomic, assign) bool reverseGyroHoldButton;
@property (nonatomic, assign) int controllerGyroSwitchHold;
@property (nonatomic, assign) int controllerGyroSwitchToggle;
@property (nonatomic, assign) bool gamepadOverlayEnabled;
@property NSArray<NSNumber *> *pressureCurvePoints;
// @property NSArray<NSNumber *> *initialTouchPressureCurvePoints;
@property (nonatomic, assign) int phase1StrokeSampleIndexEnd;
+5 -1
View File
@@ -51,6 +51,8 @@
[encoder encodeBool:self.reverseGyroHoldButton forKey:@"reverseGyroHoldButton"];
[encoder encodeInt:self.controllerGyroSwitchHold forKey:@"controllerGyroSwitchHold"];
[encoder encodeInt:self.controllerGyroSwitchToggle forKey:@"controllerGyroSwitchToggle"];
[encoder encodeBool:self.gamepadOverlayEnabled forKey:@"gamepadOverlayEnabled"];
[encoder encodeObject:self.pressureCurvePoints forKey:@"pressureCurvePoints"];
// [encoder encodeObject:self.initialTouchPressureCurvePoints forKey:@"initialTouchPressureCurvePoints"];
[encoder encodeInt:self.phase1StrokeSampleIndexEnd forKey:@"phase1StrokeSampleIndexEnd"];
@@ -112,7 +114,8 @@
self.reverseGyroHoldButton = [decoder containsValueForKey:@"reverseGyroHoldButton"] ? [decoder decodeBoolForKey:@"reverseGyroHoldButton"] : false;
self.controllerGyroSwitchHold = [decoder containsValueForKey:@"controllerGyroSwitchHold"] ? [decoder decodeIntForKey:@"controllerGyroSwitchHold"] : ControllerButtonNull;
self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerButtonNull;
self.gamepadOverlayEnabled = [decoder containsValueForKey:@"gamepadOverlayEnabled"] ? [decoder decodeBoolForKey:@"gamepadOverlayEnabled"] : true;
self.pressureCurvePoints =
[decoder containsValueForKey:@"pressureCurvePoints"]
? [decoder decodeObjectOfClasses:
@@ -209,6 +212,7 @@
copy.reverseGyroHoldButton = self.reverseGyroHoldButton;
copy.controllerGyroSwitchHold = self.controllerGyroSwitchHold;
copy.controllerGyroSwitchToggle = self.controllerGyroSwitchToggle;
copy.gamepadOverlayEnabled = self.gamepadOverlayEnabled;
copy.pressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.pressureCurvePoints copyItems:YES];
// copy.initialTouchPressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.initialTouchPressureCurvePoints copyItems:YES];
copy.phase1StrokeSampleIndexEnd = self.phase1StrokeSampleIndexEnd;
+48 -6
View File
@@ -10,6 +10,11 @@ import UIKit
import SVGKit
@objc class OnScreenWidgetView: UIView {
@objc(widgetWithCmdString:buttonLabel:shape:profile:)
class func widget(cmdString: String, buttonLabel: String, shape: String, profile: OSCProfile) -> OnScreenWidgetView {
return OnScreenWidgetView(cmdString: cmdString, buttonLabel: buttonLabel, shape: shape, profile: profile)
}
@objc public static var mapping: [Int16:OnScreenWidgetView] = [:]
@objc public static var isRestoring: Bool = false
@objc public static func set(widget:OnScreenWidgetView, for key:Int16) {
@@ -55,6 +60,7 @@ import SVGKit
func replaceEraser(shortcut:String)
func presentPressureCurveVC()
func toggleTouch(disabled:Bool)
func toggleGamepadOverlay(overlayEnabled:Bool)
}
@objc enum WidgetTypeEnum: UInt8 {
@@ -77,7 +83,7 @@ import SVGKit
@objc public var cmdString: String
@objc public var sequence: Int16 = -1
private var buttonString: String = ""
@objc private(set) var functionalButtonString: String = ""
@objc var functionalButtonString: String = ""
public var motionControlButtonString: String = ""
@objc public var touchPadString: String = ""
// super combo key string set
@@ -348,7 +354,7 @@ import SVGKit
else {
self.widgetType = WidgetTypeEnum.button // legacy combo button connected by "+"
}
print("widgetType: \(self.widgetType)")
print("touchPadString: \(self.touchPadString)")
for comboButtonString in comboButtonStrings {
@@ -498,7 +504,8 @@ import SVGKit
|| (self.widgetType == WidgetTypeEnum.button
&& (buttonMode == .slideAndHold || buttonMode == .slideToToggle)))*/
self.hasTrackPoint = true
self.hasNonEditableLabel = self.cmdString == "DISABLETOUCH"
self.hasNonEditableLabel = (self.cmdString == "DISABLETOUCH"
|| self.cmdString == "GAMEPADOVERLAY")
}
// ======================================================================================================
@@ -801,13 +808,20 @@ import SVGKit
return false
}
private func setupAtrributedText(){
@objc func setupAtrributedText(){
var text = self.widgetLabel.contains("#") ? "\(self.widgetLabel.split(separator: "#").first ?? "")" : SwiftLocalizationHelper.localizedString(forKey: self.widgetLabel)
if self.hasNonEditableLabel {
if cmdString == "DISABLETOUCH" {
switch cmdString {
case "DISABLETOUCH":
self.nonEditableWidgetLabel = SwiftLocalizationHelper.localizedString(forKey: touchDisabledFLag ? "=EnableTouch" : "=DisableTouch" )
case "GAMEPADOVERLAY":
self.nonEditableWidgetLabel = SwiftLocalizationHelper.localizedString(forKey: gamepadOverlayFLag ? "=GamepadOverlayOn" : "=GamepadOverlayOff" )
default:
nonEditableWidgetLabel = ""
}
text = self.nonEditableWidgetLabel
}
@@ -850,6 +864,7 @@ import SVGKit
self.setupAtrributedText()
label.translatesAutoresizingMaskIntoConstraints = false // enable auto alignment for the label
label.isHidden = shouldHideStandardLabel()
self.translatesAutoresizingMaskIntoConstraints = true // this is mandatory to prevent unexpected key view location change
@@ -901,6 +916,7 @@ import SVGKit
// self.layer.shadowOpacity = 0.5
addSubview(label)
_ = installCustomContentIfNeeded()
if(OnScreenWidgetView.editMode) {self.changeAndActivateContraints()}
@@ -924,6 +940,19 @@ import SVGKit
QUICK_TAP_TIME_INTERVAL = 0.15
}
}
@objc func shouldHideStandardLabel() -> Bool {
false
}
@discardableResult
@objc func installCustomContentIfNeeded() -> Bool {
false
}
@objc func handleQuickDoubleTapAction() -> Bool {
false
}
@objc public func setupL3R3Indicator() {
CATransaction.begin()
@@ -1820,6 +1849,10 @@ import SVGKit
touchTapTimeInterval = currentTime - touchTapTimeStamp
touchTapTimeStamp = currentTime
quickDoubleTapDetected = touchTapTimeInterval < QUICK_TAP_TIME_INTERVAL
if quickDoubleTapDetected, handleQuickDoubleTapAction() {
quickDoubleTapDetected = false
return
}
if quickDoubleTapDetected, self.isFolder, self.buttonMode == .slideAndHold {
self.temporarilyMovable = true
}
@@ -2523,6 +2556,8 @@ import SVGKit
}
case "DISABLETOUCH":
self.handleTouchDisableButtonUp()
case "GAMEPADOVERLAY":
self.gamepadOverlayButtonUp()
default:
break
}
@@ -2534,6 +2569,14 @@ import SVGKit
self.setupAtrributedText()
self.functionalButtonDelegate?.toggleTouch(disabled: touchDisabledFLag)
}
@objc var gamepadOverlayFLag:Bool = false
private func gamepadOverlayButtonUp(){
self.relocatedDuringStreaming = true
gamepadOverlayFLag = !gamepadOverlayFLag
self.setupAtrributedText()
self.functionalButtonDelegate?.toggleGamepadOverlay(overlayEnabled: gamepadOverlayFLag)
}
private func temporaryDisableFolderButtonAnimation(){
OnScreenWidgetView.enableFolderAnimation = false
@@ -3204,4 +3247,3 @@ import SVGKit
print("onScreenWidgetView deinit \(CACurrentMediaTime())")
}
}
+10 -1
View File
@@ -31,6 +31,7 @@
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
/*
Stream Video has been moved out of this class to _renderView in StreamFrameViewController.
*/
@@ -597,6 +598,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
newProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence;
newProfile.postExclusiveUnfoldedSequences = OnScreenWidgetView.postExclusiveUnfoldedSequences;
newProfile.gamepadOverlayEnabled = ((StreamFrameViewController*)_streamFrameVC).virtualGamepadOverlay != nil;
[oscProfileMan replaceSelectedProfileWith:newProfile overwriteDefault:YES];
}
@@ -612,6 +614,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// get streamFrameVC
if(!self->_streamFrameVC) self->_streamFrameVC = [self parentViewController];
if (@available(iOS 13.0, *)) {
if(oscProfile.gamepadOverlayEnabled) [(StreamFrameViewController* )self->_streamFrameVC loadAbstractGamepadOverlayIfNeeded];
}
// bool customOscEnabled = [self isOscEnabled] && settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
if(![self isOnScreenWidgetEnabled]) return;
@@ -629,7 +635,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
NSData* buttonStateEncoded = oscProfile.buttonStatesEncoded[i];
OnScreenButtonState* buttonState = [self->oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded];
if(buttonState.widgetType == CustomOnScreenWidget){
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView
OnScreenWidgetView* widgetView = [OnScreenWidgetView widgetWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView
widgetView.functionalButtonDelegate = (id<OnScreenFunctionalButtonDelegate>)self->_streamFrameVC;
widgetView.motionHandler = motionHandler;
@@ -682,6 +688,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[widgetView setupAutoTapTimer];
[widgetView setupInertialScrollerWithFps:self->settings.framerate.intValue];
widgetView.gamepadOverlayFLag = oscProfile.gamepadOverlayEnabled;
[widgetView setupAtrributedText];
if(sequenceGenerated){
// NSLog(@"widgetView.sequence %d %f", widgetView.sequence, CACurrentMediaTime());
buttonState.sequence = widgetView.sequence;
+154 -14
View File
@@ -675,6 +675,64 @@
}
}
},
"=GamepadOverlayOff" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "🟢 🎮 overlay"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "🟢 🎮 叠加"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "🟢 🎮 疊加"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "🟢 🎮 疊加"
}
}
}
},
"=GamepadOverlayOn" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "🚫 🎮 overlay"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "🚫 🎮 叠加"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "🚫 🎮 疊加"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "🚫 🎮 疊加"
}
}
}
},
"=gyroRsvpad" : {
"extractionState" : "manual",
"localizations" : {
@@ -1226,21 +1284,51 @@
}
}
},
"▴" : {
"comment" : "A symbol representing the \"up\" direction on a directional pad.",
"isCommentAutoGenerated" : true
"🎮 overlay" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "🎮 叠加"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "🎮 疊加"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "🎮 疊加"
}
}
}
},
"▸" : {
"comment" : "A directional arrow pointing to the right.",
"isCommentAutoGenerated" : true
},
"▾" : {
"comment" : "A symbol resembling a downward arrow.",
"isCommentAutoGenerated" : true
},
"◂" : {
"comment" : "A directional arrow pointing to the left.",
"isCommentAutoGenerated" : true
"🎮 overlay switch" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "🎮 叠加 开关"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "🎮 疊加 開關"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "🎮 疊加 開關"
}
}
}
},
"A button with touchpad functionality will be created" : {
"extractionState" : "manual",
@@ -4968,6 +5056,35 @@
}
}
},
"gamepadOverlayFeatureTip" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hello! I'm the VoidLink gamepad live feedback overlay. You can use the \"🎮\" button in the widget menu to turn me on or off.\nI can be resized by double-tap or two-finger pinch, and moved by dragging directly."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "你好!我是 VoidLink 手柄实时反馈叠加层。控件菜单中的 \"🎮\" 按键可以打开或关闭我。\n我可以由双击或双指捏合改变大小、直接拖动改变位置。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "你好!我是 VoidLink 手把即時反饋疊加層。控制選單中的「🎮」按鍵可以開啟或關閉我。\n我可以透過雙擊或雙指捏合改變大小,也可以直接拖曳改變位置。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "你好!我是 VoidLink 手把即時反饋疊加層。控制選單中的「🎮」按鍵可以開啟或關閉我。\n我可以透過雙擊或雙指捏合改變大小,也可以直接拖曳改變位置。"
}
}
}
},
"Gestures" : {
"extractionState" : "manual",
"localizations" : {
@@ -10733,6 +10850,29 @@
}
}
},
"Switch on/off a floating gamepad overlay with live input feedback" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "开关一个带有实时输入反馈的虚拟手柄叠加层"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "開關一個帶有即時輸入回饋的虛擬手把覆蓋層"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "開關一個帶有即時輸入回饋的虛擬手把覆蓋層"
}
}
}
},
"Tap 'OK' to set on-screen widget to 'Custom' and enable motion control." : {
"extractionState" : "manual",
"localizations" : {
+24
View File
@@ -41,6 +41,30 @@ import Foundation
}
return false
}
@objc static func isFirstLaunchGamepadOverlayFeature() -> Bool {
let key = "hasTouchedGamepadOverlayFeature20260405-1"
let defaults = UserDefaults.standard
let launchedBefore = defaults.bool(forKey: key)
if !launchedBefore {
defaults.set(true, forKey: key)
return true
}
return false
}
@objc static func gamepadOverlayFeatureTipTitle() -> String {
SwiftLocalizationHelper.localizedString(forKey: "Gamepad Overlay")
}
@objc static func gamepadOverlayFeatureTipMessage() -> String {
SwiftLocalizationHelper.localizedString(forKey: "gamepadOverlayFeatureTip")
}
@objc static func gamepadOverlayFeatureTipButtonTitle() -> String {
SwiftLocalizationHelper.localizedString(forKey: "Got it!")
}
@objc static func isIPhone() -> Bool {
return UIDevice.current.userInterfaceIdiom == .phone
@@ -162,7 +162,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) {
OnScreenButtonState* buttonState = [self->profilesManager unarchiveButtonStateEncoded:buttonStateEncoded];
NSLog(@"reloadOnScreenWidgets name %@", buttonState.name);
if(buttonState.widgetType == CustomOnScreenWidget){
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView
OnScreenWidgetView* widgetView = [OnScreenWidgetView widgetWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView
widgetView.sequence = buttonState.sequence == -1 ? ++sequence : buttonState.sequence;
[OnScreenWidgetView setWithWidget:widgetView for:widgetView.sequence];
@@ -926,7 +926,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) {
- (void) updateWidget:(OnScreenWidgetView* )widget byParams:(NSMutableDictionary* )widgetInitParams createNew:(bool)createNew{
if(![self isWidgetParamsValid:widgetInitParams]) return;
OSCProfile* profile = [[OSCProfilesManager sharedManager:CGRectZero] getSelectedProfile];
OnScreenWidgetView* newWidget = [[OnScreenWidgetView alloc] initWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"] profile:profile]; //reconstruct widgetView
OnScreenWidgetView* newWidget = [OnScreenWidgetView widgetWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"] profile:profile]; //reconstruct widgetView
newWidget.sequence = widget.sequence;
newWidget.revealMode = widget.revealMode;
newWidget.bulkMoveEnabled = widget.bulkMoveEnabled;
@@ -991,7 +991,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) {
if(![self isWidgetParamsValid:widgetInitParams]) return;
//saving & present the keyboard button:
OSCProfile* profile = [[OSCProfilesManager sharedManager:CGRectZero] getSelectedProfile];
OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"] profile:profile];
OnScreenWidgetView* widgetView = [OnScreenWidgetView widgetWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"] profile:profile];
[self.view insertSubview:widgetView belowSubview:self.widgetPanelStack];
widgetView.hidden = true;
widgetView.sequence = [widgetView getAvailableSequence];
@@ -348,7 +348,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
for case let encoded as Data in oscProfile.buttonStatesEncoded {
guard let buttonState = self.profilesManager?.unarchiveButtonStateEncoded(encoded) else { continue }
if buttonState.widgetType == 1 {
let widgetView = OnScreenWidgetView(cmdString: buttonState.name, buttonLabel: buttonState.alias, shape: buttonState.widgetShape, profile: oscProfile)
let widgetView = OnScreenWidgetView.widget(cmdString: buttonState.name, buttonLabel: buttonState.alias, shape: buttonState.widgetShape, profile: oscProfile)
widgetView.sequence = (buttonState.sequence == -1 || folder != nil) ? {
sequence += 1
return sequence
@@ -1014,12 +1014,12 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
private func updateWidget(_ widget: OnScreenWidgetView, params: NSMutableDictionary, createNew: Bool) {
guard isWidgetParamsValid(params) else { return }
let profile = OSCProfilesManager.sharedManager(.zero).getSelectedProfile()
guard let newWidget = OnScreenWidgetView(
let newWidget = OnScreenWidgetView.widget(
cmdString: params["cmdString"] as? String ?? "",
buttonLabel: params["buttonLabel"] as? String ?? "",
shape: params["shape"] as? String ?? "",
profile: profile
) as OnScreenWidgetView? else { return }
)
newWidget.sequence = widget.sequence
newWidget.revealMode = widget.revealMode
@@ -1079,7 +1079,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
private func createWidgetFromParams(_ params: NSMutableDictionary) {
guard isWidgetParamsValid(params) else { return }
let profile = OSCProfilesManager.sharedManager(.zero).getSelectedProfile()
let widgetView = OnScreenWidgetView(
let widgetView = OnScreenWidgetView.widget(
cmdString: params["cmdString"] as? String ?? "",
buttonLabel: params["buttonLabel"] as? String ?? "",
shape: params["shape"] as? String ?? "",
@@ -80,6 +80,7 @@
id _controllerConnectObserver;
id _controllerDisconnectObserver;
UIView* _debugGamepadOverlay;
#if TARGET_OS_TV
@@ -2495,4 +2496,3 @@ static NSMutableSet* hostList;
}
@end
@@ -41,11 +41,13 @@
@property (nonatomic, strong) MetalViewController *metalViewController;
@property (nonatomic, strong) ImGuiRenderer *imguiView;
@property (nonatomic, strong) UIView* virtualGamepadOverlay;
- (void)updatePreferredDisplayMode:(BOOL)streamActive;
- (void)setUserInteractionEnabledForStreamView:(bool)enabled;
- (bool)shallDisableGyroHotSwitch;
- (void)loadGameProfileConfigs:(OSCProfile* )profile;
- (void)loadAbstractGamepadOverlayIfNeeded API_AVAILABLE(ios(13.0));
@end
@@ -507,7 +507,7 @@
NSLog(@"pausing...");
nil;
});
// check to see if external screen is connected/disconnected
[[NSNotificationCenter defaultCenter] addObserver: self
@@ -1766,6 +1766,35 @@
dispatch_after(delayTime, dispatch_get_main_queue(), block);
}
- (void)toggleGamepadOverlayWithOverlayEnabled:(BOOL)overlayEnabled API_AVAILABLE(ios(13.0)){
if(overlayEnabled) [self loadAbstractGamepadOverlayIfNeeded];
else {
[_virtualGamepadOverlay removeFromSuperview];
_virtualGamepadOverlay = nil;
}
}
- (void)loadAbstractGamepadOverlayIfNeeded API_AVAILABLE(ios(13.0)){
if (_virtualGamepadOverlay != nil || self.view.window == nil) {
return;
}
BOOL usesXboxFaceButtons = _settings.emulatedControllerType.intValue == LI_CTYPE_XBOX;
CGFloat maxWidth = MIN(CGRectGetWidth(self.view.bounds) * 0.72, 620);
CGFloat width = MAX(420, maxWidth);
CGFloat height = width / 1.82;
CGRect overlayFrame = CGRectMake(0, 0, width, height);
AbstractGamepadOverlayView *overlayView = [[AbstractGamepadOverlayView alloc] initWithFrame:overlayFrame usesPlayStationFaceButtons:!usesXboxFaceButtons];
overlayView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
overlayView.userInteractionEnabled = YES;
[self.view addSubview:overlayView];
_virtualGamepadOverlay = overlayView;
}
- (void)dealloc {
NSLog(@"dealloc StreamFrameViewController %f", CACurrentMediaTime());
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5,7 +5,7 @@ import SwiftUI
struct VoidLinkSwiftUIApp: App {
var body: some Scene {
WindowGroup {
WidgetPickerView()
AbstractGamepadView()
}
}
}
+12 -10
View File
@@ -414,6 +414,16 @@ struct WidgetPickerView: View {
allowsShortcutCombo: false,
forcedComboMode: nil
),
FunctionalButtonOption(
label: SwiftLocalizationHelper.localizedString(forKey: "🎮 overlay switch"),
cmd: "GAMEPADOVERLAY",
tip: SwiftLocalizationHelper.localizedString(forKey: "Switch on/off a floating gamepad overlay with live input feedback"),
allowsKeyboardCombination: false,
allowsGamepadCombination: false,
allowsSkillCombo: false,
allowsShortcutCombo: false,
forcedComboMode: nil
),
FunctionalButtonOption(
label: SwiftLocalizationHelper.localizedString(forKey: "Pressure curve"),
cmd: "PRESSURECURVE",
@@ -654,6 +664,7 @@ struct WidgetPickerView: View {
case .gamepad:
AbstractGamepadView(
gamepadType: selectedGamepadType,
metricsProfile: .picker,
canSelectCommand: { canSelectCommand($0, source: .gamepad) },
isCommandSelected: { selectedCmds.contains($0) },
onCommandSelected: { appendCommand($0, source: .gamepad) },
@@ -1841,13 +1852,10 @@ struct WidgetPickerView: View {
if gamepadCommands.contains(cmd) {
return .gamepad
}
if keyboardCommands.contains(cmd) {
return .keyboard
}
if functionalCommands.contains(cmd) {
return .functional
}
return nil
return .keyboard
}
private func canonicalCommand(for cmd: String) -> String {
@@ -1877,12 +1885,6 @@ struct WidgetPickerView: View {
]
}
private var keyboardCommands: Set<String> {
Set(CommandManager.keyboardButtonMappings.keys)
.union(Set(CommandManager.mouseButtonMappings.keys))
.union(keyboardPadCommands)
}
private var functionalCommands: Set<String> {
Set(functionalButtonOptions.map(\.cmd)).union(gyroCommands)
}