gamepad live feedback overlay
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -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];
|
||||
|
||||
+4
-4
@@ -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
@@ -5,7 +5,7 @@ import SwiftUI
|
||||
struct VoidLinkSwiftUIApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
WidgetPickerView()
|
||||
AbstractGamepadView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user