diff --git a/src/components/dialog/content/SettingDialogContent.vue b/src/components/dialog/content/SettingDialogContent.vue index 3b529506d..730dd2c8b 100644 --- a/src/components/dialog/content/SettingDialogContent.vue +++ b/src/components/dialog/content/SettingDialogContent.vue @@ -54,6 +54,9 @@ + + + @@ -74,6 +77,7 @@ import SearchBox from '@/components/common/SearchBox.vue' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import { flattenTree } from '@/utils/treeUtil' import AboutPanel from './setting/AboutPanel.vue' +import KeybindingPanel from './setting/KeybindingPanel.vue' interface ISettingGroup { label: string @@ -86,10 +90,17 @@ const aboutPanelNode: SettingTreeNode = { children: [] } +const keybindingPanelNode: SettingTreeNode = { + key: 'keybinding', + label: 'Keybinding', + children: [] +} + const settingStore = useSettingStore() const settingRoot = computed(() => settingStore.settingTree) const categories = computed(() => [ ...(settingRoot.value.children || []), + keybindingPanelNode, aboutPanelNode ]) const activeCategory = ref(null) diff --git a/src/components/dialog/content/setting/KeybindingPanel.vue b/src/components/dialog/content/setting/KeybindingPanel.vue new file mode 100644 index 000000000..d6f9c1f03 --- /dev/null +++ b/src/components/dialog/content/setting/KeybindingPanel.vue @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + - + + + + + + + + + Keybinding already exists on + + + + + + + + + + + + + + diff --git a/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue new file mode 100644 index 000000000..25ab289a6 --- /dev/null +++ b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue @@ -0,0 +1,28 @@ + + + + + {{ sequence }} + + + + + + + + diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 02cc7a1fa..4e81bdefd 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -52,6 +52,7 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore' import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue' +import { useKeybindingStore } from '@/stores/keybindingStore' const emit = defineEmits(['ready']) const canvasRef = ref(null) @@ -200,6 +201,9 @@ onMounted(async () => { } }) + // Load keybindings. This must be done after comfyApp loads settings. + useKeybindingStore().loadUserKeybindings() + // Migrate legacy bookmarks useNodeBookmarkStore().migrateLegacyBookmarks() diff --git a/src/extensions/core/keybinds.ts b/src/extensions/core/keybinds.ts index 15fab32d0..6b8ab4bdb 100644 --- a/src/extensions/core/keybinds.ts +++ b/src/extensions/core/keybinds.ts @@ -59,6 +59,6 @@ app.registerExtension({ } } - window.addEventListener('keydown', keybindListener, true) + window.addEventListener('keydown', keybindListener) } }) diff --git a/src/i18n.ts b/src/i18n.ts index 89ed73496..850493410 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -9,6 +9,7 @@ const messages = { add: 'Add', confirm: 'Confirm', reset: 'Reset', + resetKeybindingsTooltip: 'Reset keybindings to default', customizeFolder: 'Customize Folder', icon: 'Icon', color: 'Color', @@ -112,6 +113,7 @@ const messages = { add: '添加', confirm: '确认', reset: '重置', + resetKeybindingsTooltip: '重置键位', customizeFolder: '定制文件夹', icon: '图标', color: '颜色', diff --git a/src/stores/commandStore.ts b/src/stores/commandStore.ts index 248018d1f..7263d2b9a 100644 --- a/src/stores/commandStore.ts +++ b/src/stores/commandStore.ts @@ -1,7 +1,7 @@ import { app } from '@/scripts/app' import { api } from '@/scripts/api' import { defineStore } from 'pinia' -import { ref } from 'vue' +import { computed, ref } from 'vue' import { globalTracker } from '@/scripts/changeTracker' import { useSettingStore } from '@/stores/settingStore' import { useToastStore } from '@/stores/toastStore' @@ -32,12 +32,14 @@ const getTracker = () => export const useCommandStore = defineStore('command', () => { const settingStore = useSettingStore() - const commands = ref>({}) + const commandsById = ref>({}) + const commands = computed(() => Object.values(commandsById.value)) + const registerCommand = (command: ComfyCommand) => { - if (commands.value[command.id]) { + if (commandsById.value[command.id]) { console.warn(`Command ${command.id} already registered`) } - commands.value[command.id] = command + commandsById.value[command.id] = command } const commandDefinitions: ComfyCommand[] = [ @@ -311,15 +313,15 @@ export const useCommandStore = defineStore('command', () => { commandDefinitions.forEach(registerCommand) const getCommandFunction = (command: string) => { - return commands.value[command]?.function ?? (() => {}) + return commandsById.value[command]?.function ?? (() => {}) } const getCommand = (command: string) => { - return commands.value[command] + return commandsById.value[command] } const isRegistered = (command: string) => { - return !!commands.value[command] + return !!commandsById.value[command] } const loadExtensionCommands = (extension: ComfyExtension) => { @@ -331,6 +333,7 @@ export const useCommandStore = defineStore('command', () => { } return { + commands, getCommand, getCommandFunction, registerCommand, diff --git a/src/stores/keybindingStore.ts b/src/stores/keybindingStore.ts index d7bb42e82..924b23ea6 100644 --- a/src/stores/keybindingStore.ts +++ b/src/stores/keybindingStore.ts @@ -74,7 +74,7 @@ export class KeyComboImpl implements KeyCombo { } toString(): string { - return `${this.key} + ${this.ctrl ? 'Ctrl' : ''}${this.alt ? 'Alt' : ''}${this.shift ? 'Shift' : ''}` + return this.getKeySequences().join(' + ') } get hasModifier(): boolean { @@ -84,6 +84,21 @@ export class KeyComboImpl implements KeyCombo { get isModifier(): boolean { return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key) } + + 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', () => { @@ -123,6 +138,37 @@ export const useKeybindingStore = defineStore('keybinding', () => { return keybindingByKeyCombo.value[combo.serialize()] } + function createKeybindingsByCommandId(keybindings: KeybindingImpl[]) { + const result: Record = {} + for (const keybinding of keybindings) { + if (!(keybinding.commandId in result)) { + result[keybinding.commandId] = [] + } + result[keybinding.commandId].push(keybinding) + } + return result + } + + const keybindingsByCommandId = computed>( + () => { + return createKeybindingsByCommandId(keybindings.value) + } + ) + + function getKeybindingsByCommandId(commandId: string) { + return keybindingsByCommandId.value[commandId] ?? [] + } + + const defaultKeybindingsByCommandId = computed< + Record + >(() => { + return createKeybindingsByCommandId(Object.values(defaultKeybindings.value)) + }) + + function getKeybindingByCommandId(commandId: string) { + return getKeybindingsByCommandId(commandId)[0] + } + function addKeybinding( target: Ref>, keybinding: KeybindingImpl, @@ -145,9 +191,23 @@ export const useKeybindingStore = defineStore('keybinding', () => { function addUserKeybinding(keybinding: KeybindingImpl) { const defaultKeybinding = defaultKeybindings.value[keybinding.combo.serialize()] - if (defaultKeybinding) { + 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 }) } @@ -170,6 +230,23 @@ export const useKeybindingStore = defineStore('keybinding', () => { throw new Error(`NOT_REACHED`) } + /** + * 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 loadUserKeybindings() { const settingStore = useSettingStore() // Unset bindings first as new bindings might conflict with default bindings. @@ -204,14 +281,51 @@ export const useKeybindingStore = defineStore('keybinding', () => { } } + async function persistUserKeybindings() { + const settingStore = useSettingStore() + // TODO(https://github.com/Comfy-Org/ComfyUI_frontend/issues/1079): + // Allow setting multiple values at once in settingStore + await settingStore.set( + 'Comfy.Keybinding.NewBindings', + Object.values(userKeybindings.value) + ) + await settingStore.set( + 'Comfy.Keybinding.UnsetBindings', + Object.values(userUnsetKeybindings.value) + ) + } + + function resetKeybindings() { + userKeybindings.value = {} + userUnsetKeybindings.value = {} + } + + 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, getKeybinding, + getKeybindingsByCommandId, + getKeybindingByCommandId, addDefaultKeybinding, addUserKeybinding, unsetKeybinding, + updateKeybindingOnCommand, loadUserKeybindings, loadCoreKeybindings, - loadExtensionKeybindings + loadExtensionKeybindings, + persistUserKeybindings, + resetKeybindings, + isCommandKeybindingModified } }) diff --git a/src/stores/settingStore.ts b/src/stores/settingStore.ts index 31ee389ca..ee08102e4 100644 --- a/src/stores/settingStore.ts +++ b/src/stores/settingStore.ts @@ -67,9 +67,9 @@ export const useSettingStore = defineStore('setting', { }) }, - set(key: K, value: Settings[K]) { + async set(key: K, value: Settings[K]) { this.settingValues[key] = value - app.ui.settings.setSettingValue(key, value) + await app.ui.settings.setSettingValueAsync(key, value) }, get(key: K): Settings[K] { diff --git a/tests-ui/tests/store/keybindingStore.test.ts b/tests-ui/tests/store/keybindingStore.test.ts index 97e64a059..c54005423 100644 --- a/tests-ui/tests/store/keybindingStore.test.ts +++ b/tests-ui/tests/store/keybindingStore.test.ts @@ -53,6 +53,25 @@ describe('useKeybindingStore', () => { 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({ @@ -119,4 +138,29 @@ describe('useKeybindingStore', () => { expect(() => store.unsetKeybinding(keybinding)).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 + ) + }) })