mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 22:34:15 +00:00
Move keybinds to coreKeybindings (#1078)
* Refactor core keybinds * Prevent default * Add playwright test
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
37
browser_tests/keybindings.spec.ts
Normal file
37
browser_tests/keybindings.spec.ts
Normal file
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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<HTMLElement | null>(null)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user