Fix shortcut recording defaults

This commit is contained in:
Leslie Leung committed 2026-05-05 00:38:32 +08:00
1 parent 913039e435
commit 789c6f3fbf
6 files changed
+94 -42

No files matched your search

@@ -166,27 +166,27 @@ fn char_to_code(ch: char) -> Option<Code> {
'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));
}
}
}
@@ -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<string, string> = {
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<string, string> = {
Backquote: '`',
Minus: '-',
Equal: '=',
BracketLeft: '[',
BracketRight: ']',
Backslash: '\\',
Semicolon: ';',
Quote: "'",
Comma: ',',
Period: '.',
Slash: '/',
IntlBackslash: '\\',
};
return codeToPrimary[code] || '';
}
+16 -4
View File
@@ -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';
+5 -4
View File
@@ -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<void> {
// 详见 issue #118。后端会发 `qa:state` / `qa:dismiss` 事件;前端通过下面四个
// 命令查询与控制 QA 浮窗。
export function getQaHotkeyLabel(): Promise<string> {
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<void> {
+6 -4
View File
@@ -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() {
</div>
<button
onClick={async () => {
await setQaHotkey(enabled ? null : { primary: ';', modifiers: ['cmd', 'shift'] });
await savePrefs({ ...prefs, qaHotkey: enabled ? null : { primary: ';', modifiers: ['cmd', 'shift'] } });
const nextHotkey = enabled ? null : defaultQaHotkey;
await setQaHotkey(nextHotkey);
await savePrefs({ ...prefs, qaHotkey: nextHotkey });
}}
style={{ fontSize: 12, padding: '5px 14px', background: enabled ? 'rgba(0,0,0,0.06)' : 'var(--ol-blue)', color: enabled ? 'var(--ol-ink-2)' : '#fff', border: 0, borderRadius: 6, fontFamily: 'inherit', fontWeight: 500, cursor: 'default', marginBottom: 10 }}
>
+1 -8
View File
@@ -9,6 +9,7 @@ import { ShortcutRecorder } from '../components/ShortcutRecorder';
import { isDialogStatus, UpdateDialog, useAutoUpdate } from '../components/AutoUpdate';
import { APP_VERSION_LABEL } from '../lib/appVersion';
import { isHotkeyModeMigrationNoticeActive } from '../lib/hotkeyMigration';
import { defaultQaShortcut } from '../lib/hotkey';
import {
checkAccessibilityPermission,
checkMicrophonePermission,
@@ -36,7 +37,6 @@ import type {
HotkeyMode,
HotkeyStatus,
PermissionStatus,
ShortcutBinding,
WindowsImeStatus,
} from '../lib/types';
import { useHotkeySettings } from '../state/HotkeySettingsContext';
@@ -975,13 +975,6 @@ function ShortcutsSection() {
);
}
function defaultQaShortcut(): ShortcutBinding {
const isMac = navigator.platform.includes('Mac') || navigator.userAgent.includes('Mac');
return isMac
? { primary: ';', modifiers: ['cmd', 'shift'] }
: { primary: ';', modifiers: ['ctrl', 'shift'] };
}
function PermissionsSection() {
const { t } = useTranslation();
const [accessibility, setAccessibility] = useState<PermissionStatus | 'loading'>('loading');