mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-01 11:10:00 +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:
@@ -5,7 +5,7 @@ import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { KeyCombo } from '../../src/platform/keybindings'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { ComfyActionbar } from '../helpers/actionbar'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
|
||||
import type { Keybinding } from '../../src/platform/keybindings'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -150,13 +150,11 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { KeyComboImpl } from '@/stores/keybindingStore'
|
||||
import type { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
|
||||
const { keyCombo, isModified = false } = defineProps<{
|
||||
keyCombo: KeyComboImpl
|
||||
|
||||
@@ -70,7 +70,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Keybinding } from '@/schemas/keyBindingSchema'
|
||||
import type { Keybinding } from './types'
|
||||
|
||||
export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
{
|
||||
@@ -76,7 +76,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
commandId: 'Comfy.ShowSettingsDialog'
|
||||
},
|
||||
// For '=' both holding shift and not holding shift
|
||||
{
|
||||
combo: {
|
||||
key: '=',
|
||||
@@ -94,7 +93,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetElementId: 'graph-canvas'
|
||||
},
|
||||
// For number pad '+'
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
86
src/platform/keybindings/keyCombo.ts
Normal file
86
src/platform/keybindings/keyCombo.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { toRaw } from 'vue'
|
||||
|
||||
import { RESERVED_BY_TEXT_INPUT } from './reserved'
|
||||
import type { KeyCombo } from './types'
|
||||
|
||||
export class KeyComboImpl implements KeyCombo {
|
||||
key: string
|
||||
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
|
||||
}
|
||||
}
|
||||
26
src/platform/keybindings/keybinding.ts
Normal file
26
src/platform/keybindings/keybinding.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { toRaw } from 'vue'
|
||||
|
||||
import { KeyComboImpl } from './keyCombo'
|
||||
import type { Keybinding } from './types'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
179
src/platform/keybindings/keybindingService.escape.test.ts
Normal file
179
src/platform/keybindings/keybindingService.escape.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CORE_KEYBINDINGS } from '@/platform/keybindings/defaults'
|
||||
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { DialogInstance } from '@/stores/dialogStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => ({
|
||||
useDialogStore: vi.fn(() => ({
|
||||
dialogStack: []
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: null
|
||||
}
|
||||
}))
|
||||
|
||||
describe('keybindingService - Escape key handling', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let mockCommandExecute: ReturnType<typeof useCommandStore>['execute']
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
mockCommandExecute = vi.fn()
|
||||
commandStore.execute = mockCommandExecute
|
||||
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [] as DialogInstance[]
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
function createKeyboardEvent(
|
||||
key: string,
|
||||
options: {
|
||||
ctrlKey?: boolean
|
||||
altKey?: boolean
|
||||
metaKey?: boolean
|
||||
shiftKey?: boolean
|
||||
} = {}
|
||||
): KeyboardEvent {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key,
|
||||
ctrlKey: options.ctrlKey ?? false,
|
||||
altKey: options.altKey ?? false,
|
||||
metaKey: options.metaKey ?? false,
|
||||
shiftKey: options.shiftKey ?? false,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
it('should execute Escape keybinding when no dialogs are open', async () => {
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [] as DialogInstance[]
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
const event = createKeyboardEvent('Escape')
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should NOT execute Escape keybinding when dialogs are open', async () => {
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [{ key: 'test-dialog' } as DialogInstance]
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
|
||||
const event = createKeyboardEvent('Escape')
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute Escape keybinding with modifiers regardless of dialog state', async () => {
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [{ key: 'test-dialog' } as DialogInstance]
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
const keybindingStore = useKeybindingStore()
|
||||
keybindingStore.addDefaultKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'Test.CtrlEscape',
|
||||
combo: { key: 'Escape', ctrl: true }
|
||||
})
|
||||
)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
|
||||
const event = createKeyboardEvent('Escape', { ctrlKey: true })
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Test.CtrlEscape')
|
||||
})
|
||||
|
||||
it('should verify Escape keybinding exists in CORE_KEYBINDINGS', () => {
|
||||
const escapeBinding = CORE_KEYBINDINGS.find(
|
||||
(kb) => kb.combo.key === 'Escape' && !kb.combo.ctrl && !kb.combo.alt
|
||||
)
|
||||
|
||||
expect(escapeBinding).toBeDefined()
|
||||
expect(escapeBinding?.commandId).toBe('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should create correct KeyComboImpl from Escape event', () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
shiftKey: false
|
||||
})
|
||||
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
|
||||
expect(keyCombo.key).toBe('Escape')
|
||||
expect(keyCombo.ctrl).toBe(false)
|
||||
expect(keyCombo.alt).toBe(false)
|
||||
expect(keyCombo.shift).toBe(false)
|
||||
})
|
||||
|
||||
it('should still close legacy modals on Escape when no keybinding matched', async () => {
|
||||
setActivePinia(createPinia())
|
||||
keybindingService = useKeybindingService()
|
||||
|
||||
const mockModal = document.createElement('div')
|
||||
mockModal.className = 'comfy-modal'
|
||||
mockModal.style.display = 'block'
|
||||
document.body.appendChild(mockModal)
|
||||
|
||||
const originalGetComputedStyle = window.getComputedStyle
|
||||
window.getComputedStyle = vi.fn().mockReturnValue({
|
||||
getPropertyValue: vi.fn().mockReturnValue('block')
|
||||
})
|
||||
|
||||
try {
|
||||
const event = createKeyboardEvent('Escape')
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockModal.style.display).toBe('none')
|
||||
} finally {
|
||||
document.body.removeChild(mockModal)
|
||||
window.getComputedStyle = originalGetComputedStyle
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2,12 +2,11 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
// Mock the app and canvas using factory functions
|
||||
vi.mock('@/scripts/app', () => {
|
||||
return {
|
||||
app: {
|
||||
@@ -18,7 +17,6 @@ vi.mock('@/scripts/app', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Mock stores
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
@@ -31,7 +29,6 @@ vi.mock('@/stores/dialogStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Test utility for creating keyboard events with mocked methods
|
||||
function createTestKeyboardEvent(
|
||||
key: string,
|
||||
options: {
|
||||
@@ -57,7 +54,6 @@ function createTestKeyboardEvent(
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
// Mock event methods
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [target])
|
||||
|
||||
@@ -71,11 +67,9 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
|
||||
// Mock command store execute
|
||||
const commandStore = useCommandStore()
|
||||
commandStore.execute = vi.fn()
|
||||
|
||||
// Reset dialog store mock to empty
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: []
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
@@ -91,9 +85,7 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
// Should forward to canvas processKey
|
||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||
// Should not execute any command
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -112,7 +104,6 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
// Should not forward to canvas when in input field
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -144,7 +135,6 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
})
|
||||
|
||||
it('should not forward Delete key when canvas is not available', async () => {
|
||||
// Temporarily set canvas to null
|
||||
const originalCanvas = vi.mocked(app).canvas
|
||||
vi.mocked(app).canvas = null!
|
||||
|
||||
@@ -164,7 +154,6 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
// Should not forward Enter key
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -174,7 +163,6 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
// Should not forward when modifiers are pressed
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,40 +1,35 @@
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
|
||||
export const useKeybindingService = () => {
|
||||
import { CORE_KEYBINDINGS } from './defaults'
|
||||
import { KeyComboImpl } from './keyCombo'
|
||||
import { KeybindingImpl } from './keybinding'
|
||||
import { useKeybindingStore } from './keybindingStore'
|
||||
|
||||
export function useKeybindingService() {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
// Helper function to determine if an event should be forwarded to canvas
|
||||
const shouldForwardToCanvas = (event: KeyboardEvent): boolean => {
|
||||
// Don't forward if modifier keys are pressed (except shift)
|
||||
function shouldForwardToCanvas(event: KeyboardEvent): boolean {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Keys that LiteGraph handles but aren't in core keybindings
|
||||
const canvasKeys = ['Delete', 'Backspace']
|
||||
|
||||
return canvasKeys.includes(event.key)
|
||||
}
|
||||
|
||||
const keybindHandler = async function (event: KeyboardEvent) {
|
||||
async function keybindHandler(event: KeyboardEvent) {
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
if (keyCombo.isModifier) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore reserved or non-modifier keybindings if typing in input fields
|
||||
const target = event.composedPath()[0] as HTMLElement
|
||||
if (
|
||||
keyCombo.isReservedByTextInput &&
|
||||
@@ -49,20 +44,17 @@ export const useKeybindingService = () => {
|
||||
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||
// Special handling for Escape key - let dialogs handle it first
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.metaKey
|
||||
) {
|
||||
// If dialogs are open, don't execute the keybinding - let the dialog handle it
|
||||
if (dialogStore.dialogStack.length > 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default browser behavior first, then execute the command
|
||||
event.preventDefault()
|
||||
const runCommandIds = new Set([
|
||||
'Comfy.QueuePrompt',
|
||||
@@ -81,7 +73,6 @@ export const useKeybindingService = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Forward unhandled canvas-targeted events to LiteGraph
|
||||
if (!keybinding && shouldForwardToCanvas(event)) {
|
||||
const canvas = app.canvas
|
||||
if (
|
||||
@@ -89,18 +80,15 @@ export const useKeybindingService = () => {
|
||||
canvas.processKey &&
|
||||
typeof canvas.processKey === 'function'
|
||||
) {
|
||||
// Let LiteGraph handle the event
|
||||
canvas.processKey(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Only clear dialogs if not using modifiers
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// Escape key: close the first open modal found, and all dialogs
|
||||
if (event.key === 'Escape') {
|
||||
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
|
||||
for (const modal of modals) {
|
||||
@@ -118,14 +106,13 @@ export const useKeybindingService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const registerCoreKeybindings = () => {
|
||||
function registerCoreKeybindings() {
|
||||
for (const keybinding of CORE_KEYBINDINGS) {
|
||||
keybindingStore.addDefaultKeybinding(new KeybindingImpl(keybinding))
|
||||
}
|
||||
}
|
||||
|
||||
function registerUserKeybindings() {
|
||||
// Unset bindings first as new bindings might conflict with default bindings.
|
||||
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
|
||||
for (const keybinding of unsetBindings) {
|
||||
keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding))
|
||||
@@ -137,8 +124,6 @@ export const useKeybindingService = () => {
|
||||
}
|
||||
|
||||
async function persistUserKeybindings() {
|
||||
// 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(keybindingStore.getUserKeybindings())
|
||||
@@ -2,7 +2,8 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
|
||||
describe('useKeybindingStore', () => {
|
||||
beforeEach(() => {
|
||||
@@ -176,18 +177,14 @@ describe('useKeybindingStore', () => {
|
||||
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
|
||||
@@ -200,10 +197,7 @@ describe('useKeybindingStore', () => {
|
||||
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)
|
||||
@@ -211,10 +205,6 @@ describe('useKeybindingStore', () => {
|
||||
})
|
||||
|
||||
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 = [
|
||||
@@ -391,18 +381,15 @@ describe('useKeybindingStore', () => {
|
||||
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 }
|
||||
@@ -413,7 +400,6 @@ describe('useKeybindingStore', () => {
|
||||
userKeybinding
|
||||
)
|
||||
|
||||
// Reset keybinding to default
|
||||
const result = store.resetKeybindingForCommand('test.command')
|
||||
|
||||
expect(result).toBe(true)
|
||||
@@ -1,140 +1,20 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { groupBy } from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, toRaw } from 'vue'
|
||||
import { computed, ref } 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
|
||||
}
|
||||
}
|
||||
import type { KeyComboImpl } from './keyCombo'
|
||||
import type { KeybindingImpl } from './keybinding'
|
||||
|
||||
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
|
||||
}
|
||||
@@ -167,7 +47,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
|
||||
const keybindingsByCommandId = computed<Record<string, KeybindingImpl[]>>(
|
||||
() => {
|
||||
return _.groupBy(keybindings.value, 'commandId')
|
||||
return groupBy(keybindings.value, 'commandId')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -178,26 +58,13 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
const defaultKeybindingsByCommandId = computed<
|
||||
Record<string, KeybindingImpl[]>
|
||||
>(() => {
|
||||
return _.groupBy(Object.values(defaultKeybindings.value), 'commandId')
|
||||
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,
|
||||
@@ -223,7 +90,6 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
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)
|
||||
@@ -232,7 +98,6 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Unset keybinding on default keybinding if it exists and is not the same as userUnsetKeybinding
|
||||
if (defaultKeybinding && !defaultKeybinding.equals(userUnsetKeybinding)) {
|
||||
unsetKeybinding(defaultKeybinding)
|
||||
}
|
||||
@@ -262,11 +127,6 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
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)) {
|
||||
@@ -284,18 +144,11 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
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)
|
||||
@@ -304,17 +157,14 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
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)
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// KeyCombo schema
|
||||
const zKeyCombo = z.object({
|
||||
key: z.string(),
|
||||
ctrl: z.boolean().optional(),
|
||||
@@ -9,17 +8,11 @@ const zKeyCombo = z.object({
|
||||
meta: z.boolean().optional()
|
||||
})
|
||||
|
||||
// Keybinding schema
|
||||
export const zKeybinding = z.object({
|
||||
commandId: z.string(),
|
||||
combo: zKeyCombo,
|
||||
// Optional target element ID to limit keybinding to.
|
||||
// Note: Currently only used to distinguish between global keybindings
|
||||
// and litegraph canvas keybindings.
|
||||
// Do NOT use this field in extensions as it has no effect.
|
||||
targetElementId: z.string().optional()
|
||||
})
|
||||
|
||||
// Infer types from schemas
|
||||
export type KeyCombo = z.infer<typeof zKeyCombo>
|
||||
export type Keybinding = z.infer<typeof zKeybinding>
|
||||
@@ -3,7 +3,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
|
||||
import type { Keybinding } from '@/schemas/keyBindingSchema'
|
||||
import type { Keybinding } from '@/platform/keybindings/types'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
import { breakpointsTailwind } from '@vueuse/core'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
||||
import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph'
|
||||
import { zNodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { colorPalettesSchema } from '@/schemas/colorPaletteSchema'
|
||||
import { zKeybinding } from '@/schemas/keyBindingSchema'
|
||||
import { zKeybinding } from '@/platform/keybindings/types'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||
|
||||
|
||||
@@ -59,7 +59,8 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
|
||||
@@ -71,7 +71,6 @@ The following table lists ALL services in the system as of 2025-09-01:
|
||||
| customerEventsService.ts | Handles customer event tracking and audit logs | Analytics |
|
||||
| dialogService.ts | Provides dialog and modal management | UI |
|
||||
| extensionService.ts | Manages extension registration and lifecycle | Extensions |
|
||||
| keybindingService.ts | Handles keyboard shortcuts and keybindings | Input |
|
||||
| litegraphService.ts | Provides utilities for working with the LiteGraph library | Graph |
|
||||
| load3dService.ts | Manages 3D model loading and visualization | 3D |
|
||||
| mediaCacheService.ts | Manages media file caching with blob storage and cleanup | Media |
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { DialogInstance } from '@/stores/dialogStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
|
||||
// Mock stores
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/dialogStore', () => ({
|
||||
useDialogStore: vi.fn(() => ({
|
||||
dialogStack: []
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('keybindingService - Escape key handling', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let mockCommandExecute: ReturnType<typeof useCommandStore>['execute']
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
|
||||
// Mock command store execute
|
||||
const commandStore = useCommandStore()
|
||||
mockCommandExecute = vi.fn()
|
||||
commandStore.execute = mockCommandExecute
|
||||
|
||||
// Reset dialog store mock to empty - only mock the properties we need for testing
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [],
|
||||
// Add other required properties as undefined/default values to satisfy the type
|
||||
// but they won't be used in these tests
|
||||
...({} as Omit<ReturnType<typeof useDialogStore>, 'dialogStack'>)
|
||||
})
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
it('should register Escape key for ExitSubgraph command', () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
|
||||
// Check that the Escape keybinding exists in core keybindings
|
||||
const escapeKeybinding = CORE_KEYBINDINGS.find(
|
||||
(kb) =>
|
||||
kb.combo.key === 'Escape' && kb.commandId === 'Comfy.Graph.ExitSubgraph'
|
||||
)
|
||||
expect(escapeKeybinding).toBeDefined()
|
||||
|
||||
// Check that it was registered in the store
|
||||
const registeredBinding = keybindingStore.getKeybinding(
|
||||
new KeyComboImpl({ key: 'Escape' })
|
||||
)
|
||||
expect(registeredBinding).toBeDefined()
|
||||
expect(registeredBinding?.commandId).toBe('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should execute ExitSubgraph command when Escape is pressed', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
// Mock event methods
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Comfy.Graph.ExitSubgraph')
|
||||
})
|
||||
|
||||
it('should not execute command when Escape is pressed with modifiers', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not execute command when typing in input field', async () => {
|
||||
const inputElement = document.createElement('input')
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [inputElement])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close dialogs when no keybinding is registered', async () => {
|
||||
// Remove the Escape keybinding
|
||||
const keybindingStore = useKeybindingStore()
|
||||
keybindingStore.unsetKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'Comfy.Graph.ExitSubgraph',
|
||||
combo: { key: 'Escape' }
|
||||
})
|
||||
)
|
||||
|
||||
// Create a mock dialog
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.open = true
|
||||
dialog.close = vi.fn()
|
||||
document.body.appendChild(dialog)
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(dialog.close).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(dialog)
|
||||
})
|
||||
|
||||
it('should allow user to rebind Escape key to different command', async () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
|
||||
// Add a user keybinding for Escape to a different command
|
||||
keybindingStore.addUserKeybinding(
|
||||
new KeybindingImpl({
|
||||
commandId: 'Custom.Command',
|
||||
combo: { key: 'Escape' }
|
||||
})
|
||||
)
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockCommandExecute).toHaveBeenCalledWith('Custom.Command')
|
||||
expect(mockCommandExecute).not.toHaveBeenCalledWith(
|
||||
'Comfy.Graph.ExitSubgraph'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not execute Escape keybinding when dialogs are open', async () => {
|
||||
// Mock dialog store to have open dialogs
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [{ key: 'test-dialog' } as DialogInstance],
|
||||
// Add other required properties as undefined/default values to satisfy the type
|
||||
...({} as Omit<ReturnType<typeof useDialogStore>, 'dialogStack'>)
|
||||
})
|
||||
|
||||
// Re-create keybinding service to pick up new mock
|
||||
keybindingService = useKeybindingService()
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
event.preventDefault = vi.fn()
|
||||
event.composedPath = vi.fn(() => [document.body])
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
// Should not call preventDefault or execute command
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { Keybinding } from '@/schemas/keyBindingSchema'
|
||||
import type { Keybinding } from '@/platform/keybindings/types'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
|
||||
@@ -62,7 +62,7 @@ import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { setupAutoQueueHandler } from '@/services/autoQueueService'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
Reference in New Issue
Block a user