diff --git a/browser_tests/ComfyPage.ts b/browser_tests/ComfyPage.ts index a35e76470..032ab4044 100644 --- a/browser_tests/ComfyPage.ts +++ b/browser_tests/ComfyPage.ts @@ -6,7 +6,8 @@ import dotenv from 'dotenv' dotenv.config() import * as fs from 'fs' import { NodeBadgeMode } from '../src/types/nodeSource' -import { NodeId } from '../src/types/comfyWorkflow' +import type { NodeId } from '../src/types/comfyWorkflow' +import type { KeyCombo } from '../src/types/keyBindingTypes' import { ManageGroupNode } from './helpers/manageGroupNode' import { ComfyTemplates } from './helpers/templates' @@ -488,6 +489,34 @@ export class ComfyPage { return `./browser_tests/assets/${fileName}` } + async registerKeybinding(keyCombo: KeyCombo, command: () => void) { + await this.page.evaluate( + ({ keyCombo, commandStr }) => { + const app = window['app'] + const randomSuffix = Math.random().toString(36).substring(2, 8) + const extensionName = `TestExtension_${randomSuffix}` + const commandId = `TestCommand_${randomSuffix}` + + app.registerExtension({ + name: extensionName, + keybindings: [ + { + combo: keyCombo, + commandId: commandId + } + ], + commands: [ + { + id: commandId, + function: eval(commandStr) + } + ] + }) + }, + { keyCombo, commandStr: command.toString() } + ) + } + async setSetting(settingId: string, settingValue: any) { return await this.page.evaluate( async ({ id, value }) => { diff --git a/browser_tests/keybindings.spec.ts b/browser_tests/keybindings.spec.ts new file mode 100644 index 000000000..e58610b5b --- /dev/null +++ b/browser_tests/keybindings.spec.ts @@ -0,0 +1,37 @@ +import { expect } from '@playwright/test' +import { comfyPageFixture as test } from './ComfyPage' + +test.describe('Keybindings', () => { + test('Should not trigger non-modifier keybinding when typing in input fields', async ({ + comfyPage + }) => { + await comfyPage.registerKeybinding({ key: 'k' }, () => { + window['TestCommand'] = true + }) + + const textBox = comfyPage.widgetTextBox + await textBox.click() + await textBox.fill('k') + await expect(textBox).toHaveValue('k') + expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( + undefined + ) + }) + + test('Should not trigger modifier keybinding when typing in input fields', async ({ + comfyPage + }) => { + await comfyPage.registerKeybinding({ key: 'k', ctrl: true }, () => { + window['TestCommand'] = true + }) + + const textBox = comfyPage.widgetTextBox + await textBox.click() + await textBox.fill('q') + await textBox.press('Control+k') + await expect(textBox).toHaveValue('q') + expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( + true + ) + }) +}) diff --git a/src/components/appMenu/AppMenu.vue b/src/components/appMenu/AppMenu.vue index 6aaf59dc5..acc536861 100644 --- a/src/components/appMenu/AppMenu.vue +++ b/src/components/appMenu/AppMenu.vue @@ -91,7 +91,7 @@ import { debounce, clamp } from 'lodash' const settingsStore = useSettingStore() const commandStore = useCommandStore() const queueCountStore = storeToRefs(useQueuePendingTaskCountStore()) -const { batchCount, mode: queueMode } = storeToRefs(useQueueSettingsStore()) +const { mode: queueMode } = storeToRefs(useQueueSettingsStore()) const visible = computed( () => settingsStore.get('Comfy.UseNewMenu') === 'Floating' @@ -139,7 +139,8 @@ const executingPrompt = computed(() => !!queueCountStore.count.value) const hasPendingTasks = computed(() => queueCountStore.count.value > 1) const queuePrompt = (e: MouseEvent) => { - app.queuePrompt(e.shiftKey ? -1 : 0, batchCount.value) + const commandId = e.shiftKey ? 'Comfy.QueuePromptFront' : 'Comfy.QueuePrompt' + commandStore.getCommandFunction(commandId)() } const panelRef = ref(null) diff --git a/src/extensions/core/keybinds.ts b/src/extensions/core/keybinds.ts index 98bf8cbe9..15fab32d0 100644 --- a/src/extensions/core/keybinds.ts +++ b/src/extensions/core/keybinds.ts @@ -1,6 +1,4 @@ import { app } from '../../scripts/app' -import { api } from '../../scripts/api' -import { useToastStore } from '@/stores/toastStore' import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore' import { useCommandStore } from '@/stores/commandStore' @@ -13,59 +11,28 @@ app.registerExtension({ if (!app.vueAppReady) return const keyCombo = KeyComboImpl.fromEvent(event) + if (keyCombo.isModifier) { + return + } + + // Ignore non-modifier keybindings if typing in input fields + const target = event.composedPath()[0] as HTMLElement + if ( + !keyCombo.hasModifier && + (target.tagName === 'TEXTAREA' || + target.tagName === 'INPUT' || + (target.tagName === 'SPAN' && + target.classList.contains('property_value'))) + ) { + return + } + const keybindingStore = useKeybindingStore() const commandStore = useCommandStore() const keybinding = keybindingStore.getKeybinding(keyCombo) if (keybinding) { await commandStore.getCommandFunction(keybinding.commandId)() - return - } - - const modifierPressed = event.ctrlKey || event.metaKey - - // Queue prompt using (ctrl or command) + enter - if (modifierPressed && event.key === 'Enter') { - // Cancel current prompt using (ctrl or command) + alt + enter - if (event.altKey) { - await api.interrupt() - useToastStore().add({ - severity: 'info', - summary: 'Interrupted', - detail: 'Execution has been interrupted', - life: 1000 - }) - return - } - // Queue prompt as first for generation using (ctrl or command) + shift + enter - app.queuePrompt(event.shiftKey ? -1 : 0).then() - return - } - - const target = event.composedPath()[0] as HTMLElement - if ( - target.tagName === 'TEXTAREA' || - target.tagName === 'INPUT' || - (target.tagName === 'SPAN' && - target.classList.contains('property_value')) - ) { - return - } - - const modifierKeyIdMap = { - s: '#comfy-save-button', - o: '#comfy-file-input', - Backspace: '#comfy-clear-button', - d: '#comfy-load-default-button', - g: '#comfy-group-selected-nodes-button', - ',': '.comfy-settings-btn' - } - - const modifierKeybindId = modifierKeyIdMap[event.key] - if (modifierPressed && modifierKeybindId) { event.preventDefault() - - const elem = document.querySelector(modifierKeybindId) - elem.click() return } @@ -90,18 +57,6 @@ app.registerExtension({ d.close() }) } - - const keyIdMap = { - q: '.queue-tab-button.side-bar-button', - h: '.queue-tab-button.side-bar-button', - r: '#comfy-refresh-button' - } - - const buttonId = keyIdMap[event.key] - if (buttonId) { - const button = document.querySelector(buttonId) - button.click() - } } window.addEventListener('keydown', keybindListener, true) diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts index 8367da219..b121e0fc2 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -5,10 +5,7 @@ import { ComfySettingsDialog } from './ui/settings' import { ComfyApp, app } from './app' import { TaskItem } from '@/types/apiTypes' import { showSettingsDialog } from '@/services/dialogService' -import { useToastStore } from '@/stores/toastStore' -import { LGraphGroup } from '@comfyorg/litegraph' import { useSettingStore } from '@/stores/settingStore' -import { useTitleEditorStore } from '@/stores/graphStore' export const ComfyDialog = _ComfyDialog @@ -695,32 +692,6 @@ export class ComfyUI { onclick: async () => { app.resetView() } - }), - $el('button', { - id: 'comfy-group-selected-nodes-button', - textContent: 'Group', - hidden: true, - onclick: () => { - if ( - !app.canvas.selected_nodes || - Object.keys(app.canvas.selected_nodes).length === 0 - ) { - useToastStore().add({ - severity: 'error', - summary: 'No nodes selected', - detail: 'Please select nodes to group', - life: 3000 - }) - return - } - const group = new LGraphGroup() - const padding = useSettingStore().get( - 'Comfy.GroupSelectedNodes.Padding' - ) - group.addNodes(Object.values(app.canvas.selected_nodes), padding) - app.canvas.graph.add(group) - useTitleEditorStore().titleEditorTarget = group - } }) ]) as HTMLDivElement diff --git a/src/stores/commandStore.ts b/src/stores/commandStore.ts index 7cdbc3923..248018d1f 100644 --- a/src/stores/commandStore.ts +++ b/src/stores/commandStore.ts @@ -5,10 +5,16 @@ import { ref } from 'vue' import { globalTracker } from '@/scripts/changeTracker' import { useSettingStore } from '@/stores/settingStore' import { useToastStore } from '@/stores/toastStore' -import { showTemplateWorkflowsDialog } from '@/services/dialogService' -import { useQueueStore } from './queueStore' +import { + showSettingsDialog, + showTemplateWorkflowsDialog +} from '@/services/dialogService' +import { useQueueSettingsStore, useQueueStore } from './queueStore' import { LiteGraph } from '@comfyorg/litegraph' import { ComfyExtension } from '@/types/comfy' +import { useWorkspaceStore } from './workspaceStateStore' +import { LGraphGroup } from '@comfyorg/litegraph' +import { useTitleEditorStore } from './graphStore' export interface ComfyCommand { id: string @@ -231,6 +237,75 @@ export const useCommandStore = defineStore('command', () => { } } })() + }, + { + id: 'Comfy.QueuePrompt', + icon: 'pi pi-play', + label: 'Queue Prompt', + versionAdded: '1.3.7', + function: () => { + const batchCount = useQueueSettingsStore().batchCount + app.queuePrompt(0, batchCount) + } + }, + { + id: 'Comfy.QueuePromptFront', + icon: 'pi pi-play', + label: 'Queue Prompt (Front)', + versionAdded: '1.3.7', + function: () => { + const batchCount = useQueueSettingsStore().batchCount + app.queuePrompt(-1, batchCount) + } + }, + { + id: 'Comfy.ToggleQueueSidebarTab', + icon: 'pi pi-history', + label: 'Queue', + versionAdded: '1.3.7', + function: () => { + const tabId = 'queue' + const workspaceStore = useWorkspaceStore() + workspaceStore.updateActiveSidebarTab( + workspaceStore.activeSidebarTab === tabId ? null : tabId + ) + } + }, + { + id: 'Comfy.ShowSettingsDialog', + icon: 'pi pi-cog', + label: 'Settings', + versionAdded: '1.3.7', + function: () => { + showSettingsDialog() + } + }, + { + id: 'Comfy.Graph.GroupSelectedNodes', + icon: 'pi pi-sitemap', + label: 'Group Selected Nodes', + versionAdded: '1.3.7', + function: () => { + if ( + !app.canvas.selected_nodes || + Object.keys(app.canvas.selected_nodes).length === 0 + ) { + useToastStore().add({ + severity: 'error', + summary: 'No nodes selected', + detail: 'Please select nodes to group', + life: 3000 + }) + return + } + const group = new LGraphGroup() + const padding = useSettingStore().get( + 'Comfy.GroupSelectedNodes.Padding' + ) + group.addNodes(Object.values(app.canvas.selected_nodes), padding) + app.canvas.graph.add(group) + useTitleEditorStore().titleEditorTarget = group + } } ] diff --git a/src/stores/coreKeybindings.ts b/src/stores/coreKeybindings.ts index 94bb1865a..f315d7f37 100644 --- a/src/stores/coreKeybindings.ts +++ b/src/stores/coreKeybindings.ts @@ -1,3 +1,86 @@ import type { Keybinding } from '@/types/keyBindingTypes' -export const CORE_KEYBINDINGS: Keybinding[] = [] +export const CORE_KEYBINDINGS: Keybinding[] = [ + { + combo: { + ctrl: true, + key: 'Enter' + }, + commandId: 'Comfy.QueuePrompt' + }, + { + combo: { + ctrl: true, + shift: true, + key: 'Enter' + }, + commandId: 'Comfy.QueuePromptFront' + }, + { + combo: { + ctrl: true, + alt: true, + key: 'Enter' + }, + commandId: 'Comfy.Interrupt' + }, + { + combo: { + key: 'r' + }, + commandId: 'Comfy.RefreshNodeDefinitions' + }, + { + combo: { + key: 'q' + }, + commandId: 'Comfy.ToggleQueueSidebarTab' + }, + { + combo: { + key: 'h' + }, + commandId: 'Comfy.ToggleQueueSidebarTab' + }, + { + combo: { + key: 's', + ctrl: true + }, + commandId: 'Comfy.ExportWorkflow' + }, + { + combo: { + key: 'o', + ctrl: true + }, + commandId: 'Comfy.OpenWorkflow' + }, + { + combo: { + key: 'Backspace' + }, + commandId: 'Comfy.ClearWorkflow' + }, + { + combo: { + key: 'd', + ctrl: true + }, + commandId: 'Comfy.LoadDefaultWorkflow' + }, + { + combo: { + key: 'g', + ctrl: true + }, + commandId: 'Comfy.Graph.GroupSelectedNodes' + }, + { + combo: { + key: ',', + ctrl: true + }, + commandId: 'Comfy.ShowSettingsDialog' + } +] diff --git a/src/stores/keybindingStore.ts b/src/stores/keybindingStore.ts index 7c42305ea..d7bb42e82 100644 --- a/src/stores/keybindingStore.ts +++ b/src/stores/keybindingStore.ts @@ -41,7 +41,7 @@ export class KeyComboImpl implements KeyCombo { static fromEvent(event: KeyboardEvent) { return new KeyComboImpl({ key: event.key, - ctrl: event.ctrlKey, + ctrl: event.ctrlKey || event.metaKey, alt: event.altKey, shift: event.shiftKey }) @@ -76,6 +76,14 @@ export class KeyComboImpl implements KeyCombo { toString(): string { return `${this.key} + ${this.ctrl ? 'Ctrl' : ''}${this.alt ? 'Alt' : ''}${this.shift ? 'Shift' : ''}` } + + get hasModifier(): boolean { + return this.ctrl || this.alt || this.shift + } + + get isModifier(): boolean { + return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key) + } } export const useKeybindingStore = defineStore('keybinding', () => { diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index 47b4a07ac..3d09ad1a4 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -45,6 +45,7 @@ import AppMenu from '@/components/appMenu/AppMenu.vue' import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue' import TopMenubar from '@/components/topbar/TopMenubar.vue' import { setupAutoQueueHandler } from '@/services/autoQueueService' +import { useKeybindingStore } from '@/stores/keybindingStore' setupAutoQueueHandler() @@ -104,6 +105,8 @@ watchEffect(() => { const init = () => { settingStore.addSettings(app.ui.settings) + useKeybindingStore().loadCoreKeybindings() + app.extensionManager = useWorkspaceStore() app.extensionManager.registerSidebarTab({ id: 'queue',