From 789c6f3fbf02aa1972b62417119655a49dc0feef Mon Sep 17 00:00:00 2001 From: Leslie Leung Date: Tue, 5 May 2026 00:38:32 +0800 Subject: [PATCH] Fix shortcut recording defaults --- .../app/src-tauri/src/shortcut_binding.rs | 62 +++++++++++++------ .../app/src/components/ShortcutRecorder.tsx | 26 +++++++- openless-all/app/src/lib/hotkey.ts | 20 ++++-- openless-all/app/src/lib/ipc.ts | 9 +-- openless-all/app/src/pages/SelectionAsk.tsx | 10 +-- openless-all/app/src/pages/Settings.tsx | 9 +-- 6 files changed, 94 insertions(+), 42 deletions(-) diff --git a/openless-all/app/src-tauri/src/shortcut_binding.rs b/openless-all/app/src-tauri/src/shortcut_binding.rs index 8256796d..731f8532 100644 --- a/openless-all/app/src-tauri/src/shortcut_binding.rs +++ b/openless-all/app/src-tauri/src/shortcut_binding.rs @@ -166,27 +166,27 @@ fn char_to_code(ch: char) -> Option { 'X' => Code::KeyX, 'Y' => Code::KeyY, 'Z' => Code::KeyZ, - '0' => Code::Digit0, - '1' => Code::Digit1, - '2' => Code::Digit2, - '3' => Code::Digit3, - '4' => Code::Digit4, - '5' => Code::Digit5, - '6' => Code::Digit6, - '7' => Code::Digit7, - '8' => Code::Digit8, - '9' => Code::Digit9, + '0' | ')' => Code::Digit0, + '1' | '!' => Code::Digit1, + '2' | '@' => Code::Digit2, + '3' | '#' => Code::Digit3, + '4' | '$' => Code::Digit4, + '5' | '%' => Code::Digit5, + '6' | '^' => Code::Digit6, + '7' | '&' => Code::Digit7, + '8' | '*' => Code::Digit8, + '9' | '(' => Code::Digit9, ';' | ':' => Code::Semicolon, - ',' => Code::Comma, - '.' => Code::Period, - '/' => Code::Slash, - '\\' => Code::Backslash, - '[' => Code::BracketLeft, - ']' => Code::BracketRight, - '\'' => Code::Quote, - '`' => Code::Backquote, - '-' => Code::Minus, - '=' => Code::Equal, + ',' | '<' => Code::Comma, + '.' | '>' => Code::Period, + '/' | '?' => Code::Slash, + '\\' | '|' => Code::Backslash, + '[' | '{' => Code::BracketLeft, + ']' | '}' => Code::BracketRight, + '\'' | '"' => Code::Quote, + '`' | '~' => Code::Backquote, + '-' | '_' => Code::Minus, + '=' | '+' => Code::Equal, ' ' => Code::Space, _ => return None, }; @@ -228,4 +228,26 @@ mod tests { Some(HotkeyTrigger::RightControl) ); } + + #[test] + fn accepts_shifted_printable_aliases() { + let cases = [ + ("?", Code::Slash), + ("!", Code::Digit1), + (":", Code::Semicolon), + ("+", Code::Equal), + ("_", Code::Minus), + ("{", Code::BracketLeft), + ("|", Code::Backslash), + ]; + for (primary, expected) in cases { + let binding = ShortcutBinding { + primary: primary.into(), + modifiers: vec!["shift".into()], + }; + let parsed = parse_global_hotkey(&binding).expect("shifted printable parses"); + assert_eq!(parsed.key, expected); + assert!(parsed.mods.contains(Modifiers::SHIFT)); + } + } } diff --git a/openless-all/app/src/components/ShortcutRecorder.tsx b/openless-all/app/src/components/ShortcutRecorder.tsx index 0e4033c9..e14da7c5 100644 --- a/openless-all/app/src/components/ShortcutRecorder.tsx +++ b/openless-all/app/src/components/ShortcutRecorder.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState, type CSSProperties, type KeyboardEvent } from 'react'; import { useTranslation } from 'react-i18next'; -import { formatComboLabel } from '../lib/hotkey'; +import { currentPlatform, formatComboLabel } from '../lib/hotkey'; import { setShortcutRecordingActive, validateShortcutBinding } from '../lib/ipc'; import type { ShortcutBinding } from '../lib/types'; @@ -154,7 +154,7 @@ export function ShortcutRecorder({ function modifiersFromKeyboardEvent(e: KeyboardEvent): string[] { const modifiers: string[] = []; - if (e.metaKey && e.key !== 'Meta') modifiers.push('cmd'); + if (e.metaKey && e.key !== 'Meta') modifiers.push(currentPlatform().isMac ? 'cmd' : 'super'); if (e.ctrlKey && e.key !== 'Control') modifiers.push('ctrl'); if (e.altKey && e.key !== 'Alt') modifiers.push('alt'); if (e.shiftKey && e.key !== 'Shift') modifiers.push('shift'); @@ -176,6 +176,8 @@ function modifierPrimaryFromCode(code: string, key: string): string { } function primaryFromKeyboardEvent(e: KeyboardEvent): string { + const printable = primaryFromPrintableCode(e.code); + if (printable) return printable; if (e.key.length === 1) return e.key; const codeToName: Record = { Space: 'Space', @@ -195,3 +197,23 @@ function primaryFromKeyboardEvent(e: KeyboardEvent): string { if (/^F\d{1,2}$/.test(e.key)) return e.key; return codeToName[e.code] || e.key; } + +function primaryFromPrintableCode(code: string): string { + if (/^Key[A-Z]$/.test(code)) return code.slice(3); + if (/^Digit[0-9]$/.test(code)) return code.slice(5); + const codeToPrimary: Record = { + Backquote: '`', + Minus: '-', + Equal: '=', + BracketLeft: '[', + BracketRight: ']', + Backslash: '\\', + Semicolon: ';', + Quote: "'", + Comma: ',', + Period: '.', + Slash: '/', + IntlBackslash: '\\', + }; + return codeToPrimary[code] || ''; +} diff --git a/openless-all/app/src/lib/hotkey.ts b/openless-all/app/src/lib/hotkey.ts index 94533fb9..048febf2 100644 --- a/openless-all/app/src/lib/hotkey.ts +++ b/openless-all/app/src/lib/hotkey.ts @@ -1,6 +1,17 @@ import i18n from '../i18n'; import type { ComboBinding, HotkeyBinding, HotkeyTrigger, QaHotkeyBinding, ShortcutBinding } from './types'; +export function defaultQaShortcut(): ShortcutBinding { + return { + primary: ';', + modifiers: defaultAppShortcutModifiers(), + }; +} + +export function defaultAppShortcutModifiers(): string[] { + return currentPlatform().isMac ? ['cmd', 'shift'] : ['ctrl', 'shift']; +} + export function getHotkeyTriggerLabel(trigger: HotkeyTrigger | null | undefined): string { if (!trigger) return i18n.t('hotkey.fallback'); if (trigger === 'custom') return i18n.t('hotkey.triggers.custom'); @@ -72,9 +83,10 @@ export function formatComboLabel(binding: ComboBinding | QaHotkeyBinding | Short return parts.join(platform.isMac ? '' : '+'); } -function currentPlatform(): { isMac: boolean; isWindows: boolean } { - const platform = navigator.platform || ''; - const userAgent = navigator.userAgent || ''; +export function currentPlatform(): { isMac: boolean; isWindows: boolean } { + const nav = typeof navigator === 'undefined' ? null : navigator; + const platform = nav?.platform || ''; + const userAgent = nav?.userAgent || ''; return { isMac: platform.includes('Mac') || userAgent.includes('Mac'), isWindows: platform.includes('Win') || userAgent.includes('Windows'), @@ -110,7 +122,7 @@ function formatPrimary(primary: string): string { return trimmed.toUpperCase(); } // 常见命名键的 macOS 符号 - const isMac = navigator.platform.includes('Mac') || navigator.userAgent.includes('Mac'); + const isMac = currentPlatform().isMac; if (isMac) { switch (trimmed.toLowerCase()) { case 'space': return '\u2423'; diff --git a/openless-all/app/src/lib/ipc.ts b/openless-all/app/src/lib/ipc.ts index 15ce1000..769e96fb 100644 --- a/openless-all/app/src/lib/ipc.ts +++ b/openless-all/app/src/lib/ipc.ts @@ -19,6 +19,7 @@ import type { VocabPresetStore, } from './types'; import { OL_DATA } from './mockData'; +import { defaultAppShortcutModifiers, defaultQaShortcut, formatComboLabel } from './hotkey'; declare global { interface Window { @@ -54,12 +55,12 @@ const mockSettings: UserPreferences = { allowNonTsfInsertionFallback: true, workingLanguages: ['简体中文'], translationTargetLanguage: '', - qaHotkey: { primary: ';', modifiers: ['cmd', 'shift'] }, + qaHotkey: defaultQaShortcut(), qaSaveHistory: false, customComboHotkey: null, translationHotkey: { primary: 'Shift', modifiers: [] }, - switchStyleHotkey: { primary: 'S', modifiers: ['cmd', 'shift'] }, - openAppHotkey: { primary: 'O', modifiers: ['cmd', 'shift'] }, + switchStyleHotkey: { primary: 'S', modifiers: defaultAppShortcutModifiers() }, + openAppHotkey: { primary: 'O', modifiers: defaultAppShortcutModifiers() }, }; const mockHotkeyCapability: HotkeyCapability = { @@ -293,7 +294,7 @@ export function restartApp(): Promise { // 详见 issue #118。后端会发 `qa:state` / `qa:dismiss` 事件;前端通过下面四个 // 命令查询与控制 QA 浮窗。 export function getQaHotkeyLabel(): Promise { - return invokeOrMock('get_qa_hotkey_label', undefined, () => 'Cmd+Shift+;'); + return invokeOrMock('get_qa_hotkey_label', undefined, () => formatComboLabel(defaultQaShortcut())); } export function setQaHotkey(binding: QaHotkeyBinding | null): Promise { diff --git a/openless-all/app/src/pages/SelectionAsk.tsx b/openless-all/app/src/pages/SelectionAsk.tsx index 447096d9..1644e9ab 100644 --- a/openless-all/app/src/pages/SelectionAsk.tsx +++ b/openless-all/app/src/pages/SelectionAsk.tsx @@ -9,13 +9,14 @@ import { useTranslation } from 'react-i18next'; import { Card, PageHeader } from './_atoms'; import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { setQaHotkey } from '../lib/ipc'; -import { formatComboLabel } from '../lib/hotkey'; +import { defaultQaShortcut, formatComboLabel } from '../lib/hotkey'; import { ShortcutRecorder } from '../components/ShortcutRecorder'; export function SelectionAsk() { const { t } = useTranslation(); const { prefs, updatePrefs: savePrefs } = useHotkeySettings(); - const defaultHotkeyLabel = 'Cmd+Shift+;'; + const defaultQaHotkey = defaultQaShortcut(); + const defaultHotkeyLabel = formatComboLabel(defaultQaHotkey); const recordHotkeyLabel = prefs ? formatComboLabel(prefs.dictationHotkey) : '快捷键'; if (!prefs) { @@ -79,8 +80,9 @@ export function SelectionAsk() {