Appearance
快捷键的登记簿
背景:快捷键分散在各模块中,没有统一注册和冲突检测。shortcut-registry 提供集中管理。 过程:ShortcutRegistry 类 → 注册/注销 API → 冲突检测 → 全局键盘事件监听 → 快捷键设置 UI。
一、三个模块,同一个键
Sisyphus 打开 main.ts,找到 Ctrl+Space 的监听。它切换动作模式。
他又打开 scene.ts,找到另一个 Ctrl+Space 的监听。它切换相机模式。
再打开 outfit.ts,找到第三个 Ctrl+Space 的监听。它切换服装预设。
三个模块,同一个键。
Sisyphus 想了一下。浏览器的事件系统是这样的:后注册的监听覆盖先注册的。所以最后一个注册的 Ctrl+Space 生效,前两个被覆盖。
这是运行时才发现的。开发者在改 outfit 的快捷键时,不会去看 main.ts 和 scene.ts 里有没有用同一个键。
冲突是静默的。
二、登记簿
Sisyphus 在 src/core/shortcut-registry.ts 里新建了一个类:ShortcutRegistry。
它的结构是一个 Map,key 是标准化的快捷键字符串,value 是处理函数和来源模块名。
typescript
class ShortcutRegistry {
private shortcuts = new Map<string, { handler: () => void; source: string }>();
private conflicts = new Map<string, string[]>();
}注册时检查冲突:
typescript
register(key: string, handler: () => void, source: string): void {
if (this.shortcuts.has(key)) {
const existing = this.shortcuts.get(key)!;
this.conflicts.set(key, [existing.source, source]);
console.warn(`快捷键冲突: ${key} 被 ${existing.source} 和 ${source} 注册`);
}
this.shortcuts.set(key, { handler, source });
}先注册的生效,后注册的被忽略。但控制台会打印一条警告,告诉开发者有冲突。
冲突从运行时提前到了注册时。
三、标准化
快捷键字符串需要标准化。Ctrl+Shift+A 和 Shift+Ctrl+A 应该是同一个键。
Sisyphus 写了一个 _buildKeyString 函数,按固定顺序组合修饰键:
Ctrl → Alt → Shift → 普通键这样无论事件里修饰键的顺序是什么,输出都是同一个字符串。
修饰键用大写,普通键用原始名称。Ctrl+Space、Alt+Tab、F5、Ctrl+Shift+Enter。
四、注销
注销需要同时匹配 key 和 source。
typescript
unregister(key: string, source: string): void {
const existing = this.shortcuts.get(key);
if (existing?.source === source) {
this.shortcuts.delete(key);
}
}如果只有 key,不检查 source,可能会误删其他模块注册的同一个键。
注销的场景:模块卸载时清掉它注册的所有快捷键。
五、设置页
Sisyphus 在 settings-shortcuts.ts 里加了快捷键设置 UI。
列出所有已注册的快捷键,显示当前绑定的 key,允许用户修改。
用户不能删除快捷键——只能改 key。快捷键是功能的入口,删除等于隐藏功能。
修改绑定时需要再次检查冲突。如果用户把某个键改成了已经被占用的,ShortcutRegistry 会打印警告,但不会阻止。用户的选择是最高优先级。
尾声
登记簿建好后,Ctrl+Space 只被一个模块占用。
冲突从静默变成了显式。开发者在控制台能看到,用户不会莫名其妙发现快捷键"不工作了"。
键盘是联邦的另一种语言。登记簿是它的字典。