mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Compare commits
8 Commits
test/queue
...
5252-keybo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6606060802 | ||
|
|
06ec45c1c5 | ||
|
|
8c05ac8eb6 | ||
|
|
692666ff63 | ||
|
|
82a603ef03 | ||
|
|
14255b3512 | ||
|
|
e5adc840fe | ||
|
|
0f44b5ea58 |
1039
docs/keybinding.md
Normal file
1039
docs/keybinding.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -237,7 +237,7 @@ async function removeKeybinding(commandData: ICommandData) {
|
||||
async function captureKeybinding(event: KeyboardEvent) {
|
||||
// Allow the use of keyboard shortcuts when adding keyboard shortcuts
|
||||
if (!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
switch (event.key) {
|
||||
switch (event.code) {
|
||||
case 'Escape':
|
||||
cancelEdit()
|
||||
return
|
||||
|
||||
@@ -26,58 +26,58 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'r'
|
||||
key: 'KeyR'
|
||||
},
|
||||
commandId: 'Comfy.RefreshNodeDefinitions'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'q'
|
||||
key: 'KeyQ'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.queue'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'w'
|
||||
key: 'KeyW'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.workflows'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'n'
|
||||
key: 'KeyN'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.node-library'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm'
|
||||
key: 'KeyM'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.model-library'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 's',
|
||||
key: 'KeyS',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.SaveWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'o',
|
||||
key: 'KeyO',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.OpenWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'g',
|
||||
key: 'KeyG',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Graph.GroupSelectedNodes'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: ',',
|
||||
key: 'Comma',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.ShowSettingsDialog'
|
||||
@@ -85,7 +85,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
// For '=' both holding shift and not holding shift
|
||||
{
|
||||
combo: {
|
||||
key: '=',
|
||||
key: 'Equal',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
@@ -93,7 +93,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
key: 'Equal',
|
||||
alt: true,
|
||||
shift: true
|
||||
},
|
||||
@@ -103,7 +103,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
// For number pad '+'
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
key: 'NumpadAdd',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
@@ -111,7 +111,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '-',
|
||||
key: 'Minus',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomOut',
|
||||
@@ -119,21 +119,21 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '.'
|
||||
key: 'Period'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.FitView',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'p'
|
||||
key: 'KeyP'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelected.Pin',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'c',
|
||||
key: 'KeyC',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
|
||||
@@ -141,7 +141,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'b',
|
||||
key: 'KeyB',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
|
||||
@@ -149,7 +149,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm',
|
||||
key: 'KeyM',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
||||
@@ -157,20 +157,20 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '`',
|
||||
key: 'Backquote',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanelTab.logs-terminal'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'f'
|
||||
key: 'KeyF'
|
||||
},
|
||||
commandId: 'Workspace.ToggleFocusMode'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'e',
|
||||
key: 'KeyE',
|
||||
ctrl: true,
|
||||
shift: true
|
||||
},
|
||||
@@ -178,7 +178,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm',
|
||||
key: 'KeyM',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleMinimap'
|
||||
@@ -187,19 +187,19 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
combo: {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
key: 'k'
|
||||
key: 'KeyK'
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'v'
|
||||
key: 'KeyV'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.Unlock'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'h'
|
||||
key: 'KeyH'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.Lock'
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
import { migrateKeybindings } from '@/utils/keybindingMigration'
|
||||
|
||||
export const useKeybindingService = () => {
|
||||
const keybindingStore = useKeybindingStore()
|
||||
@@ -51,7 +52,7 @@ export const useKeybindingService = () => {
|
||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||
// Special handling for Escape key - let dialogs handle it first
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
event.code === 'Escape' &&
|
||||
!event.ctrlKey &&
|
||||
!event.altKey &&
|
||||
!event.metaKey
|
||||
@@ -88,7 +89,7 @@ export const useKeybindingService = () => {
|
||||
}
|
||||
|
||||
// Escape key: close the first open modal found, and all dialogs
|
||||
if (event.key === 'Escape') {
|
||||
if (event.code === 'Escape') {
|
||||
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
|
||||
for (const modal of modals) {
|
||||
const modalDisplay = window
|
||||
@@ -111,14 +112,41 @@ export const useKeybindingService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function registerUserKeybindings() {
|
||||
// Unset bindings first as new bindings might conflict with default bindings.
|
||||
async function registerUserKeybindings() {
|
||||
// Load user keybindings from settings
|
||||
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
|
||||
for (const keybinding of unsetBindings) {
|
||||
const newBindings = settingStore.get('Comfy.Keybinding.NewBindings')
|
||||
|
||||
// Migrate keybindings from old event.key format to new event.code format
|
||||
const migratedUnset = migrateKeybindings(unsetBindings)
|
||||
const migratedNew = migrateKeybindings(newBindings)
|
||||
|
||||
// Save migrated keybindings back to settings if any migration occurred
|
||||
if (migratedUnset.migrated) {
|
||||
await settingStore.set(
|
||||
'Comfy.Keybinding.UnsetBindings',
|
||||
migratedUnset.keybindings
|
||||
)
|
||||
console.warn(
|
||||
'[Keybindings] Migrated unset keybindings to event.code format'
|
||||
)
|
||||
}
|
||||
|
||||
if (migratedNew.migrated) {
|
||||
await settingStore.set(
|
||||
'Comfy.Keybinding.NewBindings',
|
||||
migratedNew.keybindings
|
||||
)
|
||||
console.warn(
|
||||
'[Keybindings] Migrated custom keybindings to event.code format'
|
||||
)
|
||||
}
|
||||
|
||||
// Unset bindings first as new bindings might conflict with default bindings.
|
||||
for (const keybinding of migratedUnset.keybindings) {
|
||||
keybindingStore.unsetKeybinding(new KeybindingImpl(keybinding))
|
||||
}
|
||||
const newBindings = settingStore.get('Comfy.Keybinding.NewBindings')
|
||||
for (const keybinding of newBindings) {
|
||||
for (const keybinding of migratedNew.keybindings) {
|
||||
keybindingStore.addUserKeybinding(new KeybindingImpl(keybinding))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class KeyComboImpl implements KeyCombo {
|
||||
|
||||
static fromEvent(event: KeyboardEvent) {
|
||||
return new KeyComboImpl({
|
||||
key: event.key,
|
||||
key: event.code,
|
||||
ctrl: event.ctrlKey || event.metaKey,
|
||||
alt: event.altKey,
|
||||
shift: event.shiftKey
|
||||
@@ -75,7 +75,16 @@ export class KeyComboImpl implements KeyCombo {
|
||||
}
|
||||
|
||||
get isModifier(): boolean {
|
||||
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
|
||||
return [
|
||||
'ControlLeft',
|
||||
'ControlRight',
|
||||
'MetaLeft',
|
||||
'MetaRight',
|
||||
'AltLeft',
|
||||
'AltRight',
|
||||
'ShiftLeft',
|
||||
'ShiftRight'
|
||||
].includes(this.key)
|
||||
}
|
||||
|
||||
get modifierCount(): number {
|
||||
@@ -106,9 +115,95 @@ export class KeyComboImpl implements KeyCombo {
|
||||
if (this.shift) {
|
||||
sequences.push('Shift')
|
||||
}
|
||||
sequences.push(this.key)
|
||||
sequences.push(this.getDisplayKey())
|
||||
return sequences
|
||||
}
|
||||
|
||||
getDisplayKey(): string {
|
||||
// Convert key codes to display names
|
||||
const keyMap: Record<string, string> = {
|
||||
// Letters
|
||||
KeyA: 'A',
|
||||
KeyB: 'B',
|
||||
KeyC: 'C',
|
||||
KeyD: 'D',
|
||||
KeyE: 'E',
|
||||
KeyF: 'F',
|
||||
KeyG: 'G',
|
||||
KeyH: 'H',
|
||||
KeyI: 'I',
|
||||
KeyJ: 'J',
|
||||
KeyK: 'K',
|
||||
KeyL: 'L',
|
||||
KeyM: 'M',
|
||||
KeyN: 'N',
|
||||
KeyO: 'O',
|
||||
KeyP: 'P',
|
||||
KeyQ: 'Q',
|
||||
KeyR: 'R',
|
||||
KeyS: 'S',
|
||||
KeyT: 'T',
|
||||
KeyU: 'U',
|
||||
KeyV: 'V',
|
||||
KeyW: 'W',
|
||||
KeyX: 'X',
|
||||
KeyY: 'Y',
|
||||
KeyZ: 'Z',
|
||||
// Numbers
|
||||
Digit0: '0',
|
||||
Digit1: '1',
|
||||
Digit2: '2',
|
||||
Digit3: '3',
|
||||
Digit4: '4',
|
||||
Digit5: '5',
|
||||
Digit6: '6',
|
||||
Digit7: '7',
|
||||
Digit8: '8',
|
||||
Digit9: '9',
|
||||
// Function keys
|
||||
F1: 'F1',
|
||||
F2: 'F2',
|
||||
F3: 'F3',
|
||||
F4: 'F4',
|
||||
F5: 'F5',
|
||||
F6: 'F6',
|
||||
F7: 'F7',
|
||||
F8: 'F8',
|
||||
F9: 'F9',
|
||||
F10: 'F10',
|
||||
F11: 'F11',
|
||||
F12: 'F12',
|
||||
// Special keys
|
||||
Space: 'Space',
|
||||
Enter: 'Enter',
|
||||
Tab: 'Tab',
|
||||
Escape: 'Escape',
|
||||
Backspace: 'Backspace',
|
||||
Delete: 'Delete',
|
||||
ArrowUp: '↑',
|
||||
ArrowDown: '↓',
|
||||
ArrowLeft: '←',
|
||||
ArrowRight: '→',
|
||||
Home: 'Home',
|
||||
End: 'End',
|
||||
PageUp: 'PageUp',
|
||||
PageDown: 'PageDown',
|
||||
Insert: 'Insert',
|
||||
// Punctuation
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Backslash: '\\',
|
||||
Semicolon: ';',
|
||||
Quote: "'",
|
||||
Backquote: '`',
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/'
|
||||
}
|
||||
return keyMap[this.key] || this.key
|
||||
}
|
||||
}
|
||||
|
||||
export const useKeybindingStore = defineStore('keybinding', () => {
|
||||
|
||||
263
src/utils/keybindingMigration.ts
Normal file
263
src/utils/keybindingMigration.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
|
||||
|
||||
/**
|
||||
* Migration utility for converting old event.key format to new event.code format
|
||||
* This ensures backward compatibility for existing user keybindings
|
||||
*/
|
||||
|
||||
// Map from old event.key format to new event.code format
|
||||
const KEY_MIGRATION_MAP: Record<string, string> = {
|
||||
// Letters (both cases)
|
||||
a: 'KeyA',
|
||||
A: 'KeyA',
|
||||
b: 'KeyB',
|
||||
B: 'KeyB',
|
||||
c: 'KeyC',
|
||||
C: 'KeyC',
|
||||
d: 'KeyD',
|
||||
D: 'KeyD',
|
||||
e: 'KeyE',
|
||||
E: 'KeyE',
|
||||
f: 'KeyF',
|
||||
F: 'KeyF',
|
||||
g: 'KeyG',
|
||||
G: 'KeyG',
|
||||
h: 'KeyH',
|
||||
H: 'KeyH',
|
||||
i: 'KeyI',
|
||||
I: 'KeyI',
|
||||
j: 'KeyJ',
|
||||
J: 'KeyJ',
|
||||
k: 'KeyK',
|
||||
K: 'KeyK',
|
||||
l: 'KeyL',
|
||||
L: 'KeyL',
|
||||
m: 'KeyM',
|
||||
M: 'KeyM',
|
||||
n: 'KeyN',
|
||||
N: 'KeyN',
|
||||
o: 'KeyO',
|
||||
O: 'KeyO',
|
||||
p: 'KeyP',
|
||||
P: 'KeyP',
|
||||
q: 'KeyQ',
|
||||
Q: 'KeyQ',
|
||||
r: 'KeyR',
|
||||
R: 'KeyR',
|
||||
s: 'KeyS',
|
||||
S: 'KeyS',
|
||||
t: 'KeyT',
|
||||
T: 'KeyT',
|
||||
u: 'KeyU',
|
||||
U: 'KeyU',
|
||||
v: 'KeyV',
|
||||
V: 'KeyV',
|
||||
w: 'KeyW',
|
||||
W: 'KeyW',
|
||||
x: 'KeyX',
|
||||
X: 'KeyX',
|
||||
y: 'KeyY',
|
||||
Y: 'KeyY',
|
||||
z: 'KeyZ',
|
||||
Z: 'KeyZ',
|
||||
|
||||
// Numbers
|
||||
'0': 'Digit0',
|
||||
'1': 'Digit1',
|
||||
'2': 'Digit2',
|
||||
'3': 'Digit3',
|
||||
'4': 'Digit4',
|
||||
'5': 'Digit5',
|
||||
'6': 'Digit6',
|
||||
'7': 'Digit7',
|
||||
'8': 'Digit8',
|
||||
'9': 'Digit9',
|
||||
|
||||
// Special keys that might be in old format
|
||||
escape: 'Escape',
|
||||
enter: 'Enter',
|
||||
space: 'Space',
|
||||
tab: 'Tab',
|
||||
spacebar: 'Space',
|
||||
Spacebar: 'Space',
|
||||
esc: 'Escape',
|
||||
Esc: 'Escape',
|
||||
return: 'Enter',
|
||||
Return: 'Enter',
|
||||
backspace: 'Backspace',
|
||||
delete: 'Delete',
|
||||
|
||||
// Arrow keys
|
||||
arrowup: 'ArrowUp',
|
||||
arrowdown: 'ArrowDown',
|
||||
arrowleft: 'ArrowLeft',
|
||||
arrowright: 'ArrowRight',
|
||||
|
||||
// Function keys (already correct format but handle lowercase)
|
||||
f1: 'F1',
|
||||
f2: 'F2',
|
||||
f3: 'F3',
|
||||
f4: 'F4',
|
||||
f5: 'F5',
|
||||
f6: 'F6',
|
||||
f7: 'F7',
|
||||
f8: 'F8',
|
||||
f9: 'F9',
|
||||
f10: 'F10',
|
||||
f11: 'F11',
|
||||
f12: 'F12',
|
||||
|
||||
// Punctuation and symbols (old name -> new code)
|
||||
'-': 'Minus',
|
||||
'=': 'Equal',
|
||||
'[': 'BracketLeft',
|
||||
']': 'BracketRight',
|
||||
'\\': 'Backslash',
|
||||
';': 'Semicolon',
|
||||
"'": 'Quote',
|
||||
'`': 'Backquote',
|
||||
',': 'Comma',
|
||||
'.': 'Period',
|
||||
'/': 'Slash',
|
||||
_: 'Minus',
|
||||
'+': 'Equal',
|
||||
'{': 'BracketLeft',
|
||||
'}': 'BracketRight',
|
||||
'|': 'Backslash',
|
||||
':': 'Semicolon',
|
||||
'"': 'Quote',
|
||||
'~': 'Backquote',
|
||||
'<': 'Comma',
|
||||
'>': 'Period',
|
||||
'?': 'Slash',
|
||||
|
||||
// Shifted digits
|
||||
'!': 'Digit1',
|
||||
'@': 'Digit2',
|
||||
'#': 'Digit3',
|
||||
$: 'Digit4',
|
||||
'%': 'Digit5',
|
||||
'^': 'Digit6',
|
||||
'&': 'Digit7',
|
||||
'*': 'Digit8',
|
||||
'(': 'Digit9',
|
||||
')': 'Digit0',
|
||||
|
||||
// Common aliases
|
||||
' ': 'Space'
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key combo needs migration from old format to new format
|
||||
*/
|
||||
export function needsKeyMigration(keyCombo: KeyCombo): boolean {
|
||||
if (!keyCombo.key) return false
|
||||
|
||||
// Check if it's already in the new format
|
||||
if (
|
||||
keyCombo.key.startsWith('Key') ||
|
||||
keyCombo.key.startsWith('Digit') ||
|
||||
(keyCombo.key.startsWith('F') && /^F\d+$/.test(keyCombo.key)) ||
|
||||
[
|
||||
'Enter',
|
||||
'Escape',
|
||||
'Tab',
|
||||
'Space',
|
||||
'Backspace',
|
||||
'Delete',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'Minus',
|
||||
'Equal',
|
||||
'BracketLeft',
|
||||
'BracketRight',
|
||||
'Backslash',
|
||||
'Semicolon',
|
||||
'Quote',
|
||||
'Backquote',
|
||||
'Comma',
|
||||
'Period',
|
||||
'Slash',
|
||||
'NumpadAdd',
|
||||
'NumpadSubtract',
|
||||
'NumpadMultiply',
|
||||
'NumpadDivide'
|
||||
].includes(keyCombo.key)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If it's in our migration map, it needs migration
|
||||
return keyCombo.key in KEY_MIGRATION_MAP
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a single key combo from old format to new format
|
||||
*/
|
||||
export function migrateKeyCombo(keyCombo: KeyCombo): KeyCombo {
|
||||
if (!needsKeyMigration(keyCombo)) {
|
||||
return keyCombo
|
||||
}
|
||||
|
||||
const newKey = KEY_MIGRATION_MAP[keyCombo.key]
|
||||
if (!newKey) {
|
||||
console.warn(`Unknown key format for migration: ${keyCombo.key}`)
|
||||
return keyCombo
|
||||
}
|
||||
|
||||
return {
|
||||
...keyCombo,
|
||||
key: newKey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a single keybinding
|
||||
*/
|
||||
export function migrateKeybinding(keybinding: Keybinding): Keybinding {
|
||||
return {
|
||||
...keybinding,
|
||||
combo: migrateKeyCombo(keybinding.combo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates an array of keybindings and returns both the migrated keybindings
|
||||
* and whether any migration was performed
|
||||
*/
|
||||
export function migrateKeybindings(keybindings: Keybinding[] | undefined): {
|
||||
keybindings: Keybinding[]
|
||||
migrated: boolean
|
||||
} {
|
||||
if (!Array.isArray(keybindings)) {
|
||||
return {
|
||||
keybindings: [],
|
||||
migrated: false
|
||||
}
|
||||
}
|
||||
|
||||
let migrated = false
|
||||
const migratedKeybindings = keybindings.map((keybinding) => {
|
||||
const migratedKeybinding = migrateKeybinding(keybinding)
|
||||
if (migratedKeybinding.combo.key !== keybinding.combo.key) {
|
||||
migrated = true
|
||||
}
|
||||
return migratedKeybinding
|
||||
})
|
||||
|
||||
return {
|
||||
keybindings: migratedKeybindings,
|
||||
migrated
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a key to the event.code format
|
||||
* This handles both old and new formats
|
||||
*/
|
||||
export function normalizeKey(key: string): string {
|
||||
const migrated = migrateKeyCombo({ key } as KeyCombo)
|
||||
return migrated.key
|
||||
}
|
||||
@@ -11,10 +11,14 @@ import {
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
|
||||
const settingStoreGetMock = vi.fn()
|
||||
const settingStoreSetMock = vi.fn()
|
||||
|
||||
// Mock stores
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
get: settingStoreGetMock,
|
||||
set: settingStoreSetMock
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -32,6 +36,9 @@ describe('keybindingService - Escape key handling', () => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
settingStoreGetMock.mockImplementation(() => [])
|
||||
settingStoreSetMock.mockResolvedValue(undefined)
|
||||
|
||||
// Mock command store execute
|
||||
mockCommandExecute = vi.fn()
|
||||
const commandStore = useCommandStore()
|
||||
@@ -67,6 +74,7 @@ describe('keybindingService - Escape key handling', () => {
|
||||
it('should execute ExitSubgraph command when Escape is pressed', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
@@ -84,6 +92,7 @@ describe('keybindingService - Escape key handling', () => {
|
||||
it('should not execute command when Escape is pressed with modifiers', async () => {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
@@ -101,6 +110,7 @@ describe('keybindingService - Escape key handling', () => {
|
||||
const inputElement = document.createElement('input')
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
@@ -131,6 +141,7 @@ describe('keybindingService - Escape key handling', () => {
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
@@ -159,6 +170,7 @@ describe('keybindingService - Escape key handling', () => {
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
code: 'Escape',
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
@@ -200,3 +212,108 @@ describe('keybindingService - Escape key handling', () => {
|
||||
expect(mockCommandExecute).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('keybindingService - migration support', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let keybindingStore: ReturnType<typeof useKeybindingStore>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
|
||||
settingStoreSetMock.mockResolvedValue(undefined)
|
||||
settingStoreGetMock.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Keybinding.UnsetBindings') {
|
||||
return []
|
||||
}
|
||||
if (key === 'Comfy.Keybinding.NewBindings') {
|
||||
return []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
keybindingStore = useKeybindingStore()
|
||||
})
|
||||
|
||||
it('migrates legacy unset bindings using default combos', async () => {
|
||||
// Legacy format used lowercase letters
|
||||
// User wants to unset the 'R' shortcut (Comfy.RefreshNodeDefinitions)
|
||||
const legacyUnset = [
|
||||
{
|
||||
commandId: 'Comfy.RefreshNodeDefinitions',
|
||||
combo: { key: 'r' } // Old format
|
||||
}
|
||||
]
|
||||
|
||||
settingStoreGetMock.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Keybinding.UnsetBindings') {
|
||||
return legacyUnset
|
||||
}
|
||||
if (key === 'Comfy.Keybinding.NewBindings') {
|
||||
return []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
await keybindingService.registerUserKeybindings()
|
||||
|
||||
const unsetBindings = Object.values(
|
||||
keybindingStore.getUserUnsetKeybindings()
|
||||
)
|
||||
|
||||
// Should have migrated and unset the binding
|
||||
expect(unsetBindings).toHaveLength(1)
|
||||
expect(unsetBindings[0].combo.key).toBe('KeyR')
|
||||
expect(unsetBindings[0].commandId).toBe('Comfy.RefreshNodeDefinitions')
|
||||
|
||||
// Should have saved the migrated format
|
||||
expect(settingStoreSetMock).toHaveBeenCalledWith(
|
||||
'Comfy.Keybinding.UnsetBindings',
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
commandId: 'Comfy.RefreshNodeDefinitions',
|
||||
combo: expect.objectContaining({
|
||||
key: 'KeyR'
|
||||
})
|
||||
})
|
||||
])
|
||||
)
|
||||
|
||||
// Verify the keybinding no longer matches
|
||||
const eventCombo = new KeyComboImpl({ key: 'KeyR' })
|
||||
const resolved = keybindingStore.getKeybinding(eventCombo)
|
||||
expect(resolved).toBeUndefined()
|
||||
})
|
||||
|
||||
it('matches migrated event.code against legacy user bindings', async () => {
|
||||
// User has a legacy binding in old format
|
||||
const legacyBindings = [
|
||||
{
|
||||
commandId: 'Custom.Legacy',
|
||||
combo: { key: 'q' }
|
||||
}
|
||||
]
|
||||
|
||||
settingStoreGetMock.mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Keybinding.UnsetBindings') {
|
||||
return []
|
||||
}
|
||||
if (key === 'Comfy.Keybinding.NewBindings') {
|
||||
return legacyBindings
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// Register user keybindings (which will migrate them)
|
||||
await keybindingService.registerUserKeybindings()
|
||||
|
||||
// Now press 'Q' key with event.code format
|
||||
const eventCombo = new KeyComboImpl({ key: 'KeyQ' })
|
||||
|
||||
const resolved = keybindingStore.getKeybinding(eventCombo)
|
||||
|
||||
expect(resolved?.commandId).toBe('Custom.Legacy')
|
||||
})
|
||||
})
|
||||
|
||||
285
tests-ui/tests/utils/keybindingMigration.test.ts
Normal file
285
tests-ui/tests/utils/keybindingMigration.test.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { KeyCombo, Keybinding } from '@/schemas/keyBindingSchema'
|
||||
import {
|
||||
migrateKeyCombo,
|
||||
migrateKeybinding,
|
||||
migrateKeybindings,
|
||||
needsKeyMigration,
|
||||
normalizeKey
|
||||
} from '@/utils/keybindingMigration'
|
||||
|
||||
describe('keybindingMigration', () => {
|
||||
describe('needsKeyMigration', () => {
|
||||
it('should return false for keys already in event.code format', () => {
|
||||
expect(needsKeyMigration({ key: 'KeyA' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'KeyZ' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'Digit0' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'Digit9' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'F1' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'F12' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'Enter' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'Escape' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'ArrowUp' })).toBe(false)
|
||||
expect(needsKeyMigration({ key: 'Minus' })).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true for keys in old event.key format', () => {
|
||||
expect(needsKeyMigration({ key: 'a' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: 'z' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: 'A' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: 'Z' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: '0' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: '9' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: '-' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: '=' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle lowercase special keys', () => {
|
||||
expect(needsKeyMigration({ key: 'escape' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: 'enter' })).toBe(true)
|
||||
expect(needsKeyMigration({ key: 'space' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for empty key', () => {
|
||||
expect(needsKeyMigration({ key: '' })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('migrateKeyCombo', () => {
|
||||
it('should migrate lowercase letters to KeyX format', () => {
|
||||
expect(migrateKeyCombo({ key: 'a' }).key).toBe('KeyA')
|
||||
expect(migrateKeyCombo({ key: 'r' }).key).toBe('KeyR')
|
||||
expect(migrateKeyCombo({ key: 'z' }).key).toBe('KeyZ')
|
||||
})
|
||||
|
||||
it('should migrate uppercase letters to KeyX format', () => {
|
||||
expect(migrateKeyCombo({ key: 'A' }).key).toBe('KeyA')
|
||||
expect(migrateKeyCombo({ key: 'R' }).key).toBe('KeyR')
|
||||
expect(migrateKeyCombo({ key: 'Z' }).key).toBe('KeyZ')
|
||||
})
|
||||
|
||||
it('should migrate digit characters to DigitX format', () => {
|
||||
expect(migrateKeyCombo({ key: '0' }).key).toBe('Digit0')
|
||||
expect(migrateKeyCombo({ key: '5' }).key).toBe('Digit5')
|
||||
expect(migrateKeyCombo({ key: '9' }).key).toBe('Digit9')
|
||||
})
|
||||
|
||||
it('should migrate special keys to proper case', () => {
|
||||
expect(migrateKeyCombo({ key: 'escape' }).key).toBe('Escape')
|
||||
expect(migrateKeyCombo({ key: 'enter' }).key).toBe('Enter')
|
||||
expect(migrateKeyCombo({ key: 'space' }).key).toBe('Space')
|
||||
expect(migrateKeyCombo({ key: 'tab' }).key).toBe('Tab')
|
||||
})
|
||||
|
||||
it('should migrate punctuation to event.code names', () => {
|
||||
expect(migrateKeyCombo({ key: '-' }).key).toBe('Minus')
|
||||
expect(migrateKeyCombo({ key: '=' }).key).toBe('Equal')
|
||||
expect(migrateKeyCombo({ key: '[' }).key).toBe('BracketLeft')
|
||||
expect(migrateKeyCombo({ key: ']' }).key).toBe('BracketRight')
|
||||
expect(migrateKeyCombo({ key: ';' }).key).toBe('Semicolon')
|
||||
expect(migrateKeyCombo({ key: '/' }).key).toBe('Slash')
|
||||
})
|
||||
|
||||
it('should migrate shifted punctuation correctly', () => {
|
||||
expect(migrateKeyCombo({ key: '_' }).key).toBe('Minus')
|
||||
expect(migrateKeyCombo({ key: '+' }).key).toBe('Equal')
|
||||
expect(migrateKeyCombo({ key: '{' }).key).toBe('BracketLeft')
|
||||
expect(migrateKeyCombo({ key: '}' }).key).toBe('BracketRight')
|
||||
expect(migrateKeyCombo({ key: ':' }).key).toBe('Semicolon')
|
||||
expect(migrateKeyCombo({ key: '?' }).key).toBe('Slash')
|
||||
})
|
||||
|
||||
it('should migrate shifted digits correctly', () => {
|
||||
expect(migrateKeyCombo({ key: '!' }).key).toBe('Digit1')
|
||||
expect(migrateKeyCombo({ key: '@' }).key).toBe('Digit2')
|
||||
expect(migrateKeyCombo({ key: '#' }).key).toBe('Digit3')
|
||||
expect(migrateKeyCombo({ key: '$' }).key).toBe('Digit4')
|
||||
expect(migrateKeyCombo({ key: '%' }).key).toBe('Digit5')
|
||||
expect(migrateKeyCombo({ key: '^' }).key).toBe('Digit6')
|
||||
expect(migrateKeyCombo({ key: '&' }).key).toBe('Digit7')
|
||||
expect(migrateKeyCombo({ key: '*' }).key).toBe('Digit8')
|
||||
expect(migrateKeyCombo({ key: '(' }).key).toBe('Digit9')
|
||||
expect(migrateKeyCombo({ key: ')' }).key).toBe('Digit0')
|
||||
})
|
||||
|
||||
it('should preserve modifier flags', () => {
|
||||
const combo: KeyCombo = {
|
||||
key: 's',
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
shift: true
|
||||
}
|
||||
const migrated = migrateKeyCombo(combo)
|
||||
|
||||
expect(migrated.key).toBe('KeyS')
|
||||
expect(migrated.ctrl).toBe(true)
|
||||
expect(migrated.alt).toBe(true)
|
||||
expect(migrated.shift).toBe(true)
|
||||
})
|
||||
|
||||
it('should not modify keys already in event.code format', () => {
|
||||
expect(migrateKeyCombo({ key: 'KeyR' }).key).toBe('KeyR')
|
||||
expect(migrateKeyCombo({ key: 'Digit5' }).key).toBe('Digit5')
|
||||
expect(migrateKeyCombo({ key: 'F1' }).key).toBe('F1')
|
||||
expect(migrateKeyCombo({ key: 'Enter' }).key).toBe('Enter')
|
||||
expect(migrateKeyCombo({ key: 'ArrowUp' }).key).toBe('ArrowUp')
|
||||
})
|
||||
|
||||
it('should handle space character', () => {
|
||||
expect(migrateKeyCombo({ key: ' ' }).key).toBe('Space')
|
||||
})
|
||||
|
||||
it('should handle arrow key variations', () => {
|
||||
expect(migrateKeyCombo({ key: 'arrowup' }).key).toBe('ArrowUp')
|
||||
expect(migrateKeyCombo({ key: 'arrowdown' }).key).toBe('ArrowDown')
|
||||
expect(migrateKeyCombo({ key: 'arrowleft' }).key).toBe('ArrowLeft')
|
||||
expect(migrateKeyCombo({ key: 'arrowright' }).key).toBe('ArrowRight')
|
||||
})
|
||||
|
||||
it('should handle function key variations', () => {
|
||||
expect(migrateKeyCombo({ key: 'f1' }).key).toBe('F1')
|
||||
expect(migrateKeyCombo({ key: 'f12' }).key).toBe('F12')
|
||||
})
|
||||
})
|
||||
|
||||
describe('migrateKeybinding', () => {
|
||||
it('should migrate a keybinding object', () => {
|
||||
const keybinding: Keybinding = {
|
||||
commandId: 'Test.Command',
|
||||
combo: { key: 'r' }
|
||||
}
|
||||
|
||||
const migrated = migrateKeybinding(keybinding)
|
||||
|
||||
expect(migrated.commandId).toBe('Test.Command')
|
||||
expect(migrated.combo.key).toBe('KeyR')
|
||||
})
|
||||
|
||||
it('should preserve targetElementId', () => {
|
||||
const keybinding: Keybinding = {
|
||||
commandId: 'Test.Command',
|
||||
combo: { key: 's', ctrl: true },
|
||||
targetElementId: 'graph-canvas'
|
||||
}
|
||||
|
||||
const migrated = migrateKeybinding(keybinding)
|
||||
|
||||
expect(migrated.targetElementId).toBe('graph-canvas')
|
||||
expect(migrated.combo.key).toBe('KeyS')
|
||||
expect(migrated.combo.ctrl).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('migrateKeybindings', () => {
|
||||
it('should migrate an array of keybindings', () => {
|
||||
const keybindings: Keybinding[] = [
|
||||
{ commandId: 'Test1', combo: { key: 'r' } },
|
||||
{ commandId: 'Test2', combo: { key: 's', ctrl: true } },
|
||||
{ commandId: 'Test3', combo: { key: 'q' } }
|
||||
]
|
||||
|
||||
const result = migrateKeybindings(keybindings)
|
||||
|
||||
expect(result.migrated).toBe(true)
|
||||
expect(result.keybindings).toHaveLength(3)
|
||||
expect(result.keybindings[0].combo.key).toBe('KeyR')
|
||||
expect(result.keybindings[1].combo.key).toBe('KeyS')
|
||||
expect(result.keybindings[2].combo.key).toBe('KeyQ')
|
||||
})
|
||||
|
||||
it('should detect when no migration is needed', () => {
|
||||
const keybindings: Keybinding[] = [
|
||||
{ commandId: 'Test1', combo: { key: 'KeyR' } },
|
||||
{ commandId: 'Test2', combo: { key: 'KeyS', ctrl: true } }
|
||||
]
|
||||
|
||||
const result = migrateKeybindings(keybindings)
|
||||
|
||||
expect(result.migrated).toBe(false)
|
||||
expect(result.keybindings).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should handle mixed old and new formats', () => {
|
||||
const keybindings: Keybinding[] = [
|
||||
{ commandId: 'Test1', combo: { key: 'r' } }, // Old format
|
||||
{ commandId: 'Test2', combo: { key: 'KeyS' } }, // New format
|
||||
{ commandId: 'Test3', combo: { key: 'q' } } // Old format
|
||||
]
|
||||
|
||||
const result = migrateKeybindings(keybindings)
|
||||
|
||||
expect(result.migrated).toBe(true)
|
||||
expect(result.keybindings[0].combo.key).toBe('KeyR')
|
||||
expect(result.keybindings[1].combo.key).toBe('KeyS')
|
||||
expect(result.keybindings[2].combo.key).toBe('KeyQ')
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const result = migrateKeybindings([])
|
||||
|
||||
expect(result.migrated).toBe(false)
|
||||
expect(result.keybindings).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle undefined input', () => {
|
||||
const result = migrateKeybindings(undefined)
|
||||
|
||||
expect(result.migrated).toBe(false)
|
||||
expect(result.keybindings).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeKey', () => {
|
||||
it('should normalize old format keys', () => {
|
||||
expect(normalizeKey('r')).toBe('KeyR')
|
||||
expect(normalizeKey('s')).toBe('KeyS')
|
||||
expect(normalizeKey('5')).toBe('Digit5')
|
||||
expect(normalizeKey('-')).toBe('Minus')
|
||||
})
|
||||
|
||||
it('should leave new format keys unchanged', () => {
|
||||
expect(normalizeKey('KeyR')).toBe('KeyR')
|
||||
expect(normalizeKey('Digit5')).toBe('Digit5')
|
||||
expect(normalizeKey('Enter')).toBe('Enter')
|
||||
})
|
||||
})
|
||||
|
||||
describe('real-world migration scenarios', () => {
|
||||
it('should migrate common shortcuts', () => {
|
||||
const commonShortcuts: Keybinding[] = [
|
||||
// Refresh nodes
|
||||
{ commandId: 'Comfy.RefreshNodeDefinitions', combo: { key: 'r' } },
|
||||
// Save
|
||||
{ commandId: 'Comfy.SaveWorkflow', combo: { key: 's', ctrl: true } },
|
||||
// Queue prompt
|
||||
{ commandId: 'Comfy.QueuePrompt', combo: { key: 'Enter', ctrl: true } },
|
||||
// Toggle sidebar
|
||||
{ commandId: 'Workspace.ToggleSidebar', combo: { key: 'q' } }
|
||||
]
|
||||
|
||||
const result = migrateKeybindings(commonShortcuts)
|
||||
|
||||
expect(result.migrated).toBe(true)
|
||||
expect(result.keybindings[0].combo.key).toBe('KeyR')
|
||||
expect(result.keybindings[1].combo.key).toBe('KeyS')
|
||||
expect(result.keybindings[2].combo.key).toBe('Enter')
|
||||
expect(result.keybindings[3].combo.key).toBe('KeyQ')
|
||||
})
|
||||
|
||||
it('should handle keybindings with punctuation', () => {
|
||||
const punctuationShortcuts: Keybinding[] = [
|
||||
{ commandId: 'Zoom.In', combo: { key: '=', alt: true } },
|
||||
{ commandId: 'Zoom.Out', combo: { key: '-', alt: true } },
|
||||
{ commandId: 'Search', combo: { key: '/', ctrl: true } }
|
||||
]
|
||||
|
||||
const result = migrateKeybindings(punctuationShortcuts)
|
||||
|
||||
expect(result.migrated).toBe(true)
|
||||
expect(result.keybindings[0].combo.key).toBe('Equal')
|
||||
expect(result.keybindings[1].combo.key).toBe('Minus')
|
||||
expect(result.keybindings[2].combo.key).toBe('Slash')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user