mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
refactor: migrate keybindings to DDD structure (#8369)
## Summary Migrate keybindings domain to `src/platform/keybindings/` following DDD principles. ## Changes - **What**: Consolidate keybinding-related code (types, store, service, defaults, reserved keys) into a single domain module with flat structure - Extracted `KeyComboImpl` and `KeybindingImpl` classes into separate files - Updated all consumers to import from new location - Colocated tests with source files - Updated stores/README.md and services/README.md to remove migrated entries ## Review Focus - Verify all import paths were updated correctly - Check that the flat structure is appropriate (vs nested core/data/ui layers) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8369-refactor-migrate-keybindings-to-DDD-structure-2f66d73d36508120b169dc737075fb45) by [Unito](https://www.unito.io) --------- Co-authored-by: Subagent 5 <subagent@example.com> Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -123,7 +123,6 @@ The following table lists ALL 46 store instances in the system as of 2025-09-01:
|
||||
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
|
||||
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
|
||||
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
|
||||
| keybindingStore.ts | useKeybindingStore | Manages keyboard shortcuts | Input |
|
||||
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
|
||||
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
|
||||
| modelStore.ts | useModelStore | Manages AI models information | Models |
|
||||
|
||||
@@ -2,11 +2,10 @@ import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
import { useKeybindingStore } from './keybindingStore'
|
||||
import type { KeybindingImpl } from './keybindingStore'
|
||||
|
||||
export interface ComfyCommand {
|
||||
id: string
|
||||
function: (metadata?: Record<string, unknown>) => void | Promise<void>
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
|
||||
describe('useKeybindingStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('should add and retrieve default keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
})
|
||||
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
|
||||
})
|
||||
|
||||
it('should add and retrieve user keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'B', alt: true }
|
||||
})
|
||||
|
||||
store.addUserKeybinding(keybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
|
||||
})
|
||||
|
||||
it('should get keybindings by command id', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'C', ctrl: true }
|
||||
})
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
expect(store.getKeybindingsByCommandId('test.command')).toEqual([
|
||||
keybinding
|
||||
])
|
||||
})
|
||||
|
||||
it('should override default keybindings with user keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command1',
|
||||
combo: { key: 'C', ctrl: true }
|
||||
})
|
||||
const userKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command2',
|
||||
combo: { key: 'C', ctrl: true }
|
||||
})
|
||||
|
||||
store.addDefaultKeybinding(defaultKeybinding)
|
||||
store.addUserKeybinding(userKeybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userKeybinding.combo)).toEqual(userKeybinding)
|
||||
})
|
||||
|
||||
it('Should allow binding to unsetted default keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command1',
|
||||
combo: { key: 'C', ctrl: true }
|
||||
})
|
||||
store.addDefaultKeybinding(defaultKeybinding)
|
||||
store.unsetKeybinding(defaultKeybinding)
|
||||
|
||||
const userKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command2',
|
||||
combo: { key: 'C', ctrl: true }
|
||||
})
|
||||
store.addUserKeybinding(userKeybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userKeybinding.combo)).toEqual(userKeybinding)
|
||||
})
|
||||
|
||||
it('should unset user keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'D', meta: true }
|
||||
})
|
||||
|
||||
store.addUserKeybinding(keybinding)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
|
||||
store.unsetKeybinding(keybinding)
|
||||
expect(store.keybindings).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should unset default keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'E', ctrl: true, alt: true }
|
||||
})
|
||||
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
|
||||
store.unsetKeybinding(keybinding)
|
||||
expect(store.keybindings).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should throw an error when adding duplicate default keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'F', shift: true }
|
||||
})
|
||||
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
expect(() => store.addDefaultKeybinding(keybinding)).toThrow()
|
||||
})
|
||||
|
||||
it('should allow adding duplicate user keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding1 = new KeybindingImpl({
|
||||
commandId: 'test.command1',
|
||||
combo: { key: 'G', ctrl: true }
|
||||
})
|
||||
const keybinding2 = new KeybindingImpl({
|
||||
commandId: 'test.command2',
|
||||
combo: { key: 'G', ctrl: true }
|
||||
})
|
||||
|
||||
store.addUserKeybinding(keybinding1)
|
||||
store.addUserKeybinding(keybinding2)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(keybinding2.combo)).toEqual(keybinding2)
|
||||
})
|
||||
|
||||
it('should not throw an error when unsetting non-existent keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'H', alt: true, shift: true }
|
||||
})
|
||||
|
||||
expect(() => store.unsetKeybinding(keybinding)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw an error when unsetting unknown keybinding', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'I', ctrl: true }
|
||||
})
|
||||
store.addUserKeybinding(keybinding)
|
||||
|
||||
expect(() =>
|
||||
store.unsetKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'test.foo',
|
||||
combo: { key: 'I', ctrl: true }
|
||||
})
|
||||
)
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('should remove unset keybinding when adding back a default keybinding', () => {
|
||||
const store = useKeybindingStore()
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'I', ctrl: true }
|
||||
})
|
||||
|
||||
// Add default keybinding
|
||||
store.addDefaultKeybinding(defaultKeybinding)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
|
||||
// Unset the default keybinding
|
||||
store.unsetKeybinding(defaultKeybinding)
|
||||
expect(store.keybindings).toHaveLength(0)
|
||||
|
||||
// Add the same keybinding as a user keybinding
|
||||
store.addUserKeybinding(defaultKeybinding)
|
||||
|
||||
// Check that the keybinding is back and not in the unset list
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(defaultKeybinding.combo)).toEqual(
|
||||
defaultKeybinding
|
||||
)
|
||||
})
|
||||
|
||||
it('Should accept same keybinding from default and user', () => {
|
||||
const store = useKeybindingStore()
|
||||
const keybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'J', ctrl: true }
|
||||
})
|
||||
// Add default keybinding.
|
||||
// This can happen when we change default keybindings.
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
// Add user keybinding.
|
||||
store.addUserKeybinding(keybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
|
||||
})
|
||||
|
||||
it('Should keep previously customized keybindings after default keybindings change', () => {
|
||||
// Initially command 'foo' was bound to 'K, Ctrl'. User unset it and bound the
|
||||
// command to 'A, Ctrl'.
|
||||
// Now we change the default keybindings of 'foo' to 'A, Ctrl'.
|
||||
// The user customized keybinding should be kept.
|
||||
const store = useKeybindingStore()
|
||||
|
||||
const userUnsetKeybindings = [
|
||||
new KeybindingImpl({
|
||||
commandId: 'foo',
|
||||
combo: { key: 'K', ctrl: true }
|
||||
})
|
||||
]
|
||||
|
||||
const userNewKeybindings = [
|
||||
new KeybindingImpl({
|
||||
commandId: 'foo',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
})
|
||||
]
|
||||
|
||||
const newCoreKeybindings = [
|
||||
new KeybindingImpl({
|
||||
commandId: 'foo',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
})
|
||||
]
|
||||
|
||||
for (const keybinding of newCoreKeybindings) {
|
||||
store.addDefaultKeybinding(keybinding)
|
||||
}
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
|
||||
userNewKeybindings[0]
|
||||
)
|
||||
|
||||
for (const keybinding of userUnsetKeybindings) {
|
||||
store.unsetKeybinding(keybinding)
|
||||
}
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
|
||||
userNewKeybindings[0]
|
||||
)
|
||||
|
||||
for (const keybinding of userNewKeybindings) {
|
||||
store.addUserKeybinding(keybinding)
|
||||
}
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(userNewKeybindings[0].combo)).toEqual(
|
||||
userNewKeybindings[0]
|
||||
)
|
||||
})
|
||||
|
||||
it('should replace the previous keybinding with a new one for the same combo and unset the old command', () => {
|
||||
const store = useKeybindingStore()
|
||||
|
||||
const oldKeybinding = new KeybindingImpl({
|
||||
commandId: 'command1',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
})
|
||||
|
||||
store.addUserKeybinding(oldKeybinding)
|
||||
|
||||
const newKeybinding = new KeybindingImpl({
|
||||
commandId: 'command2',
|
||||
combo: { key: 'A', ctrl: true }
|
||||
})
|
||||
|
||||
store.updateKeybindingOnCommand(newKeybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybinding(newKeybinding.combo)?.commandId).toBe('command2')
|
||||
expect(store.getKeybindingsByCommandId('command1')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should return false when no default or current keybinding exists during reset', () => {
|
||||
const store = useKeybindingStore()
|
||||
const result = store.resetKeybindingForCommand('nonexistent.command')
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when current keybinding equals default keybinding', () => {
|
||||
const store = useKeybindingStore()
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'L', ctrl: true }
|
||||
})
|
||||
|
||||
store.addDefaultKeybinding(defaultKeybinding)
|
||||
const result = store.resetKeybindingForCommand('test.command')
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybindingByCommandId('test.command')).toEqual(
|
||||
defaultKeybinding
|
||||
)
|
||||
})
|
||||
|
||||
it('should unset user keybinding when no default keybinding exists and return true', () => {
|
||||
const store = useKeybindingStore()
|
||||
const userKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'M', ctrl: true }
|
||||
})
|
||||
|
||||
store.addUserKeybinding(userKeybinding)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
|
||||
const result = store.resetKeybindingForCommand('test.command')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(store.keybindings).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should restore default keybinding when user has overridden it and return true', () => {
|
||||
const store = useKeybindingStore()
|
||||
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'N', ctrl: true }
|
||||
})
|
||||
|
||||
const userKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'O', alt: true }
|
||||
})
|
||||
|
||||
store.addDefaultKeybinding(defaultKeybinding)
|
||||
store.updateKeybindingOnCommand(userKeybinding)
|
||||
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybindingByCommandId('test.command')).toEqual(
|
||||
userKeybinding
|
||||
)
|
||||
|
||||
const result = store.resetKeybindingForCommand('test.command')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybindingByCommandId('test.command')).toEqual(
|
||||
defaultKeybinding
|
||||
)
|
||||
})
|
||||
|
||||
it('should remove unset record and restore default keybinding when user has unset it', () => {
|
||||
const store = useKeybindingStore()
|
||||
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'P', ctrl: true }
|
||||
})
|
||||
|
||||
store.addDefaultKeybinding(defaultKeybinding)
|
||||
|
||||
store.unsetKeybinding(defaultKeybinding)
|
||||
expect(store.keybindings).toHaveLength(0)
|
||||
|
||||
const serializedCombo = defaultKeybinding.combo.serialize()
|
||||
const userUnsetKeybindings = store.getUserUnsetKeybindings()
|
||||
expect(userUnsetKeybindings[serializedCombo]).toBeTruthy()
|
||||
expect(
|
||||
userUnsetKeybindings[serializedCombo].equals(defaultKeybinding)
|
||||
).toBe(true)
|
||||
|
||||
const result = store.resetKeybindingForCommand('test.command')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybindingByCommandId('test.command')).toEqual(
|
||||
defaultKeybinding
|
||||
)
|
||||
|
||||
expect(store.getUserUnsetKeybindings()[serializedCombo]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle complex scenario with both unset and user keybindings', () => {
|
||||
const store = useKeybindingStore()
|
||||
|
||||
// Create default keybinding
|
||||
const defaultKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'Q', ctrl: true }
|
||||
})
|
||||
store.addDefaultKeybinding(defaultKeybinding)
|
||||
|
||||
// Unset default keybinding
|
||||
store.unsetKeybinding(defaultKeybinding)
|
||||
expect(store.keybindings).toHaveLength(0)
|
||||
|
||||
// Add user keybinding with different combo
|
||||
const userKeybinding = new KeybindingImpl({
|
||||
commandId: 'test.command',
|
||||
combo: { key: 'R', alt: true }
|
||||
})
|
||||
store.addUserKeybinding(userKeybinding)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybindingByCommandId('test.command')).toEqual(
|
||||
userKeybinding
|
||||
)
|
||||
|
||||
// Reset keybinding to default
|
||||
const result = store.resetKeybindingForCommand('test.command')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(store.keybindings).toHaveLength(1)
|
||||
expect(store.getKeybindingByCommandId('test.command')).toEqual(
|
||||
defaultKeybinding
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,355 +0,0 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
|
||||
import { RESERVED_BY_TEXT_INPUT } from '@/constants/reservedKeyCombos'
|
||||
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
|
||||
|
||||
export class KeybindingImpl implements Keybinding {
|
||||
commandId: string
|
||||
combo: KeyComboImpl
|
||||
targetElementId?: string
|
||||
|
||||
constructor(obj: Keybinding) {
|
||||
this.commandId = obj.commandId
|
||||
this.combo = new KeyComboImpl(obj.combo)
|
||||
this.targetElementId = obj.targetElementId
|
||||
}
|
||||
|
||||
equals(other: unknown): boolean {
|
||||
const raw = toRaw(other)
|
||||
|
||||
return raw instanceof KeybindingImpl
|
||||
? this.commandId === raw.commandId &&
|
||||
this.combo.equals(raw.combo) &&
|
||||
this.targetElementId === raw.targetElementId
|
||||
: false
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyComboImpl implements KeyCombo {
|
||||
key: string
|
||||
// ctrl or meta(cmd on mac)
|
||||
ctrl: boolean
|
||||
alt: boolean
|
||||
shift: boolean
|
||||
|
||||
constructor(obj: KeyCombo) {
|
||||
this.key = obj.key
|
||||
this.ctrl = obj.ctrl ?? false
|
||||
this.alt = obj.alt ?? false
|
||||
this.shift = obj.shift ?? false
|
||||
}
|
||||
|
||||
static fromEvent(event: KeyboardEvent) {
|
||||
return new KeyComboImpl({
|
||||
key: event.key,
|
||||
ctrl: event.ctrlKey || event.metaKey,
|
||||
alt: event.altKey,
|
||||
shift: event.shiftKey
|
||||
})
|
||||
}
|
||||
|
||||
equals(other: unknown): boolean {
|
||||
const raw = toRaw(other)
|
||||
|
||||
return raw instanceof KeyComboImpl
|
||||
? this.key.toUpperCase() === raw.key.toUpperCase() &&
|
||||
this.ctrl === raw.ctrl &&
|
||||
this.alt === raw.alt &&
|
||||
this.shift === raw.shift
|
||||
: false
|
||||
}
|
||||
|
||||
serialize(): string {
|
||||
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.getKeySequences().join(' + ')
|
||||
}
|
||||
|
||||
get hasModifier(): boolean {
|
||||
return this.ctrl || this.alt || this.shift
|
||||
}
|
||||
|
||||
get isModifier(): boolean {
|
||||
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
|
||||
}
|
||||
|
||||
get modifierCount(): number {
|
||||
const modifiers = [this.ctrl, this.alt, this.shift]
|
||||
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
|
||||
}
|
||||
|
||||
get isShiftOnly(): boolean {
|
||||
return this.shift && this.modifierCount === 1
|
||||
}
|
||||
|
||||
get isReservedByTextInput(): boolean {
|
||||
return (
|
||||
!this.hasModifier ||
|
||||
this.isShiftOnly ||
|
||||
RESERVED_BY_TEXT_INPUT.has(this.toString())
|
||||
)
|
||||
}
|
||||
|
||||
getKeySequences(): string[] {
|
||||
const sequences: string[] = []
|
||||
if (this.ctrl) {
|
||||
sequences.push('Ctrl')
|
||||
}
|
||||
if (this.alt) {
|
||||
sequences.push('Alt')
|
||||
}
|
||||
if (this.shift) {
|
||||
sequences.push('Shift')
|
||||
}
|
||||
sequences.push(this.key)
|
||||
return sequences
|
||||
}
|
||||
}
|
||||
|
||||
export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
/**
|
||||
* Default keybindings provided by core and extensions.
|
||||
*/
|
||||
const defaultKeybindings = ref<Record<string, KeybindingImpl>>({})
|
||||
/**
|
||||
* User-defined keybindings.
|
||||
*/
|
||||
const userKeybindings = ref<Record<string, KeybindingImpl>>({})
|
||||
/**
|
||||
* User-defined keybindings that unset default keybindings.
|
||||
*/
|
||||
const userUnsetKeybindings = ref<Record<string, KeybindingImpl>>({})
|
||||
|
||||
/**
|
||||
* Get user-defined keybindings.
|
||||
*/
|
||||
function getUserKeybindings() {
|
||||
return userKeybindings.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-defined keybindings that unset default keybindings.
|
||||
*/
|
||||
function getUserUnsetKeybindings() {
|
||||
return userUnsetKeybindings.value
|
||||
}
|
||||
|
||||
const keybindingByKeyCombo = computed<Record<string, KeybindingImpl>>(() => {
|
||||
const result: Record<string, KeybindingImpl> = {
|
||||
...defaultKeybindings.value
|
||||
}
|
||||
|
||||
for (const keybinding of Object.values(userUnsetKeybindings.value)) {
|
||||
const serializedCombo = keybinding.combo.serialize()
|
||||
if (result[serializedCombo]?.equals(keybinding)) {
|
||||
delete result[serializedCombo]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
...userKeybindings.value
|
||||
}
|
||||
})
|
||||
|
||||
const keybindings = computed<KeybindingImpl[]>(() =>
|
||||
Object.values(keybindingByKeyCombo.value)
|
||||
)
|
||||
|
||||
function getKeybinding(combo: KeyComboImpl) {
|
||||
return keybindingByKeyCombo.value[combo.serialize()]
|
||||
}
|
||||
|
||||
const keybindingsByCommandId = computed<Record<string, KeybindingImpl[]>>(
|
||||
() => {
|
||||
return _.groupBy(keybindings.value, 'commandId')
|
||||
}
|
||||
)
|
||||
|
||||
function getKeybindingsByCommandId(commandId: string) {
|
||||
return keybindingsByCommandId.value[commandId] ?? []
|
||||
}
|
||||
|
||||
const defaultKeybindingsByCommandId = computed<
|
||||
Record<string, KeybindingImpl[]>
|
||||
>(() => {
|
||||
return _.groupBy(Object.values(defaultKeybindings.value), 'commandId')
|
||||
})
|
||||
|
||||
function getKeybindingByCommandId(commandId: string) {
|
||||
return getKeybindingsByCommandId(commandId)[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a keybinding to the specified target reference.
|
||||
*
|
||||
* @param target - A ref that holds a record of keybindings. The keys represent
|
||||
* serialized key combos, and the values are `KeybindingImpl` objects.
|
||||
* @param keybinding - The keybinding to add, represented as a `KeybindingImpl` object.
|
||||
* @param options - An options object.
|
||||
* @param options.existOk - If true, allows overwriting an existing keybinding with the
|
||||
* same combo. Defaults to false.
|
||||
*
|
||||
* @throws {Error} Throws an error if a keybinding with the same combo already exists in
|
||||
* the target and `existOk` is false.
|
||||
*/
|
||||
function addKeybinding(
|
||||
target: Ref<Record<string, KeybindingImpl>>,
|
||||
keybinding: KeybindingImpl,
|
||||
{ existOk = false }: { existOk: boolean }
|
||||
) {
|
||||
if (!existOk && keybinding.combo.serialize() in target.value) {
|
||||
throw new Error(
|
||||
`Keybinding on ${keybinding.combo} already exists on ${
|
||||
target.value[keybinding.combo.serialize()].commandId
|
||||
}`
|
||||
)
|
||||
}
|
||||
target.value[keybinding.combo.serialize()] = keybinding
|
||||
}
|
||||
|
||||
function addDefaultKeybinding(keybinding: KeybindingImpl) {
|
||||
addKeybinding(defaultKeybindings, keybinding, { existOk: false })
|
||||
}
|
||||
|
||||
function addUserKeybinding(keybinding: KeybindingImpl) {
|
||||
const defaultKeybinding =
|
||||
defaultKeybindings.value[keybinding.combo.serialize()]
|
||||
const userUnsetKeybinding =
|
||||
userUnsetKeybindings.value[keybinding.combo.serialize()]
|
||||
|
||||
// User is adding back a keybinding that was an unsetted default keybinding.
|
||||
if (
|
||||
keybinding.equals(defaultKeybinding) &&
|
||||
keybinding.equals(userUnsetKeybinding)
|
||||
) {
|
||||
delete userUnsetKeybindings.value[keybinding.combo.serialize()]
|
||||
return
|
||||
}
|
||||
|
||||
// Unset keybinding on default keybinding if it exists and is not the same as userUnsetKeybinding
|
||||
if (defaultKeybinding && !defaultKeybinding.equals(userUnsetKeybinding)) {
|
||||
unsetKeybinding(defaultKeybinding)
|
||||
}
|
||||
|
||||
addKeybinding(userKeybindings, keybinding, { existOk: true })
|
||||
}
|
||||
|
||||
function unsetKeybinding(keybinding: KeybindingImpl) {
|
||||
const serializedCombo = keybinding.combo.serialize()
|
||||
if (!(serializedCombo in keybindingByKeyCombo.value)) {
|
||||
console.warn(
|
||||
`Trying to unset non-exist keybinding: ${JSON.stringify(keybinding)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (userKeybindings.value[serializedCombo]?.equals(keybinding)) {
|
||||
delete userKeybindings.value[serializedCombo]
|
||||
return
|
||||
}
|
||||
|
||||
if (defaultKeybindings.value[serializedCombo]?.equals(keybinding)) {
|
||||
addKeybinding(userUnsetKeybindings, keybinding, { existOk: false })
|
||||
return
|
||||
}
|
||||
|
||||
console.warn(`Unset unknown keybinding: ${JSON.stringify(keybinding)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the keybinding on given command if it is different from the current keybinding.
|
||||
*
|
||||
* @returns true if the keybinding is updated, false otherwise.
|
||||
*/
|
||||
function updateKeybindingOnCommand(keybinding: KeybindingImpl): boolean {
|
||||
const currentKeybinding = getKeybindingByCommandId(keybinding.commandId)
|
||||
if (currentKeybinding?.equals(keybinding)) {
|
||||
return false
|
||||
}
|
||||
if (currentKeybinding) {
|
||||
unsetKeybinding(currentKeybinding)
|
||||
}
|
||||
addUserKeybinding(keybinding)
|
||||
return true
|
||||
}
|
||||
|
||||
function resetAllKeybindings() {
|
||||
userKeybindings.value = {}
|
||||
userUnsetKeybindings.value = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the keybinding for a given command to its default value.
|
||||
*
|
||||
* @param commandId - The commandId of the keybind to be reset
|
||||
* @returns `true` if changes were made, `false` if not
|
||||
*/
|
||||
function resetKeybindingForCommand(commandId: string): boolean {
|
||||
const currentKeybinding = getKeybindingByCommandId(commandId)
|
||||
const defaultKeybinding =
|
||||
defaultKeybindingsByCommandId.value[commandId]?.[0]
|
||||
|
||||
// No default keybinding exists, need to remove any user binding
|
||||
if (!defaultKeybinding) {
|
||||
if (currentKeybinding) {
|
||||
unsetKeybinding(currentKeybinding)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Current binding equals default binding, no changes needed
|
||||
if (currentKeybinding?.equals(defaultKeybinding)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Unset current keybinding if exists
|
||||
if (currentKeybinding) {
|
||||
unsetKeybinding(currentKeybinding)
|
||||
}
|
||||
|
||||
// Remove the unset record if it exists
|
||||
const serializedCombo = defaultKeybinding.combo.serialize()
|
||||
if (
|
||||
userUnsetKeybindings.value[serializedCombo]?.equals(defaultKeybinding)
|
||||
) {
|
||||
delete userUnsetKeybindings.value[serializedCombo]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function isCommandKeybindingModified(commandId: string): boolean {
|
||||
const currentKeybinding: KeybindingImpl | undefined =
|
||||
getKeybindingByCommandId(commandId)
|
||||
const defaultKeybinding: KeybindingImpl | undefined =
|
||||
defaultKeybindingsByCommandId.value[commandId]?.[0]
|
||||
|
||||
return !(
|
||||
(currentKeybinding === undefined && defaultKeybinding === undefined) ||
|
||||
currentKeybinding?.equals(defaultKeybinding)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
keybindings,
|
||||
getUserKeybindings,
|
||||
getUserUnsetKeybindings,
|
||||
getKeybinding,
|
||||
getKeybindingsByCommandId,
|
||||
getKeybindingByCommandId,
|
||||
addDefaultKeybinding,
|
||||
addUserKeybinding,
|
||||
unsetKeybinding,
|
||||
updateKeybindingOnCommand,
|
||||
resetAllKeybindings,
|
||||
resetKeybindingForCommand,
|
||||
isCommandKeybindingModified
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user