mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +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 { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
|
||||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
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 type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||||
import { ComfyActionbar } from '../helpers/actionbar'
|
import { ComfyActionbar } from '../helpers/actionbar'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Locator } from '@playwright/test'
|
import type { Locator } from '@playwright/test'
|
||||||
import { expect } 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'
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
|||||||
@@ -150,13 +150,11 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
import SearchBox from '@/components/common/SearchBox.vue'
|
import SearchBox from '@/components/common/SearchBox.vue'
|
||||||
import Button from '@/components/ui/button/Button.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 { useCommandStore } from '@/stores/commandStore'
|
||||||
import {
|
|
||||||
KeyComboImpl,
|
|
||||||
KeybindingImpl,
|
|
||||||
useKeybindingStore
|
|
||||||
} from '@/stores/keybindingStore'
|
|
||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
|
|
||||||
import PanelTemplate from './PanelTemplate.vue'
|
import PanelTemplate from './PanelTemplate.vue'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import type { KeyComboImpl } from '@/stores/keybindingStore'
|
import type { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||||
|
|
||||||
const { keyCombo, isModified = false } = defineProps<{
|
const { keyCombo, isModified = false } = defineProps<{
|
||||||
keyCombo: KeyComboImpl
|
keyCombo: KeyComboImpl
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
|||||||
import { useTelemetry } from '@/platform/telemetry'
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||||
import { useUserStore } from '@/stores/userStore'
|
import { useUserStore } from '@/stores/userStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
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[] = [
|
export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||||
{
|
{
|
||||||
@@ -76,7 +76,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
|||||||
},
|
},
|
||||||
commandId: 'Comfy.ShowSettingsDialog'
|
commandId: 'Comfy.ShowSettingsDialog'
|
||||||
},
|
},
|
||||||
// For '=' both holding shift and not holding shift
|
|
||||||
{
|
{
|
||||||
combo: {
|
combo: {
|
||||||
key: '=',
|
key: '=',
|
||||||
@@ -94,7 +93,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
|||||||
commandId: 'Comfy.Canvas.ZoomIn',
|
commandId: 'Comfy.Canvas.ZoomIn',
|
||||||
targetElementId: 'graph-canvas'
|
targetElementId: 'graph-canvas'
|
||||||
},
|
},
|
||||||
// For number pad '+'
|
|
||||||
{
|
{
|
||||||
combo: {
|
combo: {
|
||||||
key: '+',
|
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 { setActivePinia } from 'pinia'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useKeybindingService } from '@/services/keybindingService'
|
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
|
||||||
// Mock the app and canvas using factory functions
|
|
||||||
vi.mock('@/scripts/app', () => {
|
vi.mock('@/scripts/app', () => {
|
||||||
return {
|
return {
|
||||||
app: {
|
app: {
|
||||||
@@ -18,7 +17,6 @@ vi.mock('@/scripts/app', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock stores
|
|
||||||
vi.mock('@/platform/settings/settingStore', () => ({
|
vi.mock('@/platform/settings/settingStore', () => ({
|
||||||
useSettingStore: vi.fn(() => ({
|
useSettingStore: vi.fn(() => ({
|
||||||
get: vi.fn(() => [])
|
get: vi.fn(() => [])
|
||||||
@@ -31,7 +29,6 @@ vi.mock('@/stores/dialogStore', () => ({
|
|||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Test utility for creating keyboard events with mocked methods
|
|
||||||
function createTestKeyboardEvent(
|
function createTestKeyboardEvent(
|
||||||
key: string,
|
key: string,
|
||||||
options: {
|
options: {
|
||||||
@@ -57,7 +54,6 @@ function createTestKeyboardEvent(
|
|||||||
cancelable: true
|
cancelable: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock event methods
|
|
||||||
event.preventDefault = vi.fn()
|
event.preventDefault = vi.fn()
|
||||||
event.composedPath = vi.fn(() => [target])
|
event.composedPath = vi.fn(() => [target])
|
||||||
|
|
||||||
@@ -71,11 +67,9 @@ describe('keybindingService - Event Forwarding', () => {
|
|||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||||
|
|
||||||
// Mock command store execute
|
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
commandStore.execute = vi.fn()
|
commandStore.execute = vi.fn()
|
||||||
|
|
||||||
// Reset dialog store mock to empty
|
|
||||||
vi.mocked(useDialogStore).mockReturnValue({
|
vi.mocked(useDialogStore).mockReturnValue({
|
||||||
dialogStack: []
|
dialogStack: []
|
||||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||||
@@ -91,9 +85,7 @@ describe('keybindingService - Event Forwarding', () => {
|
|||||||
|
|
||||||
await keybindingService.keybindHandler(event)
|
await keybindingService.keybindHandler(event)
|
||||||
|
|
||||||
// Should forward to canvas processKey
|
|
||||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||||
// Should not execute any command
|
|
||||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -112,7 +104,6 @@ describe('keybindingService - Event Forwarding', () => {
|
|||||||
|
|
||||||
await keybindingService.keybindHandler(event)
|
await keybindingService.keybindHandler(event)
|
||||||
|
|
||||||
// Should not forward to canvas when in input field
|
|
||||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||||
expect(vi.mocked(useCommandStore().execute)).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 () => {
|
it('should not forward Delete key when canvas is not available', async () => {
|
||||||
// Temporarily set canvas to null
|
|
||||||
const originalCanvas = vi.mocked(app).canvas
|
const originalCanvas = vi.mocked(app).canvas
|
||||||
vi.mocked(app).canvas = null!
|
vi.mocked(app).canvas = null!
|
||||||
|
|
||||||
@@ -164,7 +154,6 @@ describe('keybindingService - Event Forwarding', () => {
|
|||||||
|
|
||||||
await keybindingService.keybindHandler(event)
|
await keybindingService.keybindHandler(event)
|
||||||
|
|
||||||
// Should not forward Enter key
|
|
||||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -174,7 +163,6 @@ describe('keybindingService - Event Forwarding', () => {
|
|||||||
|
|
||||||
await keybindingService.keybindHandler(event)
|
await keybindingService.keybindHandler(event)
|
||||||
|
|
||||||
// Should not forward when modifiers are pressed
|
|
||||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||||
expect(vi.mocked(useCommandStore().execute)).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 { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
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 keybindingStore = useKeybindingStore()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
// Helper function to determine if an event should be forwarded to canvas
|
function shouldForwardToCanvas(event: KeyboardEvent): boolean {
|
||||||
const shouldForwardToCanvas = (event: KeyboardEvent): boolean => {
|
|
||||||
// Don't forward if modifier keys are pressed (except shift)
|
|
||||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys that LiteGraph handles but aren't in core keybindings
|
|
||||||
const canvasKeys = ['Delete', 'Backspace']
|
const canvasKeys = ['Delete', 'Backspace']
|
||||||
|
|
||||||
return canvasKeys.includes(event.key)
|
return canvasKeys.includes(event.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keybindHandler = async function (event: KeyboardEvent) {
|
async function keybindHandler(event: KeyboardEvent) {
|
||||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||||
if (keyCombo.isModifier) {
|
if (keyCombo.isModifier) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore reserved or non-modifier keybindings if typing in input fields
|
|
||||||
const target = event.composedPath()[0] as HTMLElement
|
const target = event.composedPath()[0] as HTMLElement
|
||||||
if (
|
if (
|
||||||
keyCombo.isReservedByTextInput &&
|
keyCombo.isReservedByTextInput &&
|
||||||
@@ -49,20 +44,17 @@ export const useKeybindingService = () => {
|
|||||||
|
|
||||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||||
// Special handling for Escape key - let dialogs handle it first
|
|
||||||
if (
|
if (
|
||||||
event.key === 'Escape' &&
|
event.key === 'Escape' &&
|
||||||
!event.ctrlKey &&
|
!event.ctrlKey &&
|
||||||
!event.altKey &&
|
!event.altKey &&
|
||||||
!event.metaKey
|
!event.metaKey
|
||||||
) {
|
) {
|
||||||
// If dialogs are open, don't execute the keybinding - let the dialog handle it
|
|
||||||
if (dialogStore.dialogStack.length > 0) {
|
if (dialogStore.dialogStack.length > 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent default browser behavior first, then execute the command
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const runCommandIds = new Set([
|
const runCommandIds = new Set([
|
||||||
'Comfy.QueuePrompt',
|
'Comfy.QueuePrompt',
|
||||||
@@ -81,7 +73,6 @@ export const useKeybindingService = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward unhandled canvas-targeted events to LiteGraph
|
|
||||||
if (!keybinding && shouldForwardToCanvas(event)) {
|
if (!keybinding && shouldForwardToCanvas(event)) {
|
||||||
const canvas = app.canvas
|
const canvas = app.canvas
|
||||||
if (
|
if (
|
||||||
@@ -89,18 +80,15 @@ export const useKeybindingService = () => {
|
|||||||
canvas.processKey &&
|
canvas.processKey &&
|
||||||
typeof canvas.processKey === 'function'
|
typeof canvas.processKey === 'function'
|
||||||
) {
|
) {
|
||||||
// Let LiteGraph handle the event
|
|
||||||
canvas.processKey(event)
|
canvas.processKey(event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only clear dialogs if not using modifiers
|
|
||||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escape key: close the first open modal found, and all dialogs
|
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
|
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
|
||||||
for (const modal of modals) {
|
for (const modal of modals) {
|
||||||
@@ -118,14 +106,13 @@ export const useKeybindingService = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerCoreKeybindings = () => {
|
function registerCoreKeybindings() {
|
||||||
for (const keybinding of CORE_KEYBINDINGS) {
|
for (const keybinding of CORE_KEYBINDINGS) {
|
||||||
keybindingStore.addDefaultKeybinding(new KeybindingImpl(keybinding))
|
keybindingStore.addDefaultKeybinding(new KeybindingImpl(keybinding))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerUserKeybindings() {
|
function registerUserKeybindings() {
|
||||||
// Unset bindings first as new bindings might conflict with default bindings.
|
|
||||||
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
|
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
|
||||||
for (const keybinding of unsetBindings) {
|
for (const keybinding of unsetBindings) {
|
||||||
keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding))
|
keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding))
|
||||||
@@ -137,8 +124,6 @@ export const useKeybindingService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function persistUserKeybindings() {
|
async function persistUserKeybindings() {
|
||||||
// TODO(https://github.com/Comfy-Org/ComfyUI_frontend/issues/1079):
|
|
||||||
// Allow setting multiple values at once in settingStore
|
|
||||||
await settingStore.set(
|
await settingStore.set(
|
||||||
'Comfy.Keybinding.NewBindings',
|
'Comfy.Keybinding.NewBindings',
|
||||||
Object.values(keybindingStore.getUserKeybindings())
|
Object.values(keybindingStore.getUserKeybindings())
|
||||||
@@ -2,7 +2,8 @@ import { createTestingPinia } from '@pinia/testing'
|
|||||||
import { setActivePinia } from 'pinia'
|
import { setActivePinia } from 'pinia'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
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', () => {
|
describe('useKeybindingStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -176,18 +177,14 @@ describe('useKeybindingStore', () => {
|
|||||||
combo: { key: 'I', ctrl: true }
|
combo: { key: 'I', ctrl: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add default keybinding
|
|
||||||
store.addDefaultKeybinding(defaultKeybinding)
|
store.addDefaultKeybinding(defaultKeybinding)
|
||||||
expect(store.keybindings).toHaveLength(1)
|
expect(store.keybindings).toHaveLength(1)
|
||||||
|
|
||||||
// Unset the default keybinding
|
|
||||||
store.unsetKeybinding(defaultKeybinding)
|
store.unsetKeybinding(defaultKeybinding)
|
||||||
expect(store.keybindings).toHaveLength(0)
|
expect(store.keybindings).toHaveLength(0)
|
||||||
|
|
||||||
// Add the same keybinding as a user keybinding
|
|
||||||
store.addUserKeybinding(defaultKeybinding)
|
store.addUserKeybinding(defaultKeybinding)
|
||||||
|
|
||||||
// Check that the keybinding is back and not in the unset list
|
|
||||||
expect(store.keybindings).toHaveLength(1)
|
expect(store.keybindings).toHaveLength(1)
|
||||||
expect(store.getKeybinding(defaultKeybinding.combo)).toEqual(
|
expect(store.getKeybinding(defaultKeybinding.combo)).toEqual(
|
||||||
defaultKeybinding
|
defaultKeybinding
|
||||||
@@ -200,10 +197,7 @@ describe('useKeybindingStore', () => {
|
|||||||
commandId: 'test.command',
|
commandId: 'test.command',
|
||||||
combo: { key: 'J', ctrl: true }
|
combo: { key: 'J', ctrl: true }
|
||||||
})
|
})
|
||||||
// Add default keybinding.
|
|
||||||
// This can happen when we change default keybindings.
|
|
||||||
store.addDefaultKeybinding(keybinding)
|
store.addDefaultKeybinding(keybinding)
|
||||||
// Add user keybinding.
|
|
||||||
store.addUserKeybinding(keybinding)
|
store.addUserKeybinding(keybinding)
|
||||||
|
|
||||||
expect(store.keybindings).toHaveLength(1)
|
expect(store.keybindings).toHaveLength(1)
|
||||||
@@ -211,10 +205,6 @@ describe('useKeybindingStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should keep previously customized keybindings after default keybindings change', () => {
|
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 store = useKeybindingStore()
|
||||||
|
|
||||||
const userUnsetKeybindings = [
|
const userUnsetKeybindings = [
|
||||||
@@ -391,18 +381,15 @@ describe('useKeybindingStore', () => {
|
|||||||
it('should handle complex scenario with both unset and user keybindings', () => {
|
it('should handle complex scenario with both unset and user keybindings', () => {
|
||||||
const store = useKeybindingStore()
|
const store = useKeybindingStore()
|
||||||
|
|
||||||
// Create default keybinding
|
|
||||||
const defaultKeybinding = new KeybindingImpl({
|
const defaultKeybinding = new KeybindingImpl({
|
||||||
commandId: 'test.command',
|
commandId: 'test.command',
|
||||||
combo: { key: 'Q', ctrl: true }
|
combo: { key: 'Q', ctrl: true }
|
||||||
})
|
})
|
||||||
store.addDefaultKeybinding(defaultKeybinding)
|
store.addDefaultKeybinding(defaultKeybinding)
|
||||||
|
|
||||||
// Unset default keybinding
|
|
||||||
store.unsetKeybinding(defaultKeybinding)
|
store.unsetKeybinding(defaultKeybinding)
|
||||||
expect(store.keybindings).toHaveLength(0)
|
expect(store.keybindings).toHaveLength(0)
|
||||||
|
|
||||||
// Add user keybinding with different combo
|
|
||||||
const userKeybinding = new KeybindingImpl({
|
const userKeybinding = new KeybindingImpl({
|
||||||
commandId: 'test.command',
|
commandId: 'test.command',
|
||||||
combo: { key: 'R', alt: true }
|
combo: { key: 'R', alt: true }
|
||||||
@@ -413,7 +400,6 @@ describe('useKeybindingStore', () => {
|
|||||||
userKeybinding
|
userKeybinding
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset keybinding to default
|
|
||||||
const result = store.resetKeybindingForCommand('test.command')
|
const result = store.resetKeybindingForCommand('test.command')
|
||||||
|
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
@@ -1,140 +1,20 @@
|
|||||||
import _ from 'es-toolkit/compat'
|
import { groupBy } from 'es-toolkit/compat'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { Ref } from 'vue'
|
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 { KeyComboImpl } from './keyCombo'
|
||||||
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
|
import type { KeybindingImpl } from './keybinding'
|
||||||
|
|
||||||
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', () => {
|
export const useKeybindingStore = defineStore('keybinding', () => {
|
||||||
/**
|
|
||||||
* Default keybindings provided by core and extensions.
|
|
||||||
*/
|
|
||||||
const defaultKeybindings = ref<Record<string, KeybindingImpl>>({})
|
const defaultKeybindings = ref<Record<string, KeybindingImpl>>({})
|
||||||
/**
|
|
||||||
* User-defined keybindings.
|
|
||||||
*/
|
|
||||||
const userKeybindings = ref<Record<string, KeybindingImpl>>({})
|
const userKeybindings = ref<Record<string, KeybindingImpl>>({})
|
||||||
/**
|
|
||||||
* User-defined keybindings that unset default keybindings.
|
|
||||||
*/
|
|
||||||
const userUnsetKeybindings = ref<Record<string, KeybindingImpl>>({})
|
const userUnsetKeybindings = ref<Record<string, KeybindingImpl>>({})
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user-defined keybindings.
|
|
||||||
*/
|
|
||||||
function getUserKeybindings() {
|
function getUserKeybindings() {
|
||||||
return userKeybindings.value
|
return userKeybindings.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user-defined keybindings that unset default keybindings.
|
|
||||||
*/
|
|
||||||
function getUserUnsetKeybindings() {
|
function getUserUnsetKeybindings() {
|
||||||
return userUnsetKeybindings.value
|
return userUnsetKeybindings.value
|
||||||
}
|
}
|
||||||
@@ -167,7 +47,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
|
|
||||||
const keybindingsByCommandId = computed<Record<string, KeybindingImpl[]>>(
|
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<
|
const defaultKeybindingsByCommandId = computed<
|
||||||
Record<string, KeybindingImpl[]>
|
Record<string, KeybindingImpl[]>
|
||||||
>(() => {
|
>(() => {
|
||||||
return _.groupBy(Object.values(defaultKeybindings.value), 'commandId')
|
return groupBy(Object.values(defaultKeybindings.value), 'commandId')
|
||||||
})
|
})
|
||||||
|
|
||||||
function getKeybindingByCommandId(commandId: string) {
|
function getKeybindingByCommandId(commandId: string) {
|
||||||
return getKeybindingsByCommandId(commandId)[0]
|
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(
|
function addKeybinding(
|
||||||
target: Ref<Record<string, KeybindingImpl>>,
|
target: Ref<Record<string, KeybindingImpl>>,
|
||||||
keybinding: KeybindingImpl,
|
keybinding: KeybindingImpl,
|
||||||
@@ -223,7 +90,6 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
const userUnsetKeybinding =
|
const userUnsetKeybinding =
|
||||||
userUnsetKeybindings.value[keybinding.combo.serialize()]
|
userUnsetKeybindings.value[keybinding.combo.serialize()]
|
||||||
|
|
||||||
// User is adding back a keybinding that was an unsetted default keybinding.
|
|
||||||
if (
|
if (
|
||||||
keybinding.equals(defaultKeybinding) &&
|
keybinding.equals(defaultKeybinding) &&
|
||||||
keybinding.equals(userUnsetKeybinding)
|
keybinding.equals(userUnsetKeybinding)
|
||||||
@@ -232,7 +98,6 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unset keybinding on default keybinding if it exists and is not the same as userUnsetKeybinding
|
|
||||||
if (defaultKeybinding && !defaultKeybinding.equals(userUnsetKeybinding)) {
|
if (defaultKeybinding && !defaultKeybinding.equals(userUnsetKeybinding)) {
|
||||||
unsetKeybinding(defaultKeybinding)
|
unsetKeybinding(defaultKeybinding)
|
||||||
}
|
}
|
||||||
@@ -262,11 +127,6 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
console.warn(`Unset unknown keybinding: ${JSON.stringify(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 {
|
function updateKeybindingOnCommand(keybinding: KeybindingImpl): boolean {
|
||||||
const currentKeybinding = getKeybindingByCommandId(keybinding.commandId)
|
const currentKeybinding = getKeybindingByCommandId(keybinding.commandId)
|
||||||
if (currentKeybinding?.equals(keybinding)) {
|
if (currentKeybinding?.equals(keybinding)) {
|
||||||
@@ -284,18 +144,11 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
userUnsetKeybindings.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 {
|
function resetKeybindingForCommand(commandId: string): boolean {
|
||||||
const currentKeybinding = getKeybindingByCommandId(commandId)
|
const currentKeybinding = getKeybindingByCommandId(commandId)
|
||||||
const defaultKeybinding =
|
const defaultKeybinding =
|
||||||
defaultKeybindingsByCommandId.value[commandId]?.[0]
|
defaultKeybindingsByCommandId.value[commandId]?.[0]
|
||||||
|
|
||||||
// No default keybinding exists, need to remove any user binding
|
|
||||||
if (!defaultKeybinding) {
|
if (!defaultKeybinding) {
|
||||||
if (currentKeybinding) {
|
if (currentKeybinding) {
|
||||||
unsetKeybinding(currentKeybinding)
|
unsetKeybinding(currentKeybinding)
|
||||||
@@ -304,17 +157,14 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current binding equals default binding, no changes needed
|
|
||||||
if (currentKeybinding?.equals(defaultKeybinding)) {
|
if (currentKeybinding?.equals(defaultKeybinding)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unset current keybinding if exists
|
|
||||||
if (currentKeybinding) {
|
if (currentKeybinding) {
|
||||||
unsetKeybinding(currentKeybinding)
|
unsetKeybinding(currentKeybinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the unset record if it exists
|
|
||||||
const serializedCombo = defaultKeybinding.combo.serialize()
|
const serializedCombo = defaultKeybinding.combo.serialize()
|
||||||
if (
|
if (
|
||||||
userUnsetKeybindings.value[serializedCombo]?.equals(defaultKeybinding)
|
userUnsetKeybindings.value[serializedCombo]?.equals(defaultKeybinding)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
// KeyCombo schema
|
|
||||||
const zKeyCombo = z.object({
|
const zKeyCombo = z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
ctrl: z.boolean().optional(),
|
ctrl: z.boolean().optional(),
|
||||||
@@ -9,17 +8,11 @@ const zKeyCombo = z.object({
|
|||||||
meta: z.boolean().optional()
|
meta: z.boolean().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Keybinding schema
|
|
||||||
export const zKeybinding = z.object({
|
export const zKeybinding = z.object({
|
||||||
commandId: z.string(),
|
commandId: z.string(),
|
||||||
combo: zKeyCombo,
|
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()
|
targetElementId: z.string().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Infer types from schemas
|
|
||||||
export type KeyCombo = z.infer<typeof zKeyCombo>
|
export type KeyCombo = z.infer<typeof zKeyCombo>
|
||||||
export type Keybinding = z.infer<typeof zKeybinding>
|
export type Keybinding = z.infer<typeof zKeybinding>
|
||||||
@@ -3,7 +3,7 @@ import { isCloud } from '@/platform/distribution/types'
|
|||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import type { SettingParams } from '@/platform/settings/types'
|
import type { SettingParams } from '@/platform/settings/types'
|
||||||
import type { ColorPalettes } from '@/schemas/colorPaletteSchema'
|
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 { NodeBadgeMode } from '@/types/nodeSource'
|
||||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||||
import { breakpointsTailwind } from '@vueuse/core'
|
import { breakpointsTailwind } from '@vueuse/core'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
|||||||
import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph'
|
import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph'
|
||||||
import { zNodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
import { zNodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
import { colorPalettesSchema } from '@/schemas/colorPaletteSchema'
|
import { colorPalettesSchema } from '@/schemas/colorPaletteSchema'
|
||||||
import { zKeybinding } from '@/schemas/keyBindingSchema'
|
import { zKeybinding } from '@/platform/keybindings/types'
|
||||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||||
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ import { useExecutionStore } from '@/stores/executionStore'
|
|||||||
import { useExtensionStore } from '@/stores/extensionStore'
|
import { useExtensionStore } from '@/stores/extensionStore'
|
||||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
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 { useModelStore } from '@/stores/modelStore'
|
||||||
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
|
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
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 |
|
| customerEventsService.ts | Handles customer event tracking and audit logs | Analytics |
|
||||||
| dialogService.ts | Provides dialog and modal management | UI |
|
| dialogService.ts | Provides dialog and modal management | UI |
|
||||||
| extensionService.ts | Manages extension registration and lifecycle | Extensions |
|
| 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 |
|
| litegraphService.ts | Provides utilities for working with the LiteGraph library | Graph |
|
||||||
| load3dService.ts | Manages 3D model loading and visualization | 3D |
|
| load3dService.ts | Manages 3D model loading and visualization | 3D |
|
||||||
| mediaCacheService.ts | Manages media file caching with blob storage and cleanup | Media |
|
| 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 { api } from '@/scripts/api'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useExtensionStore } from '@/stores/extensionStore'
|
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 { useMenuItemStore } from '@/stores/menuItemStore'
|
||||||
import { useWidgetStore } from '@/stores/widgetStore'
|
import { useWidgetStore } from '@/stores/widgetStore'
|
||||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
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 |
|
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
|
||||||
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
|
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
|
||||||
| imagePreviewStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
|
| 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 |
|
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
|
||||||
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
|
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
|
||||||
| modelStore.ts | useModelStore | Manages AI models information | Models |
|
| modelStore.ts | useModelStore | Manages AI models information | Models |
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { defineStore } from 'pinia'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
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 type { ComfyExtension } from '@/types/comfy'
|
||||||
|
|
||||||
import { useKeybindingStore } from './keybindingStore'
|
|
||||||
import type { KeybindingImpl } from './keybindingStore'
|
|
||||||
|
|
||||||
export interface ComfyCommand {
|
export interface ComfyCommand {
|
||||||
id: string
|
id: string
|
||||||
function: (metadata?: Record<string, unknown>) => void | Promise<void>
|
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 { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import type { SettingParams } from '@/platform/settings/types'
|
import type { SettingParams } from '@/platform/settings/types'
|
||||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
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 { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||||
import type { ComfyApp } from '@/scripts/app'
|
import type { ComfyApp } from '@/scripts/app'
|
||||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
|
|||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { setupAutoQueueHandler } from '@/services/autoQueueService'
|
import { setupAutoQueueHandler } from '@/services/autoQueueService'
|
||||||
import { useKeybindingService } from '@/services/keybindingService'
|
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||||
import { useAssetsStore } from '@/stores/assetsStore'
|
import { useAssetsStore } from '@/stores/assetsStore'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
|
|||||||
Reference in New Issue
Block a user