diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard
index dd8ba031..e918a80c 100644
--- a/VoidLink/Base.lproj/iPad.storyboard
+++ b/VoidLink/Base.lproj/iPad.storyboard
@@ -2626,7 +2626,7 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
@@ -3398,6 +3442,9 @@
+
+
+
@@ -3437,6 +3484,9 @@
+
+
+
diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard
index c96fefcc..087c9c37 100644
--- a/VoidLink/Base.lproj/iPhone.storyboard
+++ b/VoidLink/Base.lproj/iPhone.storyboard
@@ -315,7 +315,7 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -1127,6 +1172,9 @@
+
+
+
@@ -1134,7 +1182,7 @@
-
+
@@ -1166,6 +1214,9 @@
+
+
+
@@ -3420,16 +3471,16 @@
-
+
-
+
-
+
-
+
diff --git a/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift b/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift
index f9a8bc95..6ca95847 100644
--- a/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift
+++ b/VoidLink/Input/AbstractGamepadOverlayWidgetView.swift
@@ -134,28 +134,34 @@ final class AbstractGamepadOverlayView: UIView {
private weak var hostedView: UIView?
private let visualMaskLayer = CAShapeLayer()
private let usesPlayStationFaceButtons: Bool
- private var pinchBaseTransform: CGAffineTransform = .identity
+ private let baseRenderWidth: CGFloat = 420
+ private let baseAspectRatio: CGFloat = 1.82
+ private let initialDisplayWidth: CGFloat
+ private var pinchBaseWidth: CGFloat = 0
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]
+ private let minWidth: CGFloat = 70
+ private let maxWidth: CGFloat = 520
+ private let doubleTapWidths: [CGFloat] = [GenericUtils.isIPhone() ? 90 : 130, GenericUtils.isIPhone() ? 165 : 200]
@objc init(frame: CGRect, usesPlayStationFaceButtons: Bool) {
self.usesPlayStationFaceButtons = usesPlayStationFaceButtons
- super.init(frame: frame)
+ self.initialDisplayWidth = frame.width > 0 ? frame.width : baseRenderWidth
+ super.init(frame: CGRect(origin: frame.origin, size: CGSize(width: baseRenderWidth, height: baseRenderWidth / baseAspectRatio)))
setupHostedViewIfNeeded()
}
override init(frame: CGRect) {
self.usesPlayStationFaceButtons = false
- super.init(frame: frame)
+ self.initialDisplayWidth = frame.width > 0 ? frame.width : baseRenderWidth
+ super.init(frame: CGRect(origin: frame.origin, size: CGSize(width: baseRenderWidth, height: baseRenderWidth / baseAspectRatio)))
setupHostedViewIfNeeded()
}
required init?(coder: NSCoder) {
self.usesPlayStationFaceButtons = false
+ self.initialDisplayWidth = baseRenderWidth
super.init(coder: coder)
setupHostedViewIfNeeded()
}
@@ -176,6 +182,7 @@ final class AbstractGamepadOverlayView: UIView {
clipsToBounds = false
isMultipleTouchEnabled = true
layer.mask = visualMaskLayer
+ applyScale(forTargetWidth: initialDisplayWidth)
addSubview(hostingController.view)
installGesturesIfNeeded()
@@ -239,11 +246,10 @@ final class AbstractGamepadOverlayView: UIView {
case .began:
presentFirstTouchTipIfNeeded()
beginGestureSession()
- pinchBaseTransform = transform
+ pinchBaseWidth = frame.width
case .changed, .ended:
- let baseScale = currentScale(from: pinchBaseTransform)
- let nextScale = max(minScale, min(maxScale, baseScale * recognizer.scale))
- transform = CGAffineTransform(scaleX: nextScale, y: nextScale)
+ let nextWidth = max(minWidth, min(maxWidth, pinchBaseWidth * recognizer.scale))
+ applyScale(forTargetWidth: nextWidth)
if recognizer.state == .ended {
endGestureSession()
}
@@ -261,10 +267,6 @@ final class AbstractGamepadOverlayView: UIView {
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
@@ -325,8 +327,7 @@ final class AbstractGamepadOverlayView: UIView {
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)
+ applyScale(forTargetWidth: state.size.width)
center = AbstractGamepadOverlayPersistence.center(in: superview, from: state.centerRatio)
}
@@ -387,8 +388,8 @@ final class AbstractGamepadOverlayView: UIView {
}
private func applyScale(forTargetWidth targetWidth: CGFloat) {
- guard bounds.width > 0 else { return }
- let targetScale = max(minScale, min(maxScale, targetWidth / bounds.width))
+ let clampedWidth = max(minWidth, min(maxWidth, targetWidth))
+ let targetScale = clampedWidth / baseRenderWidth
transform = CGAffineTransform(scaleX: targetScale, y: targetScale)
}
}
diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m
index 72507260..3d028b22 100644
--- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m
+++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m
@@ -447,6 +447,8 @@ static CGRect layoutViewBounds;
buttonState.sequence = widgetView.sequence;
buttonState.sequenceSet = widgetView.sequenceSet;
buttonState.parentSequence = widgetView.parentSequence;
+ buttonState.autoDockTimer = widgetView.autoDockIdleDuration;
+ buttonState.dockedAlpha = widgetView.autoDockSettledAlpha;
buttonState.folded = widgetView.folded;
buttonState.revealMode = widgetView.revealMode;
buttonState.bulkMoveEnabled = widgetView.bulkMoveEnabled;
diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.h b/VoidLink/Input/CustomOSC/OnScreenButtonState.h
index 5e54b723..b3127521 100644
--- a/VoidLink/Input/CustomOSC/OnScreenButtonState.h
+++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.h
@@ -30,6 +30,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign) int16_t sequence;
@property (nonatomic, assign) int16_t parentSequence;
@property NSSet *sequenceSet;
+@property (nonatomic, assign) int16_t autoDockTimer;
+@property (nonatomic, assign) CGFloat dockedAlpha;
+
@property CGPoint position;
@property (nonatomic, assign) BOOL isHidden;
@property (nonatomic, assign) uint8_t buttonMode;
diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.m b/VoidLink/Input/CustomOSC/OnScreenButtonState.m
index f3f12b85..899933a2 100644
--- a/VoidLink/Input/CustomOSC/OnScreenButtonState.m
+++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.m
@@ -38,6 +38,9 @@
[encoder encodeInt32:self.sequence forKey:@"sequence"];
[encoder encodeInt32:self.parentSequence forKey:@"parentSequence"];
[encoder encodeObject:self.sequenceSet forKey:@"sequenceSet"];
+ [encoder encodeInt32:self.autoDockTimer forKey:@"autoDockTimer"];
+ [encoder encodeFloat:self.dockedAlpha forKey:@"dockedAlpha"];
+
[encoder encodeObject:self.alias forKey:@"alias"];
[encoder encodeInt:self.widgetType forKey:@"buttonType"]; // keep original key
[encoder encodeInt:self.sizeReference forKey:@"sizeReference"];
@@ -80,6 +83,9 @@
self.sequence = [decoder containsValueForKey:@"sequence"] ? [decoder decodeInt32ForKey:@"sequence"] : -1;
self.parentSequence = [decoder containsValueForKey:@"parentSequence"] ? [decoder decodeInt32ForKey:@"parentSequence"] : -1;
self.sequenceSet = [decoder containsValueForKey:@"sequenceSet"] ? [decoder decodeObjectForKey:@"sequenceSet"] : [NSSet set];
+ self.autoDockTimer = [decoder containsValueForKey:@"autoDockTimer"] ? [decoder decodeInt32ForKey:@"autoDockTimer"] : 10;
+ self.dockedAlpha = [decoder containsValueForKey:@"dockedAlpha"] ? [decoder decodeFloatForKey:@"dockedAlpha"] : 0.2;
+
self.alias = [decoder decodeObjectForKey:@"alias"];
self.widgetType = [decoder decodeIntForKey:@"buttonType"];
self.sizeReference = [decoder containsValueForKey:@"sizeReference"] ? [decoder decodeIntForKey:@"sizeReference"] : longSide;
diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift
index 88463b2a..8cd1dc8f 100644
--- a/VoidLink/Input/OnScreenWidgetView.swift
+++ b/VoidLink/Input/OnScreenWidgetView.swift
@@ -8,6 +8,7 @@
import UIKit
import SVGKit
+import ObjectiveC.runtime
@objc class OnScreenWidgetView: UIView {
@objc(widgetWithCmdString:buttonLabel:shape:profile:)
@@ -768,6 +769,7 @@ import SVGKit
}
if self.shape == "square" {
let widgetSize = getRecSize(widthFactor: self.widthFactor, heightFactor: self.heightFactor)
+ autoDockOriginalBoundsSize = widgetSize
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: widgetSize.width),
self.heightAnchor.constraint(equalToConstant: widgetSize.height),])
@@ -2457,6 +2459,7 @@ import SVGKit
}
private func handleFunctionalButtonDown(){
+ if autoDockIsDocked {return}
switch self.functionalButtonString {
case "FOLDER":
if self.buttonMode != .slideAndHold {break}
@@ -2486,6 +2489,7 @@ import SVGKit
private func handleFunctionalButtonUp(event: UIEvent? = nil){
// print("handleFunctionalButtonUp \(self.widgetLabel), event Empty: \(String(describing: event)), \(CACurrentMediaTime())")
+ if autoDockIsDocked {return}
if buttonMode == .movable {
if moveableButtonLongPressed() && !UITouchUtil.touches(in: self, from: event).isEmpty {return}
switch self.functionalButtonString {
@@ -2941,6 +2945,308 @@ import SVGKit
return true
}
+ // MARK: - Auto Dock
+ private static let autoDockExposedEdgeLength: CGFloat = 80
+ private static let autoDockExposedThickness: CGFloat = 17
+ private static let autoDockVerticalInset: CGFloat = 12
+ @objc var autoDockIdleDuration: TimeInterval = 5
+ private static let autoDockInitialAlpha: CGFloat = 0.8
+ @objc var autoDockSettledAlpha: CGFloat = 0.2
+ private static let autoDockSettledAlphaDelay: TimeInterval = 2
+ private static let autoDockReturnDamping: CGFloat = 0.9
+ private static let autoDockGoDamping: CGFloat = 0.82
+ private typealias TouchesIMP = @convention(c) (AnyObject, Selector, Set, UIEvent?) -> Void
+ private static var autoDockOriginalTouchesBeganIMP: TouchesIMP?
+ private static var autoDockOriginalTouchesMovedIMP: TouchesIMP?
+ private static var autoDockOriginalTouchesEndedIMP: TouchesIMP?
+ private static var autoDockOriginalTouchesCancelledIMP: TouchesIMP?
+ private static let autoDockSwizzlingInstalled: Void = {
+ let cls: AnyClass = OnScreenWidgetView.self
+ func swizzle(_ originalSelector: Selector, _ swizzledSelector: Selector) -> TouchesIMP? {
+ guard let originalMethod = class_getInstanceMethod(cls, originalSelector),
+ let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector) else { return nil }
+ let originalIMP = unsafeBitCast(method_getImplementation(originalMethod), to: TouchesIMP.self)
+ method_exchangeImplementations(originalMethod, swizzledMethod)
+ return originalIMP
+ }
+ autoDockOriginalTouchesBeganIMP = swizzle(#selector(OnScreenWidgetView.touchesBegan(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesBegan(_:with:)))
+ autoDockOriginalTouchesMovedIMP = swizzle(#selector(OnScreenWidgetView.touchesMoved(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesMoved(_:with:)))
+ autoDockOriginalTouchesEndedIMP = swizzle(#selector(OnScreenWidgetView.touchesEnded(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesEnded(_:with:)))
+ autoDockOriginalTouchesCancelledIMP = swizzle(#selector(OnScreenWidgetView.touchesCancelled(_:with:)), #selector(OnScreenWidgetView.vl_autoDock_touchesCancelled(_:with:)))
+ }()
+
+ private var autoDockTimer: Timer?
+ private var autoDockStoredCenter: CGPoint?
+ private var autoDockDockedCenter: CGPoint?
+ private var autoDockDockedToBottomEdge: Bool = false
+ private var autoDockIsDocked: Bool = false
+ private var autoDockEnabled: Bool = false
+ private var autoDockSettledAlphaTimer: Timer?
+ private var autoDockOriginalBoundsSize: CGSize = .zero
+
+ private static func installAutoDockIfNeeded() {
+ _ = autoDockSwizzlingInstalled
+ }
+
+ @objc private func vl_autoDock_touchesBegan(_ touches: Set, with event: UIEvent?) {
+ guard autoDockEnabled else {
+ OnScreenWidgetView.autoDockOriginalTouchesBeganIMP?(self, #selector(OnScreenWidgetView.touchesBegan(_:with:)), touches, event)
+ return
+ }
+ if autoDockIsDocked {
+ autoDockRestoreWidget(animated: true)
+ return
+ }
+ autoDockStopCountdown()
+ OnScreenWidgetView.autoDockOriginalTouchesBeganIMP?(self, #selector(OnScreenWidgetView.touchesBegan(_:with:)), touches, event)
+ }
+
+ @objc private func vl_autoDock_touchesMoved(_ touches: Set, with event: UIEvent?) {
+ guard autoDockEnabled else {
+ OnScreenWidgetView.autoDockOriginalTouchesMovedIMP?(self, #selector(OnScreenWidgetView.touchesMoved(_:with:)), touches, event)
+ return
+ }
+ if autoDockIsDocked {
+ return
+ }
+ autoDockStopCountdown()
+ OnScreenWidgetView.autoDockOriginalTouchesMovedIMP?(self, #selector(OnScreenWidgetView.touchesMoved(_:with:)), touches, event)
+ }
+
+ @objc private func vl_autoDock_touchesEnded(_ touches: Set, with event: UIEvent?) {
+ OnScreenWidgetView.autoDockOriginalTouchesEndedIMP?(self, #selector(OnScreenWidgetView.touchesEnded(_:with:)), touches, event)
+ guard autoDockEnabled else { return }
+ autoDockRestartCountdownIfNeeded()
+ }
+
+ @objc private func vl_autoDock_touchesCancelled(_ touches: Set, with event: UIEvent?) {
+ OnScreenWidgetView.autoDockOriginalTouchesCancelledIMP?(self, #selector(OnScreenWidgetView.touchesCancelled(_:with:)), touches, event)
+ guard autoDockEnabled else { return }
+ autoDockRestartCountdownIfNeeded()
+ }
+
+ @objc public func setAutoDockEnabled(_ enabled: Bool) {
+ autoDockEnabled = enabled
+ if enabled {
+ OnScreenWidgetView.installAutoDockIfNeeded()
+ autoDockRestartCountdownIfNeeded()
+ }
+ else {
+ autoDockStopCountdown()
+ autoDockStopSettledAlphaTimer()
+ if autoDockIsDocked {
+ autoDockRestoreWidget(animated: false)
+ }
+ autoDockRestoreOriginalSize()
+ label.alpha = 1
+ autoDockRestoreOriginalAlpha()
+ }
+ }
+
+ @objc public func restartAutoDockCountdown() {
+ guard autoDockEnabled else { return }
+ autoDockRestartCountdownIfNeeded()
+ }
+
+ @objc public func cancelAutoDockCountdown() {
+ autoDockStopCountdown()
+ }
+
+ @objc public func triggerAutoDockNow() {
+ guard autoDockEnabled else { return }
+ autoDockStopCountdown()
+ autoDockWidgetToNearestEdge()
+ }
+
+ @objc public func restoreFromAutoDock(animated: Bool) {
+ guard autoDockEnabled else { return }
+ autoDockRestoreWidget(animated: animated)
+ }
+
+ private func autoDockStopCountdown() {
+ autoDockTimer?.invalidate()
+ autoDockTimer = nil
+ }
+
+ private func autoDockStopSettledAlphaTimer() {
+ autoDockSettledAlphaTimer?.invalidate()
+ autoDockSettledAlphaTimer = nil
+ }
+
+ private func autoDockRestoreOriginalAlpha() {
+ alpha = widgetType == WidgetTypeEnum.touchPad ? 1 : 1
+ }
+
+ private func autoDockApplyTemporarySize() {
+ // autoDockOriginalBoundsSize = bounds.size
+ var adjustedFrame = frame
+ if autoDockDockedToBottomEdge {
+ adjustedFrame.size.width = OnScreenWidgetView.autoDockExposedEdgeLength
+ }
+ else {
+ adjustedFrame.size.height = OnScreenWidgetView.autoDockExposedEdgeLength
+ }
+ frame = adjustedFrame.integral
+ if shape == "round" {
+ layer.cornerRadius = min(bounds.width, bounds.height) / 2
+ }
+ else {
+ setSquareWidgetCornerRadius()
+ }
+ }
+
+ private func autoDockRestoreOriginalSize() {
+ guard autoDockOriginalBoundsSize != .zero else { return }
+ var adjustedFrame = frame
+ adjustedFrame.size = autoDockOriginalBoundsSize
+ frame = adjustedFrame.integral
+ if let hostView = superview {
+ if autoDockDockedToBottomEdge {
+ frame.origin.y = hostView.bounds.height - frame.height
+ }
+ else {
+ frame.origin.x = hostView.bounds.width - frame.width
+ }
+ }
+ if shape == "round" {
+ layer.cornerRadius = min(bounds.width, bounds.height) / 2
+ }
+ else {
+ setSquareWidgetCornerRadius()
+ }
+ }
+
+ private func autoDockRestartCountdownIfNeeded() {
+ autoDockStopCountdown()
+ autoDockStopSettledAlphaTimer()
+ guard !OnScreenWidgetView.editMode,
+ autoDockEnabled,
+ superview != nil,
+ window != nil,
+ !isHidden,
+ // alpha > 0.01,
+ !autoDockIsDocked else {
+ return
+ }
+ print("autoDockIdleDuration \(autoDockIdleDuration) \(CACurrentMediaTime())")
+ autoDockTimer = Timer.scheduledTimer(withTimeInterval: autoDockIdleDuration, repeats: false) { [weak self] _ in
+ self?.autoDockWidgetToNearestEdge()
+ }
+ }
+
+ private func autoDockWidgetToNearestEdge() {
+ if !folded {
+ restartAutoDockCountdown()
+ return
+ }
+ self.isUserInteractionEnabled = false
+ guard let hostView = superview,
+ !OnScreenWidgetView.editMode,
+ autoDockEnabled,
+ !autoDockIsDocked,
+ !isHidden else {
+ return
+ }
+ hostView.layoutIfNeeded()
+ layoutIfNeeded()
+
+ autoDockStoredCenter = center
+ let rightDistance = hostView.bounds.width - frame.maxX
+ let bottomDistance = hostView.bounds.height - frame.maxY
+ autoDockDockedToBottomEdge = bottomDistance < rightDistance
+
+ let targetFrame = autoDockTargetFrame(in: hostView.bounds)
+ autoDockDockedCenter = CGPoint(x: targetFrame.midX, y: targetFrame.midY)
+
+ self.autoDockIsDocked = true
+ UIView.animate(
+ withDuration: 0.62,
+ delay: 0,
+ usingSpringWithDamping: OnScreenWidgetView.autoDockGoDamping,
+ initialSpringVelocity: 0.35,
+ options: [.allowUserInteraction, .curveEaseOut]
+ ) {
+ self.frame = targetFrame
+ self.transform = CGAffineTransform(scaleX: 0.985, y: 0.985)
+ } completion: { _ in
+ UIView.animate(withDuration: 0.14, delay: 0, options: [.allowUserInteraction, .curveEaseOut]) {
+ self.autoDockApplyTemporarySize()
+ self.transform = .identity
+ self.label.alpha = 0
+ self.alpha = OnScreenWidgetView.autoDockInitialAlpha
+ } completion: { _ in
+ self.isUserInteractionEnabled = true
+ self.autoDockStopSettledAlphaTimer()
+ self.autoDockSettledAlphaTimer = Timer.scheduledTimer(withTimeInterval: OnScreenWidgetView.autoDockSettledAlphaDelay, repeats: false) { [weak self] _ in
+ guard let self, self.autoDockIsDocked else { return }
+ UIView.animate(withDuration: 0.2, delay: 0, options: [.allowUserInteraction, .curveEaseOut]) {
+ self.isUserInteractionEnabled = true
+ self.alpha = self.autoDockSettledAlpha
+ }
+ }
+ }
+ }
+ }
+
+ private func autoDockRestoreWidget(animated: Bool) {
+ guard autoDockIsDocked, let restoreCenter = autoDockStoredCenter else { return }
+ autoDockStopCountdown()
+ autoDockStopSettledAlphaTimer()
+ autoDockRestoreOriginalSize()
+ label.alpha = 1
+ autoDockRestoreOriginalAlpha()
+
+ let animations = {
+ self.center = restoreCenter
+ self.transform = .identity
+ }
+
+ let completion: (Bool) -> Void = { _ in
+ self.autoDockDockedCenter = nil
+ self.autoDockRestartCountdownIfNeeded()
+ }
+
+ if animated {
+ UIView.animate(
+ withDuration: 0.38,
+ delay: 0,
+ usingSpringWithDamping: OnScreenWidgetView.autoDockReturnDamping,
+ initialSpringVelocity: 0.22,
+ options: [.allowUserInteraction, .curveEaseOut],
+ animations: animations,
+ completion: {_ in
+ self.autoDockIsDocked = false
+ self.restartAutoDockCountdown()
+ }
+ )
+ }
+ else {
+ animations()
+ completion(true)
+ }
+ }
+
+ private func autoDockTargetFrame(in bounds: CGRect) -> CGRect {
+ var targetFrame = frame
+ if autoDockDockedToBottomEdge {
+ targetFrame.origin.x = min(
+ max(targetFrame.origin.x, OnScreenWidgetView.autoDockVerticalInset),
+ bounds.width - targetFrame.width - OnScreenWidgetView.autoDockVerticalInset
+ )
+ targetFrame.origin.y = bounds.height - OnScreenWidgetView.autoDockExposedThickness
+ }
+ else {
+ targetFrame.origin.y = min(
+ max(targetFrame.origin.y, OnScreenWidgetView.autoDockVerticalInset),
+ bounds.height - targetFrame.height - OnScreenWidgetView.autoDockVerticalInset
+ )
+ targetFrame.origin.x = bounds.width - OnScreenWidgetView.autoDockExposedThickness
+ }
+ return targetFrame.integral
+ }
+ /// ================================================================================================================
+
+
@objc func getAvailableSequence() -> Int16 {
var sequence:Int16 = 0
self.forEachWidget(){ widget in
@@ -3143,7 +3449,14 @@ import SVGKit
override func didMoveToSuperview() {
super.didMoveToSuperview()
+ OnScreenWidgetView.installAutoDockIfNeeded()
if superview == nil {
+ autoDockStopCountdown()
+ autoDockStopSettledAlphaTimer()
+ autoDockRestoreOriginalSize()
+ autoDockIsDocked = false
+ label.alpha = 1
+ autoDockRestoreOriginalAlpha()
if self.motionControlButtonString == "GYRO" {
self.motionHandler?.stopGyroUpdate(interruptNoneGyroInput: true)
self.motionHandler?.gyroStarter = nil
@@ -3177,6 +3490,7 @@ import SVGKit
else{
self.superViewWidth = (superview?.bounds.size.width)!
self.superViewHeight = (superview?.bounds.size.height)!
+ autoDockRestartCountdownIfNeeded()
}
}
@@ -3244,6 +3558,6 @@ import SVGKit
}
deinit {
- print("onScreenWidgetView deinit \(CACurrentMediaTime())")
+ print("onScreenWidgetView deinit \(self.widgetLabel) \(CACurrentMediaTime())")
}
}
diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m
index 9df212e5..5fe3363f 100644
--- a/VoidLink/Input/StreamView.m
+++ b/VoidLink/Input/StreamView.m
@@ -646,6 +646,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[OnScreenWidgetView setWithWidget:widgetView for:widgetView.sequence];
widgetView.sequenceSet = buttonState.sequenceSet;
widgetView.parentSequence = buttonState.parentSequence;
+ widgetView.autoDockIdleDuration = buttonState.autoDockTimer;
+ widgetView.autoDockSettledAlpha = buttonState.dockedAlpha;
widgetView.folded = buttonState.folded;
widgetView.persistedFolded = buttonState.folded;
widgetView.revealMode = buttonState.revealMode;
@@ -691,6 +693,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
widgetView.gamepadOverlayFLag = oscProfile.gamepadOverlayEnabled;
[widgetView setupAtrributedText];
+ [widgetView setAutoDockEnabled:widgetView.isFolder && widgetView.parentSequence < 0 && widgetView.autoDockIdleDuration>0];
+
if(sequenceGenerated){
// NSLog(@"widgetView.sequence %d %f", widgetView.sequence, CACurrentMediaTime());
buttonState.sequence = widgetView.sequence;
diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings
index 11d4fc34..be577c2d 100644
--- a/VoidLink/Localization/Localizable.xcstrings
+++ b/VoidLink/Localization/Localizable.xcstrings
@@ -1779,6 +1779,52 @@
}
}
},
+ "Auto dock disabled" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "自动吸附: 已关闭"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "自動吸附: 已關閉"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "自動吸附: 已關閉"
+ }
+ }
+ }
+ },
+ "Auto dock: %d s" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "自动吸附: %d 秒"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "自動吸附: %d 秒"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "自動吸附: %d 秒"
+ }
+ }
+ }
+ },
"Auto tap disabled" : {
"extractionState" : "manual",
"localizations" : {
@@ -3437,6 +3483,35 @@
}
}
},
+ "Docked alpha: %d%%" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Docked alpha: %d%"
+ }
+ },
+ "zh-Hans" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "吸附后透明度: %d%%"
+ }
+ },
+ "zh-Hant" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "吸附後透明度: %d%%"
+ }
+ },
+ "zh-HK" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "吸附後透明度: %d%%"
+ }
+ }
+ }
+ },
"Done" : {
"extractionState" : "manual",
"localizations" : {
diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift
index 12278b89..49a3b0cc 100644
--- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift
+++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift
@@ -137,22 +137,18 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
@IBOutlet weak var bulkAlphaLabel: UILabel!
@IBOutlet weak var bulkAlphaSlider: UISlider!
-
@IBOutlet weak var bulkBorderWidthStack: UIStackView!
@IBOutlet weak var bulkBorderWidthSlider: UISlider!
@IBOutlet weak var bulkBorderWidthLabel: UILabel!
-
@IBOutlet weak var bulkLabelAlphaStack: UIStackView!
@IBOutlet weak var bulkLabelAlphaLabel: UILabel!
@IBOutlet weak var bulkLabelAlphaSlider: UISlider!
-
@IBOutlet weak var bulkBorderAlphaStack: UIStackView!
@IBOutlet weak var bulkBorderAlphaLabel: UILabel!
@IBOutlet weak var bulkBorderAlphaSlider: UISlider!
-
@IBOutlet weak var bulkHighlightAlphaStack: UIStackView!
@IBOutlet weak var bulkHighlightAlphaLabel: UILabel!
@IBOutlet weak var bulkHighlightAlphaSlider: UISlider!
@@ -161,6 +157,14 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
@IBOutlet weak var bulkHighlightSizeLabel: UILabel!
@IBOutlet weak var bulkHighlightSizeSlider: UISlider!
+ @IBOutlet weak var autoDockTimerStack: UIStackView!
+ @IBOutlet weak var autoDockTimerLabel: UILabel!
+ @IBOutlet weak var autoDockTimerSlider: UISlider!
+
+ @IBOutlet weak var dockedAlphaStack: UIStackView!
+ @IBOutlet weak var dockedAlphaLabel: UILabel!
+ @IBOutlet weak var dockedAlphaSlider: UISlider!
+
@IBOutlet weak var widgetPanelStack: UIStackView!
@IBOutlet private weak var toolbarTopConstraintiPhone: NSLayoutConstraint!
@@ -357,6 +361,8 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
OnScreenWidgetView.set(widget: widgetView, for: widgetView.sequence)
widgetView.sequenceSet = buttonState.sequenceSet as? Set ?? Set()
widgetView.parentSequence = buttonState.parentSequence
+ widgetView.autoDockIdleDuration = TimeInterval(buttonState.autoDockTimer)
+ widgetView.autoDockSettledAlpha = CGFloat(buttonState.dockedAlpha)
widgetView.folded = buttonState.folded
widgetView.revealMode = RevealMode(rawValue: Int(buttonState.revealMode)) ?? .coexist
widgetView.bulkMoveEnabled = buttonState.bulkMoveEnabled
@@ -827,6 +833,15 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
mouseDownButtonStack.isHidden = !widgetView.isMousePadWithButtonActions
mouseButtonDownSelector.selectedSegmentIndex = Int(widgetView.mouseButtonAction.rawValue)
+
+
+ autoDockTimerStack.isHidden = !(widgetView.isFolder && widgetView.parentSequence == -1);
+ autoDockTimerSlider.value = Float(widgetView.autoDockIdleDuration)
+ autoDockTimerSliderMoved(autoDockTimerSlider)
+
+ dockedAlphaStack.isHidden = !(widgetView.isFolder && widgetView.parentSequence == -1);
+ dockedAlphaSlider.value = Float(widgetView.autoDockSettledAlpha)
+ dockedAlphaSliderMoved(dockedAlphaSlider)
buttonModeStack.isHidden = widgetView.widgetType != .button
buttonModeSelector.setEnabled(!widgetView.isFunctionalButton, forSegmentAt: Int(ButtonMode.slideToToggle.rawValue))
@@ -834,7 +849,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
buttonModeSelector.setEnabled(!widgetView.isFolder, forSegmentAt: Int(ButtonMode.regular.rawValue))
buttonModeSelector.setEnabled(!widgetView.isFunctionalButton || widgetView.isTapToToggleException, forSegmentAt: Int(ButtonMode.tapToToggle.rawValue))
buttonModeSelector.selectedSegmentIndex = Int(widgetView.buttonMode.rawValue)
-
+
collectedWidgetsStack.isHidden = !widgetView.isFolder
collectedWidgetsSelector.selectedSegmentIndex = widgetView.folded ? 1 : 0
revealModeSelector.selectedSegmentIndex = Int(widgetView.revealMode.rawValue)
@@ -1022,6 +1037,8 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
)
newWidget.sequence = widget.sequence
+ newWidget.autoDockIdleDuration = widget.autoDockIdleDuration
+ newWidget.autoDockSettledAlpha = widget.autoDockSettledAlpha
newWidget.revealMode = widget.revealMode
newWidget.bulkMoveEnabled = widget.bulkMoveEnabled
newWidget.guidelineDelegate = self
@@ -1161,7 +1178,7 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
let alert = UIAlertController(title: "", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: LocalizationHelper.localizedString(forKey: "Ok"), style: .default) { _ in
if !success {
- self.oscProfilesTableViewController?.profileViewRefresh()
+ // self.oscProfilesTableViewController?.profileViewRefresh()
}
})
present(alert, animated: true)
@@ -1380,6 +1397,14 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
stickIndicatorOffsetSlider.addTarget(self, action: #selector(stickIndicatorOffsetSliderMoved(_:)), for: .valueChanged)
stickIndicatorOffsetLabel.text = LocalizationHelper.localizedString(forKey: "Indicator offset")
stickIndicatorOffsetStack.isHidden = true
+
+ autoDockTimerSlider.addTarget(self, action: #selector(autoDockTimerSliderMoved(_:)), for: .valueChanged)
+ autoDockTimerLabel.text = LocalizationHelper.localizedString(forKey: "Auto dock")
+ autoDockTimerStack.isHidden = true;
+
+ dockedAlphaSlider.addTarget(self, action: #selector(dockedAlphaSliderMoved(_:)), for: .valueChanged)
+ dockedAlphaLabel.text = LocalizationHelper.localizedString(forKey: "Docked alpha")
+ dockedAlphaStack.isHidden = true;
let whiteAttrs: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white]
mouseButtonDownSelector.addTarget(self, action: #selector(mouseDownButtonChanged(_:)), for: .valueChanged)
@@ -1694,6 +1719,20 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg
return
}
+ @objc private func autoDockTimerSliderMoved(_ sender: UISlider) {
+ guard let selectedWidget = selectedWidgetView else {return}
+ selectedWidget.autoDockIdleDuration = TimeInterval(Int(sender.value))
+ autoDockTimerLabel.text = sender.value == 0 ? LocalizationHelper.localizedString(forKey: "Auto dock disabled", Int(sender.value)) : LocalizationHelper.localizedString(forKey: "Auto dock: %d s", Int(sender.value))
+ return
+ }
+
+ @objc private func dockedAlphaSliderMoved(_ sender: UISlider) {
+ guard let selectedWidget = selectedWidgetView else {return}
+ selectedWidget.autoDockSettledAlpha = CGFloat(sender.value)
+ dockedAlphaLabel.text = LocalizationHelper.localizedString(forKey: "Docked alpha: %d%%", Int(sender.value*100))
+ return
+ }
+
@objc private func mouseDownButtonChanged(_ sender: UISegmentedControl) {
selectedWidgetView?.mouseButtonAction = MouseButtonAction(rawValue: sender.selectedSegmentIndex) ?? .hovering
}
diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m
index c213ed3d..741325f1 100644
--- a/VoidLink/ViewControllers/StreamFrameViewController.m
+++ b/VoidLink/ViewControllers/StreamFrameViewController.m
@@ -1781,13 +1781,16 @@
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);
+ // CGFloat maxWidth = MIN(CGRectGetWidth(self.view.bounds) * 0.72, 620);
+ // CGFloat standardWidth = MAX(420, maxWidth);
+
+ CGFloat standardWidth = GenericUtils.isIPhone ? 165 : 200;
+
+ CGFloat standardhHeight = standardWidth / 1.82;
+ CGRect overlayFrame = CGRectMake(0, 0, standardWidth, standardhHeight);
AbstractGamepadOverlayView *overlayView = [[AbstractGamepadOverlayView alloc] initWithFrame:overlayFrame usesPlayStationFaceButtons:!usesXboxFaceButtons];
- overlayView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
+ overlayView.center = CGPointMake(self.view.bounds.size.width-standardWidth/2-20, self.view.bounds.size.height-standardhHeight/2-20);
overlayView.userInteractionEnabled = YES;
[self.view addSubview:overlayView];
diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings
index 90a2f20b..0cadec6e 100644
--- a/VoidLink/mul.lproj/iPad.xcstrings
+++ b/VoidLink/mul.lproj/iPad.xcstrings
@@ -3992,6 +3992,18 @@
}
}
},
+ "KKW-Q7-0tg.text" : {
+ "comment" : "Class = \"UILabel\"; text = \"Auto dock timer\"; ObjectID = \"KKW-Q7-0tg\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Auto dock timer"
+ }
+ }
+ }
+ },
"KNe-bN-CDb.segmentTitles[0]" : {
"comment" : "Class = \"UISegmentedControl\"; KNe-bN-CDb.segmentTitles[0] = \"Disabled\"; ObjectID = \"KNe-bN-CDb\";",
"extractionState" : "extracted_with_value",
@@ -5158,6 +5170,18 @@
}
}
},
+ "qFn-D7-7aD.text" : {
+ "comment" : "Class = \"UILabel\"; text = \"Docked alpha\"; ObjectID = \"qFn-D7-7aD\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Docked alpha"
+ }
+ }
+ }
+ },
"qNQ-fB-jhY.text" : {
"comment" : "Class = \"UILabel\"; text = \"Button mode\"; ObjectID = \"qNQ-fB-jhY\";",
"extractionState" : "extracted_with_value",
diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings
index 3d5fab12..62ccf246 100644
--- a/VoidLink/mul.lproj/iPhone.xcstrings
+++ b/VoidLink/mul.lproj/iPhone.xcstrings
@@ -2293,6 +2293,18 @@
}
}
},
+ "F8T-uC-SAR.text" : {
+ "comment" : "Class = \"UILabel\"; text = \"Auto dock timer\"; ObjectID = \"F8T-uC-SAR\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Auto dock timer"
+ }
+ }
+ }
+ },
"f46-Pt-0Pv.text" : {
"comment" : "Class = \"UILabel\"; text = \"Profile:\"; ObjectID = \"f46-Pt-0Pv\";",
"extractionState" : "extracted_with_value",
@@ -3061,6 +3073,18 @@
}
}
},
+ "kxI-bn-pRU.text" : {
+ "comment" : "Class = \"UILabel\"; text = \"Docked alpha\"; ObjectID = \"kxI-bn-pRU\";",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Docked alpha"
+ }
+ }
+ }
+ },
"KZ3-vJ-E8J.text" : {
"comment" : "Class = \"UILabel\"; text = \" \"; ObjectID = \"KZ3-vJ-E8J\";",
"extractionState" : "extracted_with_value",
diff --git a/VoidLinkSwiftUI/AbstractGamepadView.swift b/VoidLinkSwiftUI/AbstractGamepadView.swift
index 77d3a7d9..3423b8db 100644
--- a/VoidLinkSwiftUI/AbstractGamepadView.swift
+++ b/VoidLinkSwiftUI/AbstractGamepadView.swift
@@ -1586,7 +1586,7 @@ struct SmallRoundIconButton: View {
.offset(y: diameter * -0.042)
)
.frame(width: diameter, height: diameter)
- .shadow(color: Color.black.opacity(0.12), radius: 3, x: 0, y: 2)
+ .shadow(color: Color.black.opacity(0), radius: 3, x: 0, y: 2)
}
}