From 84adf3ac95c503e826d529c0a9f5962272d68ff2 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Tue, 7 Apr 2026 14:35:06 +0800 Subject: [PATCH] command picker for drawing toolkit --- VoidLink/Input/ControllerUtil.swift | 2 +- VoidLink/Input/OnScreenWidgetView.swift | 1 + VoidLink/Input/PencilHandler.swift | 80 ++++++- VoidLink/Localization/Localizable.xcstrings | 156 ++++++++++--- .../ViewControllers/SettingsViewController.h | 5 +- .../ViewControllers/SettingsViewController.m | 27 +++ VoidLinkSwiftUI/VirtualKeyboardView.swift | 209 ++++++++++++------ VoidLinkSwiftUI/WidgetPickerView.swift | 117 ++++++---- .../WidgetPickerViewController.swift | 6 + 9 files changed, 461 insertions(+), 142 deletions(-) diff --git a/VoidLink/Input/ControllerUtil.swift b/VoidLink/Input/ControllerUtil.swift index 4d3f57ff..ade31f7c 100644 --- a/VoidLink/Input/ControllerUtil.swift +++ b/VoidLink/Input/ControllerUtil.swift @@ -95,7 +95,7 @@ import Foundation || gamepad.leftThumbstick.yAxis.value != 0) if navigationActionTriggeredPrivate { navigationActionTriggered = true} else { - DispatchQueue.global().asyncAfter(deadline: .now() + 0.02) { + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { if !navigationActionTriggeredPrivate { navigationActionTriggered = false } diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 8cd1dc8f..c20d9e54 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -836,6 +836,7 @@ import ObjectiveC.runtime ] ) label.attributedText = attr + label.textAlignment = .center } private func setupView() { diff --git a/VoidLink/Input/PencilHandler.swift b/VoidLink/Input/PencilHandler.swift index 6f7ca52a..c7a7612b 100644 --- a/VoidLink/Input/PencilHandler.swift +++ b/VoidLink/Input/PencilHandler.swift @@ -416,12 +416,34 @@ import UIKit doubleTapShorcuts[i] = shortcut } } + + @available(iOS 13.0, *) + func widgetPickerViewController(_ controller: WidgetPickerViewController, didCreateWidget payload: NSDictionary) { + let params = payload.mutableCopy() as? NSMutableDictionary ?? NSMutableDictionary() + let pickerAction = ((params["pickerAction"] as? String) ?? "").lowercased() + params.removeObject(forKey: "pickerAction") + + if pickerAction == "create" { + + } + } @objc static public func enterDoubleTapShortcuts(in viewController: UIViewController){ - let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) - selectedProfile = oscProfileMan.getSelectedProfile() - guard let selectedProfile = selectedProfile else {return} - + if #available(iOS 13.0, *) { + let pickerViewController = WidgetPickerViewController() + pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate) + pickerViewController.keyboardPickerMode = .shortcutPicker + pickerViewController.tabIdentifiers = ["keyboard"] + pickerViewController.initialTabIdentifier = "keyboard" + pickerViewController.shortcutIdentifier = "eraser" + pickerViewController.shortcutPickerTipText = SwiftLocalizationHelper.localizedString(forKey: "Select eraser shortcut keys") + let nav = UINavigationController(rootViewController: pickerViewController) + nav.modalPresentationStyle = .overFullScreen + viewController.present(nav, animated: true) + return + } + + /* let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Eraser Shortcut"), message: SwiftLocalizationHelper.localizedString(forKey: "Enter eraser keyboard shortcut:"), preferredStyle: .alert) @@ -455,10 +477,24 @@ import UIKit viewController.present(alert, animated: true, completion: { }) + */ } + @available(iOS 13.0, *) @objc static public func enterBrushShortcut(in viewController: UIViewController){ - + let pickerViewController = WidgetPickerViewController() + pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate) + pickerViewController.keyboardPickerMode = .shortcutPicker + pickerViewController.tabIdentifiers = ["keyboard"] + pickerViewController.initialTabIdentifier = "keyboard" + pickerViewController.shortcutIdentifier = "brush" + pickerViewController.shortcutPickerTipText = SwiftLocalizationHelper.localizedString(forKey: "Select brush shortcut keys") + let nav = UINavigationController(rootViewController: pickerViewController) + nav.modalPresentationStyle = .overFullScreen + viewController.present(nav, animated: true) + return + + /* let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Brush Shortcut"), message: SwiftLocalizationHelper.localizedString(forKey: "Enter brush keyboard shortcut:"), preferredStyle: .alert) @@ -500,13 +536,28 @@ import UIKit viewController.present(alert, animated: true, completion: { }) - + */ } @objc static private(set) var squeezeStartShortcut:String = "" @objc static private(set) var squeezeEndShortcut:String = "" + @available(iOS 13.0, *) @objc static public func enterSqueezeShortcuts(in viewController: UIViewController){ + + let pickerViewController = WidgetPickerViewController() + pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate) + pickerViewController.keyboardPickerMode = .shortcutPicker + pickerViewController.tabIdentifiers = ["keyboard"] + pickerViewController.initialTabIdentifier = "keyboard" + pickerViewController.shortcutIdentifier = "squeezePress" + pickerViewController.shortcutPickerTipText = SwiftLocalizationHelper.localizedString(forKey: "squeezePressShortcutPickerTip") + let nav = UINavigationController(rootViewController: pickerViewController) + nav.modalPresentationStyle = .overFullScreen + viewController.present(nav, animated: true) + return + + /* let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero) selectedProfile = oscProfileMan.getSelectedProfile() guard let selectedProfile = selectedProfile else {return} @@ -543,10 +594,24 @@ import UIKit viewController.present(alert, animated: true, completion: { }) + */ } + @available(iOS 13.0, *) @objc static public func enterSqueezeEndShortcut(in viewController: UIViewController){ - + let pickerViewController = WidgetPickerViewController() + pickerViewController.delegate = (viewController as! any WidgetPickerViewControllerDelegate) + pickerViewController.keyboardPickerMode = .shortcutPicker + pickerViewController.tabIdentifiers = ["keyboard"] + pickerViewController.initialTabIdentifier = "keyboard" + pickerViewController.shortcutIdentifier = "squeezeRelease" + pickerViewController.shortcutPickerTipText = SwiftLocalizationHelper.localizedString(forKey: "squeezeReleaseShortcutPickerTip") + let nav = UINavigationController(rootViewController: pickerViewController) + nav.modalPresentationStyle = .overFullScreen + viewController.present(nav, animated: true) + return + + /* let alert = UIAlertController(title: SwiftLocalizationHelper.localizedString(forKey: "Squeeze Shortcut"), message: SwiftLocalizationHelper.localizedString(forKey: "enterSqueezeReleaseShort"), preferredStyle: .alert) @@ -588,6 +653,7 @@ import UIKit viewController.present(alert, animated: true, completion: { }) + */ } private func attachHoverLeave(normalizedLocation:CGPoint){ diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index be577c2d..e573db9a 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -2988,6 +2988,29 @@ } } }, + "Create shortcut" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "生成快捷键" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "生成快速鍵" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "生成快速鍵" + } + } + } + }, "Create widget" : { "extractionState" : "manual", "localizations" : { @@ -4192,35 +4215,6 @@ } } }, - "enterSqueezePressShort" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Enter shortcut on squeeze Press:" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "输入捏笔时发送的快捷键:" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "輸入捏筆時傳送的快速鍵:" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "輸入捏筆時傳送的快速鍵:" - } - } - } - }, "enterSqueezeReleaseShort" : { "extractionState" : "manual", "localizations" : { @@ -9997,6 +9991,29 @@ } } }, + "Select brush shortcut keys" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择笔刷快捷键组合" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇筆刷快速鍵組合" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇筆刷快速鍵組合" + } + } + } + }, "Select Control" : { "extractionState" : "manual", "localizations" : { @@ -10020,6 +10037,29 @@ } } }, + "Select eraser shortcut keys" : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择橡皮擦快捷键组合" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇橡皮擦快速鍵組合" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇橡皮擦快速鍵組合" + } + } + } + }, "Select Gyro Button" : { "extractionState" : "manual", "localizations" : { @@ -10700,6 +10740,64 @@ } } }, + "squeezePressShortcutPickerTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select shortcut keys on squeeze press" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择捏笔按压时发送的快捷键组合" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇捏筆按壓時發送的快捷鍵組合" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇捏筆按壓時發送的快捷鍵組合" + } + } + } + }, + "squeezeReleaseShortcutPickerTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select shortcut keys on squeeze release. To keep the previously selected keys pressed while squeezing, select “Null” on top right" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择捏笔松开时的快捷键组合, 若要在捏笔时保持之前选择的组合键按下, 选右上方”Null”" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇捏筆鬆開時的快速鍵組合,若要在捏筆時保持之前選擇的組合鍵按下,選右上方「Null」" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "選擇捏筆鬆開時的快速鍵組合,若要在捏筆時保持之前選擇的組合鍵按下,選右上方「Null」" + } + } + } + }, "squeezeShortcutStackTip" : { "extractionState" : "manual", "localizations" : { diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index d10b1b93..c2d9ed4e 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -17,9 +17,10 @@ @class LayoutOnScreenControlsViewController; @class MicHandler; -@protocol MicHandlerDelegate; +@class WidgetPickerViewController; +@protocol WidgetPickerViewControllerDelegate; -@interface SettingsViewController : UIViewController +@interface SettingsViewController : UIViewController @property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar; @property (strong, nonatomic) UIStackView *parentStack; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index f7d9cd68..9c5b1e9b 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -3614,6 +3614,33 @@ BOOL isCustomResolution(int resolutionSelected) { } } +- (void)widgetPickerViewController:(WidgetPickerViewController *)controller didCreateWidget:(NSDictionary *)payload API_AVAILABLE(ios(13.0)){ + OSCProfilesManager * profileMan = [OSCProfilesManager sharedManager:CGRectZero]; + OSCProfile* profile = [profileMan getSelectedProfile]; + NSString* shortcutIdentifier = [payload objectForKey:@"shortcutIdentifier"]; + + if([shortcutIdentifier isEqualToString:@"eraser"]) { + profile.eraserShortcut = [payload objectForKey:@"cmdString"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.8 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [PencilHandler enterBrushShortcutIn:self]; + }); + } + if([shortcutIdentifier isEqualToString:@"brush"]) { + profile.brushShortcut = [payload objectForKey:@"cmdString"]; + } + if([shortcutIdentifier isEqualToString:@"squeezePress"]) { + profile.squeezeStartShortcut = [payload objectForKey:@"cmdString"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.8 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [PencilHandler enterSqueezeEndShortcutIn:self]; + }); + } + if([shortcutIdentifier isEqualToString:@"squeezeRelease"]) { + profile.squeezeEndShortcut = [payload objectForKey:@"cmdString"]; + } + + [profileMan replaceSelectedProfileWith:profile overwriteDefault:true]; +} + /* - (void)autoHoverSwitchFlipped:(UISwitch* )sender{ if(sender.isOn && !settingsViewJustLoaded){ diff --git a/VoidLinkSwiftUI/VirtualKeyboardView.swift b/VoidLinkSwiftUI/VirtualKeyboardView.swift index fbf27a87..0d57e56e 100644 --- a/VoidLinkSwiftUI/VirtualKeyboardView.swift +++ b/VoidLinkSwiftUI/VirtualKeyboardView.swift @@ -59,9 +59,10 @@ enum KeyHighlightStyle { } @available(iOS 13.0, *) -enum VirtualKeyboardMode { +public enum VirtualKeyboardMode { case picker case typing + case shortcutPicker } @available(iOS 13.0, *) @@ -179,12 +180,12 @@ struct VirtualKeyboardView: View { self.selectionSyncToken = selectionSyncToken self.externalDeselectionCommand = externalDeselectionCommand self.externalDeselectionToken = externalDeselectionToken - _showsMouseWidgets = SwiftUI.State(initialValue: mode == .picker) + _showsMouseWidgets = SwiftUI.State(initialValue: mode != .typing) } var body: some View { Group { - if mode == .picker { + if usesPickerLayout { GeometryReader { proxy in let isPhone = UIDevice.current.userInterfaceIdiom == .phone let horizontalInset: CGFloat = isPhone ? 6 : 12 @@ -298,7 +299,7 @@ struct VirtualKeyboardView: View { height: keyHeight, keyboardType: keyboardType, mode: mode, - usesDirectionPadBaseStyle: mode == .picker, + usesDirectionPadBaseStyle: usesDirectionPadBaseStyle, isActive: (key.role == .fn && fnMode) || (key.role == .keyboardSwitch && keyboardType == .mac), @@ -325,7 +326,7 @@ struct VirtualKeyboardView: View { height: height ?? keyHeight, keyboardType: keyboardType, mode: mode, - usesDirectionPadBaseStyle: mode == .picker, + usesDirectionPadBaseStyle: usesDirectionPadBaseStyle, isActive: isActive, highlightStyle: highlightStyle(for: key), action: { @@ -345,9 +346,9 @@ struct VirtualKeyboardView: View { case .keyboardSwitch: keyboardType = keyboardType == .win ? .mac : .win case .normal: - if mode == .picker, let family = selectionFamily(for: key) { + if usesPickerSelectionBehavior, let family = selectionFamily(for: key) { handleDirectionKeyTap(key, family: family) - } else if mode == .picker || isTypingToggleKey(key) { + } else if usesPickerSelectionBehavior || isTypingToggleKey(key) { toggleRegularKeyHighlight(key) } } @@ -399,7 +400,9 @@ struct VirtualKeyboardView: View { if VirtualKeyboardView.selectedCmd == padCommand(for: family) { VirtualKeyboardView.selectedCmd = "" } - return + if mode != .shortcutPicker { + return + } } if directionSingleKeyLabels.contains(key.label) { @@ -413,6 +416,29 @@ struct VirtualKeyboardView: View { return } + if mode == .shortcutPicker { + let command = key.cmdString(for: keyboardType) ?? key.label + if canSelectCommand?(command) == false { + return + } + let option = KeyboardWidgetOption( + command: command, + description: singleKeyDescription(for: key, family: family), + highlightedLabels: [key.label], + family: family, + selectionState: .single(key.label) + ) + applySelection(option) + VirtualKeyboardView.tappedKeyLabels = [key.label] + VirtualKeyboardView.selectedCmd = command + VirtualKeyboardView.lastSelectionDisplayText = poolDisplayText(for: option) + VirtualKeyboardView.lastSelectionUsesMacLayout = keyboardType == .mac + VirtualKeyboardView.lastSelectionFromMouseWidgets = false + VirtualKeyboardView.lastSelectionUsesFnCommandDisplay = false + onCommandSelected?(command) + return + } + pendingSelectionFamily = family pendingKeyboardOptions = keyboardOptions(for: key, family: family) VirtualKeyboardView.tappedKeyLabels = [key.label] @@ -590,12 +616,12 @@ struct VirtualKeyboardView: View { } private func highlightStyle(for key: Key) -> KeyHighlightStyle { - if mode == .picker, let family = selectionFamily(for: key), padSelections.contains(family) { + if usesPadSelectionHighlight, let family = selectionFamily(for: key), padSelections.contains(family) { return .pad } if highlightedKeyIDs.contains(regularHighlightID(for: key)) - || (mode == .picker && directionSingleKeyLabels.contains(key.label)) { + || (usesPickerSelectionBehavior && directionSingleKeyLabels.contains(key.label)) { return isPadLikeControl(key) ? .pad : .single } @@ -770,7 +796,7 @@ struct VirtualKeyboardView: View { // MARK: - 小键盘 / 鼠标控件区 var numpadBlock: some View { VStack(spacing: spacing) { - if mode == .picker { + if usesPickerLayout { HStack(spacing: spacing) { KeyView( key: Key( @@ -813,7 +839,7 @@ struct VirtualKeyboardView: View { } } - if mode == .picker { + if usesPickerLayout { if showsMouseWidgets { mouseWidgetsBlock } else { @@ -980,65 +1006,93 @@ struct VirtualKeyboardView: View { ) } - HStack(spacing: spacing) { - keyboardKeyView( - Key( - label: SwiftLocalizationHelper.localizedString(forKey: "Wheel\nUp"), - width: unit, - winCmdString: "WHEELDOWN", - macCmdString: "WHEELDOWN", - winKeyCode: 0xFF, - macKeyCode: 0xFF + if mode == .shortcutPicker { + HStack(spacing: spacing) { + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "Wheel\nUp"), + width: unit, + winCmdString: "WHEELDOWN", + macCmdString: "WHEELDOWN", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ) ) - ) - keyboardKeyView( - Key( - label: SwiftLocalizationHelper.localizedString(forKey: "Wheel\nDown"), - width: unit, - winCmdString: "WHEELUP", - macCmdString: "WHEELUP", - winKeyCode: 0xFF, - macKeyCode: 0xFF + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "Wheel\nDown"), + width: unit, + winCmdString: "WHEELUP", + macCmdString: "WHEELUP", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ) + ) + } + } else { + HStack(spacing: spacing) { + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "Wheel\nUp"), + width: unit, + winCmdString: "WHEELDOWN", + macCmdString: "WHEELDOWN", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ) ) - ) - keyboardKeyView( - Key( - label: SwiftLocalizationHelper.localizedString(forKey: "WheelPad"), - width: unit * 2 + spacing, - winCmdString: "WHEEL", - macCmdString: "WHEEL", - winKeyCode: 0xFF, - macKeyCode: 0xFF + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "Wheel\nDown"), + width: unit, + winCmdString: "WHEELUP", + macCmdString: "WHEELUP", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ) ) - ) + + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "WheelPad"), + width: unit * 2 + spacing, + winCmdString: "WHEEL", + macCmdString: "WHEEL", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ) + ) + } } - HStack(spacing: spacing) { - keyboardKeyView( - Key( - label: SwiftLocalizationHelper.localizedString(forKey: "MousePad"), - width: unit * 2 + spacing, - winCmdString: "MOUSEPAD", - macCmdString: "MOUSEPAD", - winKeyCode: 0xFF, - macKeyCode: 0xFF - ), - height: keyHeight * 2 + spacing - ) + if mode != .shortcutPicker { + HStack(spacing: spacing) { + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "MousePad"), + width: unit * 2 + spacing, + winCmdString: "MOUSEPAD", + macCmdString: "MOUSEPAD", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ), + height: keyHeight * 2 + spacing + ) - keyboardKeyView( - Key( - label: SwiftLocalizationHelper.localizedString(forKey: "Trackball"), - width: unit * 2 + spacing, - winCmdString: "TRACKBALL", - macCmdString: "TRACKBALL", - winKeyCode: 0xFF, - macKeyCode: 0xFF - ), - height: keyHeight * 2 + spacing - ) + keyboardKeyView( + Key( + label: SwiftLocalizationHelper.localizedString(forKey: "Trackball"), + width: unit * 2 + spacing, + winCmdString: "TRACKBALL", + macCmdString: "TRACKBALL", + winKeyCode: 0xFF, + macKeyCode: 0xFF + ), + height: keyHeight * 2 + spacing + ) + } } } } @@ -1210,7 +1264,7 @@ struct VirtualKeyboardView: View { } private var mouseWidgetKeys: [Key] { - [ + var keys = [ Key(label: SwiftLocalizationHelper.localizedString(forKey: "Left Button"), width: unit * 2 + spacing, winCmdString: "MLEFT", macCmdString: "MLEFT", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: SwiftLocalizationHelper.localizedString(forKey: "Right Button"), width: unit * 2 + spacing, winCmdString: "MRIGHT", macCmdString: "MRIGHT", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: SwiftLocalizationHelper.localizedString(forKey: "Middle Button"), width: unit * 2 + spacing, winCmdString: "MMIDDLE", macCmdString: "MMIDDLE", winKeyCode: 0xFF, macKeyCode: 0xFF), @@ -1222,6 +1276,29 @@ struct VirtualKeyboardView: View { Key(label: SwiftLocalizationHelper.localizedString(forKey: "MousePad"), width: unit * 2 + spacing, winCmdString: "MOUSEPAD", macCmdString: "MOUSEPAD", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: SwiftLocalizationHelper.localizedString(forKey: "Trackball"), width: unit * 2 + spacing, winCmdString: "TRACKBALL", macCmdString: "TRACKBALL", winKeyCode: 0xFF, macKeyCode: 0xFF) ] + if mode == .shortcutPicker { + keys.removeAll { key in + let command = key.cmdString(for: keyboardType) ?? "" + return command == "WHEEL" || command == "MOUSEPAD" || command == "TRACKBALL" + } + } + return keys + } + + private var usesPickerLayout: Bool { + mode != .typing + } + + private var usesPickerSelectionBehavior: Bool { + mode == .picker || mode == .shortcutPicker + } + + private var usesPadSelectionHighlight: Bool { + mode == .picker + } + + private var usesDirectionPadBaseStyle: Bool { + mode == .picker } } @@ -1528,6 +1605,10 @@ struct VirtualKeyboardView_Previews: PreviewProvider { VirtualKeyboardView(mode: .picker) .frame(height: 320) .previewDisplayName("Picker") + + VirtualKeyboardView(mode: .shortcutPicker) + .frame(height: 320) + .previewDisplayName("Shortcut Picker") } } } diff --git a/VoidLinkSwiftUI/WidgetPickerView.swift b/VoidLinkSwiftUI/WidgetPickerView.swift index 99acb801..caf001b8 100644 --- a/VoidLinkSwiftUI/WidgetPickerView.swift +++ b/VoidLinkSwiftUI/WidgetPickerView.swift @@ -283,6 +283,9 @@ struct WidgetPickerView: View { private let initialShape: String? private let availableTabs: [WidgetPickerTab] private let preferredInitialTab: WidgetPickerTab? + private let keyboardPickerMode: VirtualKeyboardMode + private let shortcutPickerTipText: String? + private let shortcutIdentifier: String? var onWidgetCreated: (([String: String]) -> Void)? = nil var onCloseRequested: (() -> Void)? = nil @@ -291,7 +294,7 @@ struct WidgetPickerView: View { @SwiftUI.State private var selectedCmds: [String] = [] @SwiftUI.State private var poolItems: [WidgetPoolItem] = [] @SwiftUI.State private var resetToken: Int = 0 - @SwiftUI.State private var tipMessage: String = SwiftLocalizationHelper.localizedString(forKey: "Tap any control to add it") + @SwiftUI.State private var tipMessage: String @SwiftUI.State private var tipMessageType: PoolTipMessageType = .normal @SwiftUI.State private var keyboardDeselectionCommand: String = "" @SwiftUI.State private var keyboardDeselectionToken: Int = 0 @@ -316,6 +319,9 @@ struct WidgetPickerView: View { initialShape: String? = nil, availableTabs: [WidgetPickerTab] = WidgetPickerTab.allCases, preferredInitialTab: WidgetPickerTab? = nil, + keyboardPickerMode: VirtualKeyboardMode = .picker, + shortcutPickerTipText: String? = nil, + shortcutIdentififier: String? = nil, onWidgetCreated: (([String: String]) -> Void)? = nil, onCloseRequested: (() -> Void)? = nil ) { @@ -327,9 +333,17 @@ struct WidgetPickerView: View { self.initialShape = initialShape self.availableTabs = normalizedTabs self.preferredInitialTab = resolvedInitialTab + self.keyboardPickerMode = keyboardPickerMode + self.shortcutPickerTipText = shortcutPickerTipText + self.shortcutIdentifier = shortcutIdentififier self.onWidgetCreated = onWidgetCreated self.onCloseRequested = onCloseRequested _selectedTab = SwiftUI.State(initialValue: resolvedInitialTab ?? (normalizedTabs.contains(.keyboard) ? .keyboard : normalizedTabs[0])) + _tipMessage = SwiftUI.State( + initialValue: (keyboardPickerMode == .shortcutPicker && !(shortcutPickerTipText ?? "").isEmpty) + ? (shortcutPickerTipText ?? "") + : SwiftLocalizationHelper.localizedString(forKey: "Tap any control to add it") + ) } private let functionalButtonOptions: [FunctionalButtonOption] = [ @@ -487,7 +501,10 @@ struct WidgetPickerView: View { ] private var defaultTipMessage: String { - SwiftLocalizationHelper.localizedString(forKey: "Tap any control to add it") + if isShortcutPickerMode, let shortcutPickerTipText, !shortcutPickerTipText.isEmpty { + return shortcutPickerTipText + } + return SwiftLocalizationHelper.localizedString(forKey: "Tap any control to add it") } private var tipMessageColor: Color { @@ -500,6 +517,11 @@ struct WidgetPickerView: View { } private func setTipMessage(_ message: String, type: PoolTipMessageType = .normal) { + if isShortcutPickerMode, let shortcutPickerTipText, !shortcutPickerTipText.isEmpty { + tipMessage = shortcutPickerTipText + tipMessageType = .normal + return + } tipMessage = message tipMessageType = type } @@ -678,7 +700,7 @@ struct WidgetPickerView: View { .padding(.vertical, pickerInsetGamepadVertical) case .keyboard: VirtualKeyboardView( - mode: .picker, + mode: keyboardPickerMode, canSelectCommand: { canSelectCommand($0, source: .keyboard) }, isCommandSelected: { selectedCmds.contains($0) }, onCommandSelected: { appendCommand($0, source: .keyboard) }, @@ -852,44 +874,48 @@ struct WidgetPickerView: View { .frame(maxWidth: poolGridWidth ?? .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center) + if keyboardPickerMode != .shortcutPicker { PoolActionChip( - title: SwiftLocalizationHelper.localizedString(forKey: "Gyro switch button"), - isPrimary: false, - height: poolChipHeight, - fontSize: poolChipFontSize, - cornerRadius: metrics.chipCornerRadius - ) { - handleGyroButtonTap() - } - .overlay( - Group { - if selectedGyroCommand != nil { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .stroke(Color.orange.opacity(0.75), lineWidth: 1.4) - .background( - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill( - LinearGradient( - gradient: Gradient(colors: [ - Color(red: 1.00, green: 0.96, blue: 0.74).opacity(0.34), - Color(red: 0.95, green: 0.78, blue: 0.32).opacity(0.22) - ]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - ) - .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) - .allowsHitTesting(false) - } + title: SwiftLocalizationHelper.localizedString(forKey: "Gyro switch button"), + isPrimary: false, + height: poolChipHeight, + fontSize: poolChipFontSize, + cornerRadius: metrics.chipCornerRadius + ) { + handleGyroButtonTap() } - ) + .overlay( + Group { + if selectedGyroCommand != nil { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke(Color.orange.opacity(0.75), lineWidth: 1.4) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill( + LinearGradient( + gradient: Gradient(colors: [ + Color(red: 1.00, green: 0.96, blue: 0.74).opacity(0.34), + Color(red: 0.95, green: 0.78, blue: 0.32).opacity(0.22) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .allowsHitTesting(false) + } + } + ) + } HStack(spacing: metrics.isPhone ? 8 : 12) { PoolActionChip( - title: isEditMode - ? WidgetPickerSubmissionAction.create.poolActionTitle - : SwiftLocalizationHelper.localizedString(forKey: "Create widget"), + title: isShortcutPickerMode + ? SwiftLocalizationHelper.localizedString(forKey: "Create shortcut") + : (isEditMode + ? WidgetPickerSubmissionAction.create.poolActionTitle + : SwiftLocalizationHelper.localizedString(forKey: "Create widget")), isPrimary: true, height: poolChipHeight, fontSize: poolChipFontSize, @@ -1662,7 +1688,15 @@ struct WidgetPickerView: View { return "{\n \"cmdString\": \"\(payload["cmdString"] ?? "")\",\n \"buttonLabel\": \"\(payload["buttonLabel"] ?? "")\",\n \"shape\": \"\(payload["shape"] ?? "")\"\n}" } + private var isShortcutPickerMode: Bool { + keyboardPickerMode == .shortcutPicker + } + private var shouldBypassCreateWidgetSheet: Bool { + if isShortcutPickerMode { + return true + } + guard !poolItems.isEmpty else { return false } let hasVisibleConfigurationSection = @@ -1724,7 +1758,7 @@ struct WidgetPickerView: View { } private func presentCreateWidgetSheet(for submissionAction: WidgetPickerSubmissionAction) { - guard !poolItems.isEmpty else { + guard isShortcutPickerMode || !poolItems.isEmpty else { setTipMessage(SwiftLocalizationHelper.localizedString(forKey: "Select at least one command before creating a widget"), type: .error) return } @@ -1740,7 +1774,7 @@ struct WidgetPickerView: View { } private func submitWidgetPayload() { - let payload = makeWidgetPayload() + var payload = makeWidgetPayload() lastCreatedWidgetPayload = payload setTipMessage(SwiftLocalizationHelper.localizedString(forKey: "Widget config generated")) onWidgetCreated?(payload) @@ -1753,11 +1787,16 @@ struct WidgetPickerView: View { "cmdString": buildCmdString(), "buttonLabel": targetWidgetKind == .button ? widgetButtonLabel : "", "shape": targetWidgetKind == .button ? widgetShape.rawValue : "", - "pickerAction": pendingSubmissionAction.payloadValue + "pickerAction": pendingSubmissionAction.payloadValue, + "shortcutIdentifier": shortcutIdentifier ?? "" ] } private func buildCmdString() -> String { + if isShortcutPickerMode { + return selectedCmds.joined(separator: "+") + } + let joiner = effectiveWidgetComboMode == .shortcut && showsComboModeControl ? "+" : "-" var cmdString = selectedCmds.joined(separator: joiner) diff --git a/VoidLinkSwiftUI/WidgetPickerViewController.swift b/VoidLinkSwiftUI/WidgetPickerViewController.swift index b3618cb6..659c938a 100644 --- a/VoidLinkSwiftUI/WidgetPickerViewController.swift +++ b/VoidLinkSwiftUI/WidgetPickerViewController.swift @@ -17,6 +17,9 @@ public final class WidgetPickerViewController: UIViewController { public var initialShape: String? public var tabIdentifiers: [String] = [] public var initialTabIdentifier: String? + public var keyboardPickerMode: VirtualKeyboardMode = .picker + public var shortcutPickerTipText: String? + @objc public var shortcutIdentifier: String? private var hostingViewController: UIHostingController? @@ -32,6 +35,9 @@ public final class WidgetPickerViewController: UIViewController { initialShape: initialShape, availableTabs: resolvedTabs(), preferredInitialTab: resolvedInitialTab(), + keyboardPickerMode: keyboardPickerMode, + shortcutPickerTipText: shortcutPickerTipText, + shortcutIdentififier: shortcutIdentifier, onWidgetCreated: { [weak self] payload in guard let self else { return } self.delegate?.widgetPickerViewController(self, didCreateWidget: payload as NSDictionary)