Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Input Source Pro/Models/PreferencesVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Input Source Pro/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
3 changes: 3 additions & 0 deletions Input Source Pro/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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" = "有効にすると、キーを押した瞬間にショートカットがトリガーされます。無効にすると、キーを離したときにトリガーされます。";
3 changes: 3 additions & 0 deletions Input Source Pro/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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" = "활성화하면 키를 누르는 즉시 단축키가 트리거됩니다. 비활성화하면 키를 뗄 때 단축키가 트리거됩니다.";
3 changes: 3 additions & 0 deletions Input Source Pro/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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" = "启用时,按下按键立即触发快捷键。禁用时,松开按键时触发快捷键。";
3 changes: 3 additions & 0 deletions Input Source Pro/Resources/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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" = "啟用時,按下按鍵立即觸發快捷鍵。停用時,放開按鍵時觸發快捷鍵。";
32 changes: 31 additions & 1 deletion Input Source Pro/UI/Screens/KeyboardsSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,44 @@ struct KeyboardsSettingsView: View {
if hasSingleModifierShortcuts && (needsAccessibilityPermission || needsInputMonitoringPermission) {
permissionWarningSection
}


shortcutOptionsSection
normalSection
groupSection
AddSwitchingGroupButton(onSelect: preferencesVM.addHotKeyGroup)
}
.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
Expand Down
49 changes: 38 additions & 11 deletions Input Source Pro/Utilities/ShortcutTrigger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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 }
Expand All @@ -507,7 +519,9 @@ final class ShortcutTriggerManager {
return
}

triggerCompletedCombos(at: event.timestamp)
if !triggerOnKeyDown {
triggerCompletedCombos(at: event.timestamp)
}
comboInvalidated.removeAll()
comboCompleted.removeAll()
comboPressTimestamps.removeAll()
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
}
}

Expand All @@ -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
}
}
}
Expand Down