Skip to content

快捷键的登记簿

背景:快捷键分散在各模块中,没有统一注册和冲突检测。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+AShift+Ctrl+A 应该是同一个键。

Sisyphus 写了一个 _buildKeyString 函数,按固定顺序组合修饰键:

Ctrl → Alt → Shift → 普通键

这样无论事件里修饰键的顺序是什么,输出都是同一个字符串。

修饰键用大写,普通键用原始名称。Ctrl+SpaceAlt+TabF5Ctrl+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 只被一个模块占用。

冲突从静默变成了显式。开发者在控制台能看到,用户不会莫名其妙发现快捷键"不工作了"。

键盘是联邦的另一种语言。登记簿是它的字典。