From eb03b055ea1f937f541ff44ef91c80df3fb06fba Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Mon, 6 Apr 2026 23:07:43 +0800 Subject: [PATCH] folder button auto dock --- VoidLink/Base.lproj/iPad.storyboard | 326 +++++++------ VoidLink/Base.lproj/iPhone.storyboard | 455 ++++++++++-------- .../AbstractGamepadOverlayWidgetView.swift | 37 +- VoidLink/Input/CustomOSC/OSCProfilesManager.m | 2 + .../Input/CustomOSC/OnScreenButtonState.h | 3 + .../Input/CustomOSC/OnScreenButtonState.m | 6 + VoidLink/Input/OnScreenWidgetView.swift | 316 +++++++++++- VoidLink/Input/StreamView.m | 4 + VoidLink/Localization/Localizable.xcstrings | 75 +++ ...LayoutOnScreenControlsViewController.swift | 51 +- .../StreamFrameViewController.m | 13 +- VoidLink/mul.lproj/iPad.xcstrings | 24 + VoidLink/mul.lproj/iPhone.xcstrings | 24 + VoidLinkSwiftUI/AbstractGamepadView.swift | 2 +- 14 files changed, 967 insertions(+), 371 deletions(-) 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) } }