feat: 快捷键录入重绑(FR-K3,keymap:load/save IPC+config.json,SettingsPanel 录入 UI+冲突检测,use-hotkeys 读 store 实时生效,重开恢复)
This commit is contained in:
@@ -1,33 +1,48 @@
|
||||
import { create } from 'zustand';
|
||||
import { IPC } from '@hunter/shared';
|
||||
|
||||
// KeymapService — 快捷键绑定管理 + 冲突检测(FR-K3/K4/K5)
|
||||
// 注:? 帮助键保留硬编码(非 ctrl 系,不便录入);此处管理 4 个 ctrl 系全局绑定
|
||||
export type Scope = 'global' | 'app' | 'editor';
|
||||
export interface Binding { id: string; scope: Scope; combo: string; defaultCombo: string; }
|
||||
export interface Binding { id: string; scope: Scope; label: string; combo: string; defaultCombo: string; }
|
||||
|
||||
export class KeymapService {
|
||||
private bindings = new Map<string, Binding>();
|
||||
const DEFAULTS: Binding[] = [
|
||||
{ id: 'newDoc', scope: 'global', label: '新建文档', combo: 'ctrl+n', defaultCombo: 'ctrl+n' },
|
||||
{ id: 'save', scope: 'global', label: '保存', combo: 'ctrl+s', defaultCombo: 'ctrl+s' },
|
||||
{ id: 'focusSearch', scope: 'global', label: '全库搜索', combo: 'ctrl+shift+f', defaultCombo: 'ctrl+shift+f' },
|
||||
{ id: 'toggleOutline', scope: 'global', label: '大纲面板', combo: 'ctrl+shift+l', defaultCombo: 'ctrl+shift+l' },
|
||||
];
|
||||
|
||||
register(b: Binding) { this.bindings.set(b.id, b); }
|
||||
setCombo(id: string, combo: string) {
|
||||
const b = this.bindings.get(id); if (b) this.bindings.set(id, { ...b, combo });
|
||||
}
|
||||
resetAll() { for (const [id, b] of this.bindings) this.bindings.set(id, { ...b, combo: b.defaultCombo }); }
|
||||
getAll(): Binding[] { return [...this.bindings.values()]; }
|
||||
// 冲突检测:同一 combo 绑定到多个 action(不同 id)即冲突
|
||||
conflicts(): Record<string, string[]> {
|
||||
interface KeymapState {
|
||||
bindings: Binding[];
|
||||
setCombo: (id: string, combo: string) => void;
|
||||
resetAll: () => void;
|
||||
load: () => Promise<void>;
|
||||
save: () => Promise<void>;
|
||||
conflicts: () => Record<string, string[]>;
|
||||
}
|
||||
|
||||
export const useKeymap = create<KeymapState>((set, get) => ({
|
||||
bindings: DEFAULTS.map(b => ({ ...b })),
|
||||
setCombo: (id, combo) => set(s => ({ bindings: s.bindings.map(b => b.id === id ? { ...b, combo } : b) })),
|
||||
resetAll: () => set({ bindings: DEFAULTS.map(b => ({ ...b })) }),
|
||||
load: async () => {
|
||||
try {
|
||||
const saved = (await (globalThis as any).dsh.invoke(IPC.keymapLoad)) as { id: string; combo: string }[] | null;
|
||||
if (saved) set(s => ({ bindings: s.bindings.map(b => ({ ...b, combo: saved.find(x => x.id === b.id)?.combo ?? b.combo })) }));
|
||||
} catch { /* 首次启动 */ }
|
||||
},
|
||||
save: async () => {
|
||||
const data = get().bindings.map(b => ({ id: b.id, combo: b.combo }));
|
||||
await (globalThis as any).dsh.invoke(IPC.keymapSave, data);
|
||||
},
|
||||
conflicts: () => {
|
||||
const byCombo = new Map<string, string[]>();
|
||||
for (const b of this.bindings.values()) {
|
||||
for (const b of get().bindings) {
|
||||
const arr = byCombo.get(b.combo) ?? []; arr.push(b.id); byCombo.set(b.combo, arr);
|
||||
}
|
||||
const out: Record<string, string[]> = {};
|
||||
for (const [combo, ids] of byCombo) if (ids.length > 1) out[combo] = ids;
|
||||
return out;
|
||||
}
|
||||
conflictCount(): number { return Object.keys(this.conflicts()).length; }
|
||||
// 作用域裁决:给定当前是否编辑器聚焦,返回某 combo 应触发哪个 binding(编辑器聚焦时 global 让位规则按需)
|
||||
resolve(combo: string, editorFocused: boolean): Binding | null {
|
||||
const matches = [...this.bindings.values()].filter(b => b.combo === combo);
|
||||
if (matches.length === 0) return null;
|
||||
// 优先 editor(编辑器聚焦时),再 app,再 global
|
||||
if (editorFocused) { const e = matches.find(m => m.scope === 'editor'); if (e) return e; }
|
||||
return matches.find(m => m.scope === 'global') ?? matches[0];
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user