diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index e918a80c..5f84ad8d 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -2626,7 +2626,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -3513,6 +3597,10 @@ + + + + @@ -3523,6 +3611,10 @@ + + + + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index 087c9c37..bc724629 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -315,7 +315,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1239,6 +1329,10 @@ + + + + @@ -1251,6 +1345,10 @@ + + + + @@ -3471,16 +3569,19 @@ - + - + - + + + + - + diff --git a/VoidLink/Input/ControllerUtil.swift b/VoidLink/Input/ControllerUtil.swift index 662a3adb..7265683c 100644 --- a/VoidLink/Input/ControllerUtil.swift +++ b/VoidLink/Input/ControllerUtil.swift @@ -78,31 +78,6 @@ import Foundation // 单一 gamepad.valueChangedHandler gamepad.valueChangedHandler = { gamepad, element in - /* - navigationActionTriggeredPrivate = (gamepad.dpad.up.isPressed - || gamepad.dpad.down.isPressed - || gamepad.dpad.left.isPressed - || gamepad.dpad.right.isPressed - || gamepad.buttonA.isPressed - || gamepad.buttonB.isPressed - || gamepad.buttonX.isPressed - || gamepad.leftShoulder.isPressed - || gamepad.rightShoulder.isPressed - || gamepad.leftTrigger.value != 0 - || gamepad.rightTrigger.value != 0 - || gamepad.rightThumbstick.xAxis.value != 0 - || gamepad.rightThumbstick.yAxis.value != 0 - || gamepad.leftThumbstick.xAxis.value != 0 - || gamepad.leftThumbstick.yAxis.value != 0) - if navigationActionTriggeredPrivate { navigationActionTriggered = true} - else { - DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { - if !navigationActionTriggeredPrivate { - navigationActionTriggered = false - } - } - } - */ handler(buttonDict, gamepad, element) if #available(iOS 13.0, *) { if controller.playerIndex == .index1 { diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m index d909e9f1..e7802f94 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m @@ -478,7 +478,11 @@ static CGRect layoutViewBounds; buttonState.walkModeThreshold = widgetView.dWheelWalkModeThreshold; buttonState.minStickOffset = widgetView.minStickOffset; buttonState.buttonMode = widgetView.buttonMode; - + buttonState.sprintKeyActionType = widgetView.sprintKeyActionType; + buttonState.sprintKeyThreshold = widgetView.sprintKeyThreshold; + buttonState.walkKeyActionType = widgetView.walkKeyActionType; + buttonState.walkKeyThreshold = widgetView.walkKeyThreshold; + NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil]; [buttonStatesEncoded addObject: buttonStateEncoded]; } diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.h b/VoidLink/Input/CustomOSC/OnScreenButtonState.h index b3127521..073545a9 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.h +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.h @@ -65,6 +65,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) uint8_t mouseButtonAction; @property (nonatomic, assign) uint16_t autoTapInterval; +@property (nonatomic, assign) uint8_t walkKeyActionType; +@property (nonatomic, assign) CGFloat walkKeyThreshold; +@property (nonatomic, assign) uint8_t sprintKeyActionType; +@property (nonatomic, assign) CGFloat sprintKeyThreshold; + + // @property (nonatomic, assign) BOOL hasValidPosition; typedef NS_ENUM(NSInteger, OnScreenWidgetType) { diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.m b/VoidLink/Input/CustomOSC/OnScreenButtonState.m index 899933a2..4a5652f9 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.m +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.m @@ -72,6 +72,10 @@ [encoder encodeObject:self.widgetShape forKey:@"widgetShape"]; [encoder encodeFloat:self.walkModeThreshold forKey:@"walkModeThreshold"]; [encoder encodeFloat:self.minStickOffset forKey:@"minStickOffset"]; + [encoder encodeInt:self.sprintKeyActionType forKey:@"sprintKeyActionType"]; + [encoder encodeFloat:self.sprintKeyThreshold forKey:@"sprintKeyThreshold"]; + [encoder encodeInt:self.walkKeyActionType forKey:@"walkKeyActionType"]; + [encoder encodeFloat:self.walkKeyThreshold forKey:@"walkKeyThreshold"]; } - (id) initWithCoder:(NSCoder*)decoder { @@ -117,6 +121,12 @@ self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"]; self.walkModeThreshold = [decoder containsValueForKey:@"walkModeThreshold"] ? [decoder decodeFloatForKey:@"walkModeThreshold"] : 16383; self.minStickOffset = [decoder decodeFloatForKey:@"minStickOffset"]; + + self.sprintKeyActionType = [decoder containsValueForKey:@"sprintKeyActionType"] ? [decoder decodeIntForKey:@"sprintKeyActionType"] : WalkSprintKeyActionTypeHold; + self.sprintKeyThreshold = [decoder containsValueForKey:@"sprintKeyThreshold"] ? [decoder decodeFloatForKey:@"sprintKeyThreshold"] : 0.6; + self.walkKeyActionType = [decoder containsValueForKey:@"walkKeyActionType"] ? [decoder decodeIntForKey:@"walkKeyActionType"] : WalkSprintKeyActionTypeHold; + self.walkKeyThreshold = [decoder containsValueForKey:@"walkKeyThreshold"] ? [decoder decodeFloatForKey:@"walkKeyThreshold"] : 0.08; + } return self; } diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 408367a9..6c799182 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -174,6 +174,7 @@ import ObjectiveC.runtime @objc public var isTapToToggleException: Bool = false @objc public var hasHapticFeedback: Bool = false @objc public var isDirectionPad: Bool = false + @objc public var hasWalkSprintKeys: Bool = false @objc public var hasL3R3Indicator: Bool = false @objc public var isStickWheel: Bool = false @@ -232,10 +233,24 @@ import ObjectiveC.runtime @objc public var stickIndicatorOffset: CGFloat = 120 // for all LRUD pads + + @objc public enum WalkSprintKeyActionType: UInt8 { + case hold + case toggle + } + + @objc public var keyboardDpadThresholdRef: CGFloat = 300 + @objc public var walkKeyThreshold: CGFloat = 0.08 + @objc public var walkKeyActionType: WalkSprintKeyActionType = .hold + @objc public var sprintKeyThreshold: CGFloat = 0.6 + @objc public var sprintKeyActionType: WalkSprintKeyActionType = .hold + @objc public var upIndicator = CAShapeLayer() @objc public var downIndicator = CAShapeLayer() @objc public var leftIndicator = CAShapeLayer() @objc public var rightIndicator = CAShapeLayer() + @objc public var walkKeyThresholdPreviewLayer = CAShapeLayer() + @objc public var sprintKeyThresholdPreviewLayer = CAShapeLayer() // for DPAD LRUD pad @objc public var lrudIndicatorBall = CAShapeLayer() @@ -497,6 +512,8 @@ import ObjectiveC.runtime ) self.hasHapticFeedback = !self.comboButtonStrings.isEmpty || CommandManager.directionPads.contains(self.touchPadString) self.isDirectionPad = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.directionPads.contains(self.touchPadString) + self.hasWalkSprintKeys = self.isDirectionPad && (self.touchPadString == "WASDPAD" + || self.touchPadString == "ARROWPAD") self.isStickWheel = self.widgetType == WidgetTypeEnum.touchPad && CommandManager.stickWheels.contains(self.touchPadString) self.isFolder = self.cmdString == "FOLDER" @@ -635,6 +652,7 @@ import ObjectiveC.runtime // Re-setup widgetView style setupView() + updateMovementThresholdPreview() CATransaction.commit() } @@ -669,6 +687,8 @@ import ObjectiveC.runtime self.downIndicator.borderColor = self.buttonDownVisualEffectLayer.borderColor self.leftIndicator.borderColor = self.buttonDownVisualEffectLayer.borderColor self.rightIndicator.borderColor = self.buttonDownVisualEffectLayer.borderColor + self.walkKeyThresholdPreviewLayer.strokeColor = self.buttonDownVisualEffectLayer.borderColor + self.sprintKeyThresholdPreviewLayer.strokeColor = self.buttonDownVisualEffectLayer.borderColor } private func tweakAlpha(tweakBorderAlpha:Bool, tweakLabelAlpha:Bool = true){ @@ -1352,10 +1372,64 @@ import ObjectiveC.runtime } } + private var previousWalkMode:Bool = false + private var previousSprintMode:Bool = false private func handleLrudTouchMove(){ CATransaction.begin() CATransaction.setDisableActions(true) + + + if self.hasWalkSprintKeys { + let dist = hypot(self.offSetX, self.offSetY) + let isInWalkMode = dist < self.walkKeyThreshold * keyboardDpadThresholdRef * 0.5 + let isInSprintMode = dist > self.sprintKeyThreshold * keyboardDpadThresholdRef * 0.5 + + if (isInSprintMode != previousSprintMode || directionPadTouchBegan) + , self.comboButtonStrings.count>0 { + switch sprintKeyActionType { + case .hold: + if isInSprintMode {sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[0]])} + else {sendComboButtonsUpEvent(comboStrings: [comboButtonStrings[0]])} + case .toggle : + if directionPadTouchBegan {break} + sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[0]]) + DispatchQueue.global().asyncAfter(deadline: .now() + 0.025) { + self.sendComboButtonsUpEvent(comboStrings: [self.comboButtonStrings[0]]) + } + } + + if !directionPadTouchBegan, isInSprintMode, vibrationOn { + vibrationGenerator.prepare() + vibrationGenerator.impactOccurred() + } + } + + if (isInWalkMode != previousWalkMode || directionPadTouchBegan) + , self.comboButtonStrings.count>1 { + switch walkKeyActionType { + case .hold: + if isInWalkMode {sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[1]])} + else {sendComboButtonsUpEvent(comboStrings: [comboButtonStrings[1]])} + case .toggle : + if directionPadTouchBegan {break} + sendComboButtonsDownEvent(comboStrings: [comboButtonStrings[1]]) + DispatchQueue.global().asyncAfter(deadline: .now() + 0.025) { + self.sendComboButtonsUpEvent(comboStrings: [self.comboButtonStrings[1]]) + } + } + + if !directionPadTouchBegan, isInWalkMode, vibrationOn { + vibrationGenerator.prepare() + vibrationGenerator.impactOccurred() + } + } + + previousWalkMode = isInWalkMode + previousSprintMode = isInSprintMode + } + + let radians = atan2(-offSetY,offSetX) let degrees = radians * 180 / .pi @@ -1649,9 +1723,55 @@ import ObjectiveC.runtime self.layer.addSublayer(self.stickWheelAxis) self.stickWheelAxis.isHidden = false } + + setupMovementThresholdPreviewLayersIfNeeded() + updateMovementThresholdPreview() CATransaction.commit() } + + private func setupMovementThresholdPreviewLayersIfNeeded() { + for previewLayer in [sprintKeyThresholdPreviewLayer, walkKeyThresholdPreviewLayer] { + previewLayer.fillColor = UIColor.clear.cgColor + previewLayer.strokeColor = standardHighlightColor.cgColor + previewLayer.lineWidth = 3 + previewLayer.lineDashPattern = [8, 6] + previewLayer.isHidden = true + if previewLayer.superlayer == nil { + layer.insertSublayer(previewLayer, below: lrudIndicatorBall) + } + } + } + + private func updateMovementThresholdPreview(layer: CAShapeLayer, threshold: CGFloat) { + let clampedThreshold = max(0, min(1, threshold)) + let diameter: CGFloat = keyboardDpadThresholdRef * clampedThreshold + let radius = max(diameter / 2, 1) + layer.position = CGPoint(x: bounds.midX, y: bounds.midY) + layer.path = UIBezierPath( + arcCenter: .zero, + radius: radius, + startAngle: 0, + endAngle: .pi * 2, + clockwise: true + ).cgPath + } + + @objc public func updateMovementThresholdPreview() { + guard isDirectionPad, touchPadString == "WASDPAD" || touchPadString == "ARROWPAD" else { + walkKeyThresholdPreviewLayer.isHidden = true + sprintKeyThresholdPreviewLayer.isHidden = true + return + } + + setupMovementThresholdPreviewLayersIfNeeded() + updateMovementThresholdPreview(layer: sprintKeyThresholdPreviewLayer, threshold: sprintKeyThreshold) + if self.comboButtonStrings.count>1 {updateMovementThresholdPreview(layer: walkKeyThresholdPreviewLayer, threshold: walkKeyThreshold)} + + let shouldShow = OnScreenWidgetView.editMode + sprintKeyThresholdPreviewLayer.isHidden = !shouldShow + walkKeyThresholdPreviewLayer.isHidden = !(shouldShow && self.comboButtonStrings.count>1) + } @objc public func hideAllHighlightLayersOfAllWidgets(selfIncluded:Bool) { self.forEachWidget(){ widget in @@ -1663,6 +1783,8 @@ import ObjectiveC.runtime widget.downIndicator.isHidden = true widget.leftIndicator.isHidden = true widget.rightIndicator.isHidden = true + widget.walkKeyThresholdPreviewLayer.isHidden = true + widget.sprintKeyThresholdPreviewLayer.isHidden = true } } @@ -1921,7 +2043,7 @@ import ObjectiveC.runtime self.getVector(touch: touch) self.handleLrudTouchMove() } - if quickDoubleTapDetected { + if quickDoubleTapDetected && self.touchPadString == "DPAD" { self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings) DispatchQueue.global(qos: .userInteractive).async { usleep(100000) @@ -3445,7 +3567,13 @@ import ObjectiveC.runtime parentFolder?.sequenceSet.remove(widget.sequence) } widget.parentSequence = -1 - widget.sequenceSet.removeAll() + if widget.isFolder { + for sequence in widget.sequenceSet { + guard let subWidget = OnScreenWidgetView.mapping[sequence] else {continue} + subWidget.parentSequence = -1 + } + widget.sequenceSet.removeAll() + } } @objc static func restoreFoldedStates(){ diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index c75b184c..9234bd2a 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -675,6 +675,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; widgetView.minStickOffset = buttonState.minStickOffset; widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold; widgetView.buttonMode = buttonState.buttonMode; + widgetView.sprintKeyActionType = buttonState.sprintKeyActionType; + widgetView.sprintKeyThreshold = buttonState.sprintKeyThreshold; + widgetView.walkKeyActionType = buttonState.walkKeyActionType; + widgetView.walkKeyThreshold = buttonState.walkKeyThreshold; + // Add the widgetView to the view controller's view [self->_streamFrameTopLayerView addSubview:widgetView]; // add keyboard button to the stream frame view. must add it to the target view before setting location. buttonState.position = [self denormalizeWidgetPosition:buttonState.position]; diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 18d17bd5..83548894 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -47,6 +47,29 @@ } } }, + " Cmd: %@ Sprint: %@ Walk: %@ Parent: %@" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : " 命令: %@ 疾走: %@ 静步: %@ 收纳于: %@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 命令: %@ 疾走: %@ 靜步: %@ 收納於: %@" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : " 命令: %@ 疾走: %@ 靜步: %@ 收納於: %@" + } + } + } + }, " disconnect " : { "extractionState" : "manual", "localizations" : { @@ -162,7 +185,7 @@ } } }, - " Profile: %@ Alias: %@ Cmd: %@ Parent:%@" : { + " Profile: %@ Alias: %@ Cmd: %@ Parent: %@" : { "extractionState" : "manual", "localizations" : { "zh-Hans" : { @@ -300,6 +323,29 @@ } } }, + " Slide threshold: %0.2f " : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : " 触发距离: %0.2f " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 觸發距離: %0.2f " + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : " 觸發距離: %0.2f " + } + } + } + }, "!ML_TEST_RESULT_INCONCLUSIVE" : { "extractionState" : "manual", "localizations" : { @@ -8533,6 +8579,29 @@ } } }, + "Please select a sprint key" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请选择疾走键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "請選擇疾走鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "請選擇疾走鍵" + } + } + } + }, "Pofiles imported" : { "extractionState" : "manual", "localizations" : { @@ -10901,6 +10970,52 @@ } } }, + "Sprint and walk keys are set" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已设定疾走键和静步键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已設定疾走鍵和靜步鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "已設定疾走鍵和靜步鍵" + } + } + } + }, + "Sprint key set. Select a walk key (optional, you can skip and create)" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已设定疾走键,请选择静步键(可跳过并创建)" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已設定疾走鍵,請選擇靜步鍵(可跳過並建立)" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "已設定疾走鍵,請選擇靜步鍵(可跳過並建立)" + } + } + } + }, "Square" : { "extractionState" : "manual", "localizations" : { diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift index 5edee7b8..baa2995d 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.swift @@ -165,6 +165,16 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg @IBOutlet weak var dockedAlphaLabel: UILabel! @IBOutlet weak var dockedAlphaSlider: UISlider! + @IBOutlet weak var sprintKeyStack: UIStackView! + @IBOutlet weak var sprintKeySelector: UISegmentedControl! + @IBOutlet weak var sprintKeyThresholdLabel: UILabel! + @IBOutlet weak var sprintKeyThresholdSlider: UISlider! + + @IBOutlet weak var walkKeyStack: UIStackView! + @IBOutlet weak var walkKeySelector: UISegmentedControl! + @IBOutlet weak var walkKeyThresholdLabel: UILabel! + @IBOutlet weak var walkKeyThresholdSlider: UISlider! + @IBOutlet weak var widgetPanelStack: UIStackView! @IBOutlet private weak var toolbarTopConstraintiPhone: NSLayoutConstraint! @@ -293,6 +303,9 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg override func viewWillDisappear(_ animated: Bool) { OnScreenWidgetView.editMode = false OnScreenWidgetView.isTweakingHighlight = false + for case let widget as OnScreenWidgetView in onScreenWidgetViews { + widget.updateMovementThresholdPreview() + } super.viewWillDisappear(animated) NotificationCenter.default.post(name: Notification.Name("OscLayoutCloseNotification"), object: self) } @@ -388,6 +401,11 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg widgetView.dWheelWalkModeThreshold = buttonState.walkModeThreshold widgetView.minStickOffset = buttonState.minStickOffset widgetView.buttonMode = ButtonMode(rawValue: Int(buttonState.buttonMode)) ?? .slideToToggle + widgetView.sprintKeyActionType = OnScreenWidgetView.WalkSprintKeyActionType(rawValue: buttonState.sprintKeyActionType) ?? .hold + widgetView.sprintKeyThreshold = buttonState.sprintKeyThreshold + widgetView.walkKeyActionType = OnScreenWidgetView.WalkSprintKeyActionType(rawValue: buttonState.walkKeyActionType) ?? .hold + widgetView.walkKeyThreshold = buttonState.walkKeyThreshold + self.view.insertSubview(widgetView, belowSubview: self.widgetPanelStack) buttonState.position = self.denormalizeWidgetPosition(buttonState.position) widgetView.setLocation(position: buttonState.position) @@ -717,7 +735,14 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg autoFitLabel(currentProfileLabel) currentProfileLabel.textAlignment = .left - currentProfileLabel.text = LocalizationHelper.localizedString(forKey: " Profile: %@ Alias: %@ Cmd: %@ Parent:%@", profilesManager.getSelectedProfile().name, widgetView.widgetLabel, widgetView.cmdString, LocalizationHelper.localizedString(forKey:OnScreenWidgetView.mapping[widgetView.parentSequence]?.widgetLabel ?? "null")) + currentProfileLabel.text = LocalizationHelper.localizedString(forKey: " Profile: %@ Alias: %@ Cmd: %@ Parent: %@", profilesManager.getSelectedProfile().name, widgetView.widgetLabel, widgetView.cmdString, LocalizationHelper.localizedString(forKey:OnScreenWidgetView.mapping[widgetView.parentSequence]?.widgetLabel ?? "null")) + if selectedWidgetView?.hasWalkSprintKeys == true { + let cmdCounts = widgetView.comboButtonStrings.count + currentProfileLabel.text = LocalizationHelper.localizedString(forKey: " Cmd: %@ Sprint: %@ Walk: %@ Parent: %@", + widgetView.cmdString, cmdCounts>0 ? widgetView.comboButtonStrings[0] : LocalizationHelper.localizedString(forKey: "null"), + cmdCounts>1 ? widgetView.comboButtonStrings[1] : LocalizationHelper.localizedString(forKey: "null"), LocalizationHelper.localizedString(forKey:OnScreenWidgetView.mapping[widgetView.parentSequence]?.widgetLabel ?? "null")) + } + undoButton.alpha = widgetView.layoutChanges.count > 1 ? 1.0 : 0.3 widgetView.accessWidgetAttributes() @@ -755,12 +780,37 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg autoFitLabel(walkModeThresholdLabel) } + + sprintKeyStack.isHidden = !(widgetView.hasWalkSprintKeys && !widgetView.comboButtonStrings.isEmpty) + walkKeyStack.isHidden = !(widgetView.hasWalkSprintKeys && widgetView.comboButtonStrings.count > 1) + if widgetView.hasWalkSprintKeys { + let clampedWalkThreshold = min(widgetView.walkKeyThreshold, widgetView.sprintKeyThreshold) + if clampedWalkThreshold != widgetView.walkKeyThreshold { + widgetView.walkKeyThreshold = clampedWalkThreshold + } + + sprintKeyThresholdSlider.value = Float(widgetView.sprintKeyThreshold) + sprintKeyThresholdLabel.text = LocalizationHelper.localizedString(forKey: " Slide threshold: %0.2f ", sprintKeyThresholdSlider.value) + autoFitLabel(sprintKeyThresholdLabel) + sprintKeySelector.selectedSegmentIndex = Int(widgetView.sprintKeyActionType.rawValue) + + walkKeyThresholdSlider.value = Float(widgetView.walkKeyThreshold) + walkKeyThresholdLabel.text = LocalizationHelper.localizedString(forKey: " Slide threshold: %0.2f ", walkKeyThresholdSlider.value) + walkKeySelector.selectedSegmentIndex = Int(widgetView.walkKeyActionType.rawValue) + autoFitLabel(walkKeyThresholdLabel) + + widgetView.updateMovementThresholdPreview() + } else { + widgetView.updateMovementThresholdPreview() + } + + slideThresholdStack.isHidden = !widgetView.hasSlideThreshold if widgetView.hasSlideThreshold { slideThresholdSlider.minimumValue = Float(widgetView.slideThresholdMin) slideThresholdSlider.maximumValue = Float(widgetView.slideThresholdMax) slideThresholdSlider.value = Float(widgetView.slideThreshold) - slideThresholdLabel.text = LocalizationHelper.localizedString(forKey: "Slide threshold: %.1f", widgetView.slideThreshold) + slideThresholdLabel.text = LocalizationHelper.localizedString(forKey: " Slide threshold: %.1f ", widgetView.slideThreshold) autoFitLabel(slideThresholdLabel) } @@ -833,7 +883,7 @@ 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) @@ -900,6 +950,8 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg stickIndicatorOffsetStack.isHidden = true sensitivityXStack.isHidden = true sensitivityYStack.isHidden = true + sprintKeyStack.isHidden = true + walkKeyStack.isHidden = true mouseDownButtonStack.isHidden = true decelerationRateStack.isHidden = true @@ -1068,6 +1120,10 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg newWidget.setVibration(style: Int(widget.vibrationStyle)) newWidget.mouseButtonAction = widget.mouseButtonAction newWidget.buttonMode = widget.buttonMode + newWidget.sprintKeyActionType = widget.sprintKeyActionType + newWidget.sprintKeyThreshold = widget.sprintKeyThreshold + newWidget.walkKeyActionType = widget.walkKeyActionType + newWidget.walkKeyThreshold = widget.walkKeyThreshold guard newWidget.widgetType == widget.widgetType else { return } view.insertSubview(newWidget, belowSubview: widgetPanelStack) @@ -1386,9 +1442,21 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg sensitivityYSlider.addTarget(self, action: #selector(sensitivityYSliderMoved(_:)), for: .valueChanged) sensitivityYLabel.text = LocalizationHelper.localizedString(forKey: "SensitivityY") sensitivityYStack.isHidden = true + walkModeThresholdSlider.addTarget(self, action: #selector(walkModeThresholdSliderMoved(_:)), for: .valueChanged) walkModeThresholdLabel.text = LocalizationHelper.localizedString(forKey: "Walkmode threshold") walkModeThresholdStack.isHidden = true + + sprintKeyThresholdSlider.addTarget(self, action: #selector(sprintKeyThresholdSliderMoved(_:)), for: .valueChanged) + sprintKeyThresholdLabel.text = LocalizationHelper.localizedString(forKey: "Slide threshold") + sprintKeySelector.addTarget(self, action: #selector(sprintKeyActionChanged(_:)), for: .valueChanged) + sprintKeyStack.isHidden = true + + walkKeyThresholdSlider.addTarget(self, action: #selector(walkKeyThresholdSliderMoved(_:)), for: .valueChanged) + walkKeyThresholdLabel.text = LocalizationHelper.localizedString(forKey: "Slide threshold") + walkKeySelector.addTarget(self, action: #selector(walkKeyActionChanged(_:)), for: .valueChanged) + walkKeyStack.isHidden = true + minStickOffsetSlider.addTarget(self, action: #selector(minStickOffsetSliderMoved(_:)), for: .valueChanged) minStickOffsetLabel.text = LocalizationHelper.localizedString(forKey: "Minimum offset") minStickOffsetStack.isHidden = true @@ -1851,6 +1919,45 @@ final class LayoutOnScreenControlsViewController: UIViewController, OnScreenWidg slideThresholdLabel.text = LocalizationHelper.localizedString(forKey: "Slide threshold: %.1f", sender.value) selectedWidgetView?.slideThreshold = CGFloat(sender.value) } + + @objc private func sprintKeyActionChanged(_ sender: UISegmentedControl) { + selectedWidgetView?.sprintKeyActionType = OnScreenWidgetView.WalkSprintKeyActionType(rawValue: UInt8(sender.selectedSegmentIndex)) ?? .hold + } + + @objc private func sprintKeyThresholdSliderMoved(_ sender: UISlider) { + sprintKeyThresholdLabel.text = LocalizationHelper.localizedString(forKey: " Slide threshold: %0.2f ", sender.value) + autoFitLabel(sprintKeyThresholdLabel) + + guard OnScreenWidgetView.editMode, let selectedWidgetView, widgetViewSelected else { return } + + if sender.value < walkKeyThresholdSlider.value { + walkKeyThresholdSlider.value = sender.value + walkKeyThresholdLabel.text = LocalizationHelper.localizedString(forKey: " Slide threshold: %0.2f ", walkKeyThresholdSlider.value) + autoFitLabel(walkKeyThresholdLabel) + } + + selectedWidgetView.sprintKeyThreshold = CGFloat(sender.value) + selectedWidgetView.walkKeyThreshold = CGFloat(walkKeyThresholdSlider.value) + selectedWidgetView.updateMovementThresholdPreview() + } + + @objc private func walkKeyActionChanged(_ sender: UISegmentedControl) { + selectedWidgetView?.walkKeyActionType = OnScreenWidgetView.WalkSprintKeyActionType(rawValue: UInt8(sender.selectedSegmentIndex)) ?? .hold + } + + @objc private func walkKeyThresholdSliderMoved(_ sender: UISlider) { + if sender.value > sprintKeyThresholdSlider.value { + sender.value = sprintKeyThresholdSlider.value + } + + walkKeyThresholdLabel.text = LocalizationHelper.localizedString(forKey: " Slide threshold: %0.2f ", sender.value) + autoFitLabel(walkKeyThresholdLabel) + + guard OnScreenWidgetView.editMode, let selectedWidgetView, widgetViewSelected else { return } + + selectedWidgetView.walkKeyThreshold = CGFloat(sender.value) + selectedWidgetView.updateMovementThresholdPreview() + } @objc private func yawFactorSliderMoved(_ sender: UISlider) { yawFactorLabel.text = LocalizationHelper.localizedString(forKey: "Yaw factor: %.2f", sender.value) diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 0cadec6e..9c53f302 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -187,6 +187,66 @@ } } }, + "3fM-1s-S0k.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; 3fM-1s-S0k.segmentTitles[0] = \"hold\"; ObjectID = \"3fM-1s-S0k\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "hold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + } + } + }, + "3fM-1s-S0k.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; 3fM-1s-S0k.segmentTitles[1] = \"toggle\"; ObjectID = \"3fM-1s-S0k\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "toggle" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換" + } + } + } + }, "3rP-dh-TJi.text" : { "comment" : "Class = \"UILabel\"; text = \"Enable PiP\"; ObjectID = \"3rP-dh-TJi\";", "extractionState" : "extracted_with_value", @@ -427,6 +487,36 @@ } } }, + "5Uu-1K-ov7.text" : { + "comment" : "Class = \"UILabel\"; text = \"Sprint key\"; ObjectID = \"5Uu-1K-ov7\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sprint key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "疾走键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "疾走鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "疾走鍵" + } + } + } + }, "6Cy-aH-DXp.text" : { "comment" : "Class = \"UILabel\"; text = \"Curve Exponent\"; ObjectID = \"6Cy-aH-DXp\";", "extractionState" : "extracted_with_value", @@ -2305,6 +2395,66 @@ } } }, + "csH-cx-ZWH.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; csH-cx-ZWH.segmentTitles[0] = \"hold\"; ObjectID = \"csH-cx-ZWH\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "hold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + } + } + }, + "csH-cx-ZWH.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; csH-cx-ZWH.segmentTitles[1] = \"toggle\"; ObjectID = \"csH-cx-ZWH\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "toggle" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "切換" + } + } + } + }, "cyH-Fh-vW6.normalTitle" : { "comment" : "Class = \"UIButton\"; normalTitle = \"Button\"; ObjectID = \"cyH-Fh-vW6\";", "extractionState" : "extracted_with_value", @@ -2672,6 +2822,36 @@ } } }, + "eRk-iM-nN4.text" : { + "comment" : "Class = \"UILabel\"; text = \"Walk key\"; ObjectID = \"eRk-iM-nN4\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Walk key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "静步键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "靜步鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "靜步鍵" + } + } + } + }, "eTT-y7-oTM.configuration.title" : { "comment" : "Class = \"UIButton\"; configuration.title = \"9999999\"; ObjectID = \"eTT-y7-oTM\";", "extractionState" : "stale", @@ -3812,6 +3992,18 @@ } } }, + "kex-PV-gvq.text" : { + "comment" : "Class = \"UILabel\"; text = \"Slide threshold\"; ObjectID = \"kex-PV-gvq\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Slide threshold" + } + } + } + }, "kho-5R-daP.segmentTitles[0]" : { "comment" : "Class = \"UISegmentedControl\"; kho-5R-daP.segmentTitles[0] = \"3 Finger Tap\"; ObjectID = \"kho-5R-daP\";", "extractionState" : "extracted_with_value", @@ -5884,6 +6076,18 @@ } } }, + "v4J-ot-qpz.text" : { + "comment" : "Class = \"UILabel\"; text = \"Slide threshold\"; ObjectID = \"v4J-ot-qpz\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Slide threshold" + } + } + } + }, "v5f-j3-pEc.text" : { "comment" : "Class = \"UILabel\"; text = \"Custom Double Tap Keyboard Shortcut\"; ObjectID = \"v5f-j3-pEc\";", "extractionState" : "extracted_with_value", diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index 62ccf246..b62a58bc 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -2521,7 +2521,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "内置陀螺仪映射为" } }, @@ -2551,7 +2551,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "指针速度" } }, @@ -2995,6 +2995,66 @@ } } }, + "Jz7-vJ-mMl.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; Jz7-vJ-mMl.segmentTitles[0] = \"hold\"; ObjectID = \"Jz7-vJ-mMl\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "hold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + } + } + }, + "Jz7-vJ-mMl.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; Jz7-vJ-mMl.segmentTitles[1] = \"toggle\"; ObjectID = \"Jz7-vJ-mMl\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "toggle" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + } + } + }, "KbX-bp-pYg.text" : { "comment" : "Class = \"UILabel\"; text = \" \"; ObjectID = \"KbX-bp-pYg\";", "extractionState" : "extracted_with_value", @@ -3061,6 +3121,36 @@ } } }, + "KpH-R8-kny.text" : { + "comment" : "Class = \"UILabel\"; text = \"Walk key\"; ObjectID = \"KpH-R8-kny\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Walk key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "静步键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "靜步鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "靜步鍵" + } + } + } + }, "kva-gQ-CZh.text" : { "comment" : "Class = \"UILabel\"; text = \"Size\"; ObjectID = \"kva-gQ-CZh\";", "extractionState" : "extracted_with_value", @@ -3139,6 +3229,36 @@ } } }, + "lJx-Tn-7we.text" : { + "comment" : "Class = \"UILabel\"; text = \"Sprint key\"; ObjectID = \"lJx-Tn-7we\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Sprint key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "疾走键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "疾走鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "疾走鍵" + } + } + } + }, "LVq-7c-3zg.text" : { "comment" : "Class = \"UILabel\"; text = \"Performance Graph \"; ObjectID = \"LVq-7c-3zg\";", "extractionState" : "extracted_with_value", @@ -4009,7 +4129,7 @@ }, "zh-HK" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "滑出時鬆開" } } @@ -4039,7 +4159,7 @@ }, "zh-HK" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "滑動時按下" } } @@ -5119,6 +5239,18 @@ } } }, + "sYb-ef-Vkq.text" : { + "comment" : "Class = \"UILabel\"; text = \"Slide threshold\"; ObjectID = \"sYb-ef-Vkq\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Slide threshold" + } + } + } + }, "T0w-xM-ze7.text" : { "comment" : "Class = \"UILabel\"; text = \"Resolution \"; ObjectID = \"T0w-xM-ze7\";", "extractionState" : "extracted_with_value", @@ -5389,6 +5521,66 @@ } } }, + "TsJ-mx-pbL.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; TsJ-mx-pbL.segmentTitles[0] = \"hold\"; ObjectID = \"TsJ-mx-pbL\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "hold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "保持" + } + } + } + }, + "TsJ-mx-pbL.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; TsJ-mx-pbL.segmentTitles[1] = \"toggle\"; ObjectID = \"TsJ-mx-pbL\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "toggle" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换" + } + } + } + }, "ttV-kY-jCi.text" : { "comment" : "Class = \"UILabel\"; text = \"The recommended value is 2. 1 frame provides the lowest latency, but may require an Ethernet connection.\"; ObjectID = \"ttV-kY-jCi\";", "extractionState" : "stale", @@ -6007,6 +6199,18 @@ } } }, + "YQo-bc-mwR.text" : { + "comment" : "Class = \"UILabel\"; text = \"Slide threshold\"; ObjectID = \"YQo-bc-mwR\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Slide threshold" + } + } + } + }, "Z9M-yq-an2.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Import\"; ObjectID = \"Z9M-yq-an2\";", "extractionState" : "extracted_with_value", diff --git a/VoidLinkPreview/WidgetPickerView.swift b/VoidLinkPreview/WidgetPickerView.swift index 2ddfb41c..4c171bbe 100644 --- a/VoidLinkPreview/WidgetPickerView.swift +++ b/VoidLinkPreview/WidgetPickerView.swift @@ -612,9 +612,36 @@ struct WidgetPickerView: View { return isPad(firstItem) && !isPad(lastItem) && poolItems.contains(where: isPad) && poolItems.contains(where: { !isPad($0) }) } + private var selectedMovementPadCommand: String? { + poolItems.first(where: { $0.cmd == "WASDPAD" || $0.cmd == "ARROWPAD" })?.cmd + } + + private var movementPadSingleButtonCount: Int { + guard selectedMovementPadCommand != nil else { return 0 } + return poolItems.filter { !isPad($0) }.count + } + + private var movementPadNeedsWalkKeyTip: Bool { + selectedMovementPadCommand != nil && movementPadSingleButtonCount == 0 + } + + private var movementPadNeedsSprintKeyTip: Bool { + selectedMovementPadCommand != nil && movementPadSingleButtonCount == 1 + } + + private var movementPadHasReachedSingleKeyLimit: Bool { + selectedMovementPadCommand != nil && movementPadSingleButtonCount >= 2 + } + private func updateTipMessageForCurrentPoolState() { if poolItems.isEmpty { resetTipMessage() + } else if movementPadNeedsWalkKeyTip { + setTipMessage(SwiftLocalizationHelper.localizedString(forKey: "Please select a sprint key")) + } else if movementPadNeedsSprintKeyTip { + setTipMessage(SwiftLocalizationHelper.localizedString(forKey: "Sprint key set. Select a walk key (optional, you can skip and create)")) + } else if movementPadHasReachedSingleKeyLimit { + setTipMessage(SwiftLocalizationHelper.localizedString(forKey: "Sprint and walk keys are set")) } else if hasButtonThenPadCombination { setTipMessage(SwiftLocalizationHelper.localizedString(forKey: "A button with touchpad functionality will be created")) } else if hasPadThenButtonCombination { @@ -1332,6 +1359,14 @@ struct WidgetPickerView: View { return false } + if movementPadHasReachedSingleKeyLimit { + setTipMessage( + SwiftLocalizationHelper.localizedString(forKey: "Walk and sprint keys are already set. No more keys can be added"), + type: .error + ) + return false + } + let candidate = WidgetPoolItem( cmd: cmd, source: source, @@ -1774,11 +1809,19 @@ struct WidgetPickerView: View { keyboardPickerMode == .shortcutPicker } + private var containsMovementDirectionPad: Bool { + poolItems.contains { $0.cmd == "WASDPAD" || $0.cmd == "ARROWPAD" } + } + private var shouldBypassCreateWidgetSheet: Bool { if isShortcutPickerMode { return true } + if containsMovementDirectionPad { + return true + } + guard !poolItems.isEmpty else { return false } let hasVisibleConfigurationSection =