diff --git a/Input Source Pro/Models/PreferencesVM.swift b/Input Source Pro/Models/PreferencesVM.swift index e58c7ab..8a54e7b 100644 --- a/Input Source Pro/Models/PreferencesVM.swift +++ b/Input Source Pro/Models/PreferencesVM.swift @@ -254,6 +254,7 @@ struct Preferences { static let singleModifierTriggerGroupMapping = "singleModifierTriggerGroupMapping" static let singleModifierInputSourceMapping = "singleModifierInputSourceMapping" static let singleModifierGroupMapping = "singleModifierGroupMapping" + static let isShortcutTriggerOnKeyDown = "isShortcutTriggerOnKeyDown" static let isAutoAppearanceMode = "isAutoAppearanceMode" static let appearanceMode = "appearanceMode" @@ -347,6 +348,9 @@ struct Preferences { @CodableUserDefault(Preferences.Key.singleModifierGroupMapping) var singleModifierGroupMapping = [String: ModifierCombo]() + @UserDefault(Preferences.Key.isShortcutTriggerOnKeyDown) + var isShortcutTriggerOnKeyDown = false + // MARK: - App Rules @UserDefault(Preferences.Key.systemWideDefaultKeyboardId) diff --git a/Input Source Pro/Resources/en.lproj/Localizable.strings b/Input Source Pro/Resources/en.lproj/Localizable.strings index 0844bec..9d57019 100644 --- a/Input Source Pro/Resources/en.lproj/Localizable.strings +++ b/Input Source Pro/Resources/en.lproj/Localizable.strings @@ -215,3 +215,6 @@ "Open Input Monitoring Settings" = "Open Input Monitoring Settings"; "Open Input Monitoring Settings, click the \"+\" button and add \"Input Source Pro\" to the list." = "Open Input Monitoring Settings, click the \"+\" button and add \"Input Source Pro\" to the list."; "Disabled until permissions are granted" = "Disabled until permissions are granted"; + +"Trigger on Key Press" = "Trigger on Key Press"; +"Trigger on Key Press Description" = "When enabled, shortcuts trigger immediately when keys are pressed. When disabled, shortcuts trigger when keys are released."; diff --git a/Input Source Pro/Resources/ja.lproj/Localizable.strings b/Input Source Pro/Resources/ja.lproj/Localizable.strings index e087346..b715340 100644 --- a/Input Source Pro/Resources/ja.lproj/Localizable.strings +++ b/Input Source Pro/Resources/ja.lproj/Localizable.strings @@ -215,3 +215,6 @@ "Open Input Monitoring Settings" = "入力監視設定を開く"; "Open Input Monitoring Settings, click the \"+\" button and add \"Input Source Pro\" to the list." = "入力監視設定を開き、「+」ボタンをクリックして一覧に「Input Source Pro」を追加してください。"; "Disabled until permissions are granted" = "権限が付与されるまで無効"; + +"Trigger on Key Press" = "キーを押したときにトリガー"; +"Trigger on Key Press Description" = "有効にすると、キーを押した瞬間にショートカットがトリガーされます。無効にすると、キーを離したときにトリガーされます。"; diff --git a/Input Source Pro/Resources/ko.lproj/Localizable.strings b/Input Source Pro/Resources/ko.lproj/Localizable.strings index 05c4bd5..791d5ec 100644 --- a/Input Source Pro/Resources/ko.lproj/Localizable.strings +++ b/Input Source Pro/Resources/ko.lproj/Localizable.strings @@ -215,3 +215,6 @@ "Open Input Monitoring Settings" = "입력 모니터링 설정 열기"; "Open Input Monitoring Settings, click the \"+\" button and add \"Input Source Pro\" to the list." = "입력 모니터링 설정을 열고 \"+\" 버튼을 클릭한 다음 목록에 \"Input Source Pro\"를 추가하세요."; "Disabled until permissions are granted" = "권한이 허용될 때까지 비활성화됨"; + +"Trigger on Key Press" = "키를 누를 때 트리거"; +"Trigger on Key Press Description" = "활성화하면 키를 누르는 즉시 단축키가 트리거됩니다. 비활성화하면 키를 뗄 때 단축키가 트리거됩니다."; diff --git a/Input Source Pro/Resources/zh-Hans.lproj/Localizable.strings b/Input Source Pro/Resources/zh-Hans.lproj/Localizable.strings index e61b52f..b3436b1 100644 --- a/Input Source Pro/Resources/zh-Hans.lproj/Localizable.strings +++ b/Input Source Pro/Resources/zh-Hans.lproj/Localizable.strings @@ -217,3 +217,6 @@ "Open Input Monitoring Settings" = "打开「输入监控」设置"; "Open Input Monitoring Settings, click the \"+\" button and add \"Input Source Pro\" to the list." = "打开「输入监控」设置,点击“+”按钮并将“Input Source Pro”添加到列表中。"; "Disabled until permissions are granted" = "权限授予前将被禁用"; + +"Trigger on Key Press" = "按下时触发"; +"Trigger on Key Press Description" = "启用时,按下按键立即触发快捷键。禁用时,松开按键时触发快捷键。"; diff --git a/Input Source Pro/Resources/zh-Hant.lproj/Localizable.strings b/Input Source Pro/Resources/zh-Hant.lproj/Localizable.strings index e1dfe3b..7808c69 100644 --- a/Input Source Pro/Resources/zh-Hant.lproj/Localizable.strings +++ b/Input Source Pro/Resources/zh-Hant.lproj/Localizable.strings @@ -217,3 +217,6 @@ "Open Input Monitoring Settings" = "打開「輸入監控」設定"; "Open Input Monitoring Settings, click the \"+\" button and add \"Input Source Pro\" to the list." = "打開「輸入監控」設定,點擊「+」按鈕並將「Input Source Pro」加入列表。"; "Disabled until permissions are granted" = "權限授予前將被停用"; + +"Trigger on Key Press" = "按下時觸發"; +"Trigger on Key Press Description" = "啟用時,按下按鍵立即觸發快捷鍵。停用時,放開按鍵時觸發快捷鍵。"; diff --git a/Input Source Pro/UI/Screens/KeyboardsSettingsView.swift b/Input Source Pro/UI/Screens/KeyboardsSettingsView.swift index be5aad4..e74ca5c 100644 --- a/Input Source Pro/UI/Screens/KeyboardsSettingsView.swift +++ b/Input Source Pro/UI/Screens/KeyboardsSettingsView.swift @@ -51,7 +51,8 @@ struct KeyboardsSettingsView: View { if hasSingleModifierShortcuts && (needsAccessibilityPermission || needsInputMonitoringPermission) { permissionWarningSection } - + + shortcutOptionsSection normalSection groupSection AddSwitchingGroupButton(onSelect: preferencesVM.addHotKeyGroup) @@ -59,6 +60,35 @@ struct KeyboardsSettingsView: View { .padding() } .background(NSColor.background1.color) + .toggleStyle(.switch) + } + + @ViewBuilder + var shortcutOptionsSection: some View { + let isShortcutTriggerOnKeyDown = Binding( + get: { preferencesVM.preferences.isShortcutTriggerOnKeyDown }, + set: { newValue in + preferencesVM.update { $0.isShortcutTriggerOnKeyDown = newValue } + indicatorVM.refreshShortcut() + } + ) + + SettingsSection(title: "") { + HStack(alignment: .firstTextBaseline) { + Toggle("", + isOn: isShortcutTriggerOnKeyDown + ) + VStack(alignment: .leading, spacing: 6) { + Text("Trigger on Key Press".i18n()) + Text(.init("Trigger on Key Press Description".i18n())) + .font(.system(size: 12)) + .opacity(0.8) + } + Spacer() + } + .padding() + } + .padding(.bottom) } @ViewBuilder diff --git a/Input Source Pro/Utilities/ShortcutTrigger.swift b/Input Source Pro/Utilities/ShortcutTrigger.swift index ec87172..e338783 100644 --- a/Input Source Pro/Utilities/ShortcutTrigger.swift +++ b/Input Source Pro/Utilities/ShortcutTrigger.swift @@ -340,9 +340,16 @@ final class ShortcutTriggerManager { } private func registerKeyboardShortcuts(_ bindings: [ShortcutBinding]) { + let triggerOnKeyDown = preferencesVM.preferences.isShortcutTriggerOnKeyDown for binding in bindings { - KeyboardShortcuts.onKeyUp(for: .init(binding.id)) { - binding.onTrigger() + if triggerOnKeyDown { + KeyboardShortcuts.onKeyDown(for: .init(binding.id)) { + binding.onTrigger() + } + } else { + KeyboardShortcuts.onKeyUp(for: .init(binding.id)) { + binding.onTrigger() + } } } } @@ -484,6 +491,7 @@ final class ShortcutTriggerManager { flags.remove(.capsLock) let isKeyDown = flags.contains(key.modifierFlag) + let triggerOnKeyDown = preferencesVM.preferences.isShortcutTriggerOnKeyDown if isKeyDown { lastKeyDownTimestamps[event.keyCode] = event.timestamp @@ -495,6 +503,10 @@ final class ShortcutTriggerManager { pressedModifiers[key] = event.timestamp } updateComboState(pressedKeys: Set(pressedModifiers.keys), timestamp: event.timestamp) + + if triggerOnKeyDown { + triggerCompletedCombos(at: event.timestamp, isKeyDown: true) + } } else { // Modifier key released guard pressedModifiers.removeValue(forKey: key) != nil else { return } @@ -507,7 +519,9 @@ final class ShortcutTriggerManager { return } - triggerCompletedCombos(at: event.timestamp) + if !triggerOnKeyDown { + triggerCompletedCombos(at: event.timestamp) + } comboInvalidated.removeAll() comboCompleted.removeAll() comboPressTimestamps.removeAll() @@ -552,15 +566,18 @@ final class ShortcutTriggerManager { } } - private func triggerCompletedCombos(at timestamp: TimeInterval) { + private func triggerCompletedCombos(at timestamp: TimeInterval, isKeyDown: Bool = false) { for combo in comboCompleted { guard !comboInvalidated.contains(combo) else { continue } guard let binding = currentBindingsByCombo[combo] else { continue } - let pressTimestamp = comboPressTimestamps[combo] ?? timestamp - let holdDuration = timestamp - pressTimestamp - if holdDuration > maxHoldDuration { - continue + // Only check hold duration on key up + if !isKeyDown { + let pressTimestamp = comboPressTimestamps[combo] ?? timestamp + let holdDuration = timestamp - pressTimestamp + if holdDuration > maxHoldDuration { + continue + } } if didPressOtherKeyRecently(before: timestamp, excluding: combo.keys) { @@ -574,12 +591,17 @@ final class ShortcutTriggerManager { ? .singlePress : binding.singleModifierTrigger - handleSingleModifierTrigger( + let didTrigger = handleSingleModifierTrigger( combo: combo, timestamp: timestamp, trigger: effectiveTrigger, action: binding.onTrigger ) + + // Invalidate the combo after triggering on key down to prevent double-triggering + if isKeyDown && didTrigger { + comboInvalidated.insert(combo) + } } } @@ -597,28 +619,33 @@ final class ShortcutTriggerManager { return timestamp - lastOtherKeyTimestamp <= otherKeyPressSuppressInterval } + /// Returns true if action was triggered + @discardableResult private func handleSingleModifierTrigger( combo: ModifierCombo, timestamp: TimeInterval, trigger: SingleModifierTrigger, action: () -> Void - ) { + ) -> Bool { guard let key = combo.singleKey else { action() - return + return true } switch trigger { case .singlePress: action() + return true case .doublePress: if let lastTap = modifierTapTimestamps[key], timestamp - lastTap <= doublePressInterval { modifierTapTimestamps.removeValue(forKey: key) action() + return true } else { modifierTapTimestamps[key] = timestamp + return false } } }