initial implementation, copy from pr #1820 + migration + comments

This commit is contained in:
Tristan Sommer
2024-12-09 15:20:30 +01:00
parent 0b91d53c9f
commit 44482e017a
14 changed files with 469 additions and 265 deletions

View File

@@ -5,6 +5,10 @@
v-model="filters['global'].value"
:placeholder="$t('g.searchKeybindings') + '...'"
/>
<KeyContextSelect
:contexts="keybindingContexts"
@context-changed="handleContextChange"
/>
</template>
<DataTable
@@ -54,7 +58,7 @@
<template #body="slotProps">
<KeyComboDisplay
v-if="slotProps.data.keybinding"
:keyCombo="slotProps.data.keybinding.combo"
:keyCombo="slotProps.data.keybinding.effectiveCombo"
:isModified="
keybindingStore.isCommandKeybindingModified(slotProps.data.id)
"
@@ -130,6 +134,7 @@ import Message from 'primevue/message'
import Tag from 'primevue/tag'
import PanelTemplate from './PanelTemplate.vue'
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
import KeyContextSelect from './keybinding/KeyContextSelect.vue'
import SearchBox from '@/components/common/SearchBox.vue'
import { useToast } from 'primevue/usetoast'
import { FilterMatchMode } from '@primevue/core/api'
@@ -146,11 +151,22 @@ interface ICommandData {
keybinding: KeybindingImpl | null
}
const selectedContext = ref('global')
const commandsData = computed<ICommandData[]>(() => {
return Object.values(commandStore.commands).map((command) => ({
id: command.id,
keybinding: keybindingStore.getKeybindingByCommandId(command.id)
}))
return Object.values(commandStore.commands)
.map((command) => ({
id: command.id,
keybinding: keybindingStore.getKeybindingByCommandId(command.id)
}))
.filter((data) => {
// If keybinding is null, treat as global context
if (!data.keybinding) {
return selectedContext.value === 'global'
}
// Show commands that match the selected context
return data.keybinding.context === selectedContext.value
})
})
const selectedCommandData = ref<ICommandData | null>(null)
@@ -158,6 +174,7 @@ const editDialogVisible = ref(false)
const newBindingKeyCombo = ref<KeyComboImpl | null>(null)
const currentEditingCommand = ref<ICommandData | null>(null)
const keybindingInput = ref(null)
const keybindingContexts = keybindingStore.keybindingContexts
const existingKeybindingOnCombo = computed<KeybindingImpl | null>(() => {
if (!currentEditingCommand.value) {
@@ -177,14 +194,16 @@ const existingKeybindingOnCombo = computed<KeybindingImpl | null>(() => {
return null
}
return keybindingStore.getKeybinding(newBindingKeyCombo.value)
return keybindingStore.getKeybinding(
newBindingKeyCombo.value,
currentEditingCommand.value.keybinding?.context
)
})
function editKeybinding(commandData: ICommandData) {
currentEditingCommand.value = commandData
newBindingKeyCombo.value = commandData.keybinding
? commandData.keybinding.combo
: null
newBindingKeyCombo.value = commandData.keybinding?.effectiveCombo ?? null
editDialogVisible.value = true
}
@@ -199,7 +218,7 @@ watchEffect(() => {
function removeKeybinding(commandData: ICommandData) {
if (commandData.keybinding) {
keybindingStore.unsetKeybinding(commandData.keybinding)
keybindingStore.unsetKeybinding(commandData.id)
keybindingStore.persistUserKeybindings()
}
}
@@ -220,7 +239,8 @@ function saveKeybinding() {
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({
commandId: currentEditingCommand.value.id,
combo: newBindingKeyCombo.value
combo: newBindingKeyCombo.value,
context: currentEditingCommand.value.keybinding?.context
})
)
if (updated) {
@@ -230,6 +250,11 @@ function saveKeybinding() {
cancelEdit()
}
function handleContextChange(contextId: string) {
selectedContext.value = contextId
console.log('Context changed to', contextId)
}
const toast = useToast()
async function resetKeybindings() {
keybindingStore.resetKeybindings()

View File

@@ -1,11 +1,14 @@
<template>
<span>
<template v-for="(sequence, index) in keySequences" :key="index">
<Tag :severity="isModified ? 'info' : 'secondary'">
{{ sequence }}
</Tag>
<span v-if="index < keySequences.length - 1" class="px-2">+</span>
<template v-if="keyCombo">
<template v-for="(sequence, index) in keySequences" :key="index">
<Tag :severity="isModified ? 'info' : 'secondary'">
{{ sequence }}
</Tag>
<span v-if="index < keySequences.length - 1" class="px-2">+</span>
</template>
</template>
<span v-else>-</span>
</span>
</template>
@@ -16,7 +19,7 @@ import { computed } from 'vue'
const props = withDefaults(
defineProps<{
keyCombo: KeyComboImpl
keyCombo: KeyComboImpl | null
isModified: boolean
}>(),
{
@@ -24,5 +27,5 @@ const props = withDefaults(
}
)
const keySequences = computed(() => props.keyCombo.getKeySequences())
const keySequences = computed(() => props.keyCombo?.getKeySequences() ?? [])
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div class="context-select">
<button
v-for="context in props.contexts"
:key="context.id"
:class="{ selected: selectedContext === context.id }"
@click="selectContext(context.id)"
class="context-button"
>
{{ context.name }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { KeyBindingContextImpl } from '@/stores/keybindingStore'
interface Props {
contexts: KeyBindingContextImpl[]
}
const props = withDefaults(defineProps<Props>(), {
contexts: () => []
})
const emit = defineEmits<{
(e: 'context-changed', contextId: string): void
}>()
const selectedContext = ref(
props.contexts.find((c) => c.id === 'global')?.id || 'global'
)
const selectContext = (contextId: string) => {
selectedContext.value = contextId
emit('context-changed', contextId)
}
</script>
<style scoped>
.context-select {
display: flex;
gap: 8px;
margin: 12px 0;
}
.context-button {
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: none;
cursor: pointer;
}
.context-button.selected {
background: var(--p-highlight-focus-background);
}
</style>

View File

@@ -21,7 +21,7 @@
v-if="item?.comfyCommand?.keybinding"
class="ml-auto border border-surface rounded text-muted text-xs p-1 keybinding-tag"
>
{{ item.comfyCommand.keybinding.combo.toString() }}
{{ item.comfyCommand.keybinding.currentCombo?.toString() || '-' }}
</span>
</a>
</template>

View File

@@ -173,5 +173,22 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
key: 'f'
},
commandId: 'Workspace.ToggleFocusMode'
},
{
combo: {
key: 'z',
ctrl: true
},
commandId: 'Comfy.Undo',
targetSelector: '#graph-canvas'
},
{
combo: {
key: 'z',
ctrl: true,
shift: true
},
commandId: 'Comfy.Redo',
targetSelector: '#graph-canvas'
}
]

View File

@@ -416,6 +416,15 @@ export const CORE_SETTINGS: SettingParams[] = [
versionAdded: '1.3.5'
},
{
//new array that saves all modified keybindings, no distinction between modified and unset -> unified in object
id: 'Comfy.Keybinding.ModifiedBindings',
name: 'Keybindings changed by the user',
type: 'hidden',
defaultValue: [] as Keybinding[],
versionAdded: '1.5.7'
},
{
//deprecated
id: 'Comfy.Keybinding.UnsetBindings',
name: 'Keybindings unset by the user',
type: 'hidden',
@@ -423,6 +432,7 @@ export const CORE_SETTINGS: SettingParams[] = [
versionAdded: '1.3.7'
},
{
//deprecated
id: 'Comfy.Keybinding.NewBindings',
name: 'Keybindings set by the user',
type: 'hidden',

View File

@@ -2,64 +2,99 @@ import { app } from '../../scripts/app'
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useCommandStore } from '@/stores/commandStore'
app.registerExtension({
name: 'Comfy.Keybinds',
init() {
const keybindListener = async function (event: KeyboardEvent) {
// Ignore keybindings for legacy jest tests as jest tests don't have
// a Vue app instance or pinia stores.
if (!app.vueAppReady) return
//this class is responsible for handling key events and executing commands
class KeyboardManager {
private modifiers: string[] = []
private context: string = 'global'
const keyCombo = KeyComboImpl.fromEvent(event)
if (keyCombo.isModifier) {
return
}
constructor() {
this.addListeners()
}
// Ignore non-modifier keybindings if typing in input fields
const target = event.composedPath()[0] as HTMLElement
addListeners() {
window.addEventListener('keydown', (event) => this.handleKeyDown(event))
window.addEventListener('keyup', (event) => this.handleKeyUp(event))
window.addEventListener('blur', () => this.clearKeys())
app.extensionManager.setting.set('Comfy.KeybindContext', 'global')
}
private clearKeys() {
this.modifiers = []
}
private handleKeyUp(event: KeyboardEvent) {
this.modifiers = this.modifiers.filter((key) => key !== event.key)
}
private setContext(event?: KeyboardEvent) {
if (!event) return
event.preventDefault()
const context = app.extensionManager.setting.get('Comfy.KeybindContext')
this.context = context
}
private async handleKeyDown(event: KeyboardEvent) {
if (!app.vueAppReady) return
if (event.key === 'Escape' && this.modifiers.length === 0) {
this.handleEscapeKey()
return
}
if (event.key === 'F12') return // prevent opening dev tools
this.setContext(event)
const target = event.composedPath()[0] as HTMLElement
const excludedTags = ['TEXTAREA', 'INPUT', 'SPAN']
if (this.context === 'global') {
if (
!keyCombo.hasModifier &&
(target.tagName === 'TEXTAREA' ||
target.tagName === 'INPUT' ||
(target.tagName === 'SPAN' &&
target.classList.contains('property_value')))
excludedTags.includes(target.tagName) ||
target.classList.contains('property_value')
) {
return
}
const keybindingStore = useKeybindingStore()
const commandStore = useCommandStore()
const keybinding = keybindingStore.getKeybinding(keyCombo)
if (keybinding && keybinding.targetSelector !== '#graph-canvas') {
// Prevent default browser behavior first, then execute the command
event.preventDefault()
await commandStore.execute(keybinding.commandId)
return
}
// Only clear dialogs if not using modifiers
if (event.ctrlKey || event.altKey || event.metaKey) {
return
}
// Escape key: close the first open modal found, and all dialogs
if (event.key === 'Escape') {
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
for (const modal of modals) {
const modalDisplay = window
.getComputedStyle(modal)
.getPropertyValue('display')
if (modalDisplay !== 'none') {
modal.style.display = 'none'
break
}
}
for (const d of document.querySelectorAll('dialog')) d.close()
}
}
window.addEventListener('keydown', keybindListener)
const keyCombo = KeyComboImpl.fromEvent(event)
if (keyCombo.isModifier) return
const keybindingStore = useKeybindingStore()
const commandStore = useCommandStore()
const keybinding = keybindingStore.getKeybinding(keyCombo, this.context)
console.log(keyCombo, keybinding)
if (keybinding) {
console.log('executing command', keybinding.commandId)
event.preventDefault()
await commandStore.execute(keybinding.commandId)
return
}
}
private handleEscapeKey() {
const modals = document.querySelectorAll<HTMLElement>('.comfy-modal')
const modal = Array.from(modals).find(
(modal) =>
window.getComputedStyle(modal).getPropertyValue('display') !== 'none'
)
if (modal) {
modal.style.display = 'none'
}
;[...document.querySelectorAll('dialog')].forEach((d) => {
d.close()
})
}
setKeybindingContext(context: string) {
this.context = context
}
}
app.registerExtension({
name: 'Comfy.Keybinds',
init() {
const manager = new KeyboardManager()
}
})

View File

@@ -1706,7 +1706,7 @@ export class ComfyApp {
await this.#loadExtensions()
this.#addProcessMouseHandler()
this.#addProcessKeyHandler()
//this.#addProcessKeyHandler() //removes another key handler
this.#addConfigureHandler()
this.#addApiUpdateHandlers()
this.#addRestoreWorkflowView()

View File

@@ -162,20 +162,6 @@ export class ChangeTracker {
)
}
async undoRedo(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && !e.altKey) {
const key = e.key.toUpperCase()
// Redo: Ctrl + Y, or Ctrl + Shift + Z
if ((key === 'Y' && !e.shiftKey) || (key == 'Z' && e.shiftKey)) {
await this.redo()
return true
} else if (key === 'Z' && !e.shiftKey) {
await this.undo()
return true
}
}
}
beforeChange() {
this.changeCount++
}
@@ -194,6 +180,7 @@ export class ChangeTracker {
ChangeTracker.app = app
let keyIgnored = false
/*
window.addEventListener(
'keydown',
(e: KeyboardEvent) => {
@@ -237,7 +224,7 @@ export class ChangeTracker {
},
true
)
*/
window.addEventListener('keyup', (e) => {
if (keyIgnored) {
keyIgnored = false

View File

@@ -57,7 +57,7 @@ export class ComfyCommandImpl implements ComfyCommand {
: this._menubarLabel
}
get keybinding(): KeybindingImpl | null {
get keybinding(): KeybindingImpl | undefined {
return useKeybindingStore().getKeybindingByCommandId(this.id)
}
}

View File

@@ -46,6 +46,7 @@ export const useExtensionStore = defineStore('extension', () => {
}
extensionByName.value[extension.name] = markRaw(extension)
useKeybindingStore().loadExtensionKeybindingContexts(extension)
useKeybindingStore().loadExtensionKeybindings(extension)
useCommandStore().loadExtensionCommands(extension)
useMenuItemStore().loadExtensionMenuCommands(extension)

View File

@@ -1,29 +1,70 @@
// @ts-strict-ignore - will remove in the future
import { defineStore } from 'pinia'
import { computed, Ref, ref, toRaw } from 'vue'
import { Keybinding, KeyCombo } from '@/types/keyBindingTypes'
import { ref, toRaw } from 'vue'
import {
Keybinding,
KeyCombo,
KeyBindingContext
} from '@/types/keyBindingTypes'
import { useSettingStore } from './settingStore'
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
import type { ComfyExtension } from '@/types/comfy'
export class KeybindingImpl implements Keybinding {
commandId: string
combo: KeyComboImpl
targetSelector?: string
readonly commandId: string
readonly combo?: KeyComboImpl | null
currentCombo?: KeyComboImpl | null = null
readonly targetSelector?: string
readonly context?: string
//the new system saves the state of the keybindings in the object instead of the store
constructor(obj: Keybinding) {
this.commandId = obj.commandId
this.combo = new KeyComboImpl(obj.combo)
this.combo = obj.combo ? new KeyComboImpl(obj.combo) : null //this is the default combo set by comfyUI/the extension, can also be null for unset keybindings
this.currentCombo = this.combo //this is the current combo set by the user, can also be null for user unset keybindings
if (obj.currentCombo) this.currentCombo = new KeyComboImpl(obj.currentCombo)
this.targetSelector = obj.targetSelector
this.context = obj.context ?? 'global'
}
get defaultCombo(): KeyComboImpl {
return this.combo
}
get effectiveCombo(): KeyComboImpl {
return this.currentCombo // null = unset, currentCombo != combo = user set combo
}
overwriteCombo(combo: KeyComboImpl) {
this.currentCombo = combo // user set combo
}
unsetCombo() {
this.currentCombo = null // unset
}
resetCombo() {
this.currentCombo = this.combo //resets the combo to the default combo
}
isModified(): boolean {
return this.currentCombo !== this.combo
}
getContext(): string {
return this.context
}
equals(other: unknown): boolean {
const raw = toRaw(other)
return raw instanceof KeybindingImpl
? this.commandId === raw.commandId &&
this.combo.equals(raw.combo) &&
this.targetSelector === raw.targetSelector
: false
if (!(raw instanceof KeybindingImpl)) return false
//the new system compares keybindingsd by the object properties instead of a serialized string
return (
this.commandId === raw.commandId &&
this.combo.equals(raw.combo) &&
this.targetSelector === raw.targetSelector &&
this.context === raw.context
)
}
}
@@ -50,6 +91,8 @@ export class KeyComboImpl implements KeyCombo {
})
}
//removed the serialization function because the new system compares keybindings using the object properties
equals(other: unknown): boolean {
const raw = toRaw(other)
@@ -61,10 +104,6 @@ export class KeyComboImpl implements KeyCombo {
: false
}
serialize(): string {
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
}
toString(): string {
return this.getKeySequences().join(' + ')
}
@@ -93,182 +132,182 @@ export class KeyComboImpl implements KeyCombo {
}
}
//used in the vue settings header to select between keybinding contexts
export class KeyBindingContextImpl implements KeyBindingContext {
id: string
name: string
constructor(obj: KeyBindingContext) {
this.id = obj.id
this.name = obj.name
}
}
export const useKeybindingStore = defineStore('keybinding', () => {
/**
* Default keybindings provided by core and extensions.
*/
const defaultKeybindings = ref<Record<string, KeybindingImpl>>({})
/**
* User-defined keybindings.
*/
const userKeybindings = ref<Record<string, KeybindingImpl>>({})
/**
* User-defined keybindings that unset default keybindings.
*/
const userUnsetKeybindings = ref<Record<string, KeybindingImpl>>({})
const keybindingByKeyCombo = computed<Record<string, KeybindingImpl>>(() => {
const result: Record<string, KeybindingImpl> = {
...defaultKeybindings.value
const keybindings = ref<KeybindingImpl[]>([])
const keybindingContexts = ref<KeyBindingContextImpl[]>([
{
id: 'global',
name: 'Global'
}
for (const keybinding of Object.values(userUnsetKeybindings.value)) {
const serializedCombo = keybinding.combo.serialize()
if (result[serializedCombo]?.equals(keybinding)) {
delete result[serializedCombo]
}
}
return {
...result,
...userKeybindings.value
}
})
const keybindings = computed<KeybindingImpl[]>(() =>
Object.values(keybindingByKeyCombo.value)
)
function getKeybinding(combo: KeyComboImpl) {
return keybindingByKeyCombo.value[combo.serialize()]
]) // global is default
function getKeybinding(
combo: KeyCombo,
context: string = 'global'
): KeybindingImpl | undefined {
return keybindings.value.find((keybinding) => {
return (
keybinding.effectiveCombo &&
keybinding.effectiveCombo.equals(combo) &&
keybinding.context === context
)
})
}
function createKeybindingsByCommandId(keybindings: KeybindingImpl[]) {
const result: Record<string, KeybindingImpl[]> = {}
for (const keybinding of keybindings) {
if (!(keybinding.commandId in result)) {
result[keybinding.commandId] = []
}
result[keybinding.commandId].push(keybinding)
}
return result
function getKeybindingsByCommandId(commandId: string): KeybindingImpl[] {
return keybindings.value.filter(
(keybinding) => keybinding.commandId === commandId
)
}
const keybindingsByCommandId = computed<Record<string, KeybindingImpl[]>>(
() => {
return createKeybindingsByCommandId(keybindings.value)
}
)
function getKeybindingsByCommandId(commandId: string) {
return keybindingsByCommandId.value[commandId] ?? []
}
const defaultKeybindingsByCommandId = computed<
Record<string, KeybindingImpl[]>
>(() => {
return createKeybindingsByCommandId(Object.values(defaultKeybindings.value))
})
function getKeybindingByCommandId(commandId: string) {
function getKeybindingByCommandId(
commandId: string
): KeybindingImpl | undefined {
return getKeybindingsByCommandId(commandId)[0]
}
function addKeybinding(
target: Ref<Record<string, KeybindingImpl>>,
keybinding: KeybindingImpl,
{ existOk = false }: { existOk: boolean }
) {
if (!existOk && keybinding.combo.serialize() in target.value) {
throw new Error(
`Keybinding on ${keybinding.combo} already exists on ${
target.value[keybinding.combo.serialize()].commandId
}`
)
function addKeybinding(keybinding: KeybindingImpl) {
if (
getKeybinding(keybinding.effectiveCombo, keybinding.context) !== undefined
) {
throw new Error(`Keybinding ${keybinding.commandId} already exists.`)
}
target.value[keybinding.combo.serialize()] = keybinding
keybindings.value.push(keybinding)
}
//kept for backwards compatibility
function addDefaultKeybinding(keybinding: KeybindingImpl) {
addKeybinding(defaultKeybindings, keybinding, { existOk: false })
addKeybinding(keybinding)
}
function addUserKeybinding(keybinding: KeybindingImpl) {
const defaultKeybinding =
defaultKeybindings.value[keybinding.combo.serialize()]
const userUnsetKeybinding =
userUnsetKeybindings.value[keybinding.combo.serialize()]
const effectiveCombo = keybinding.effectiveCombo
const context = keybinding.context ?? 'global'
// User is adding back a keybinding that was an unsetted default keybinding.
if (
keybinding.equals(defaultKeybinding) &&
keybinding.equals(userUnsetKeybinding)
) {
delete userUnsetKeybindings.value[keybinding.combo.serialize()]
return
const existingKeybinding = getKeybinding(effectiveCombo, context)
if (existingKeybinding) {
existingKeybinding.overwriteCombo(effectiveCombo)
} else {
addKeybinding(keybinding)
}
// Unset keybinding on default keybinding if it exists and is not the same as userUnsetKeybinding
if (defaultKeybinding && !defaultKeybinding.equals(userUnsetKeybinding)) {
unsetKeybinding(defaultKeybinding)
}
addKeybinding(userKeybindings, keybinding, { existOk: true })
}
function unsetKeybinding(keybinding: KeybindingImpl) {
const serializedCombo = keybinding.combo.serialize()
if (!(serializedCombo in keybindingByKeyCombo.value)) {
console.warn(
`Trying to unset non-exist keybinding: ${JSON.stringify(keybinding)}`
)
return
}
function unsetKeybinding(commandId: string) {
const existingKeybinding = getKeybindingByCommandId(commandId)
if (userKeybindings.value[serializedCombo]?.equals(keybinding)) {
delete userKeybindings.value[serializedCombo]
return
if (existingKeybinding) {
existingKeybinding.unsetCombo()
}
if (defaultKeybindings.value[serializedCombo]?.equals(keybinding)) {
addKeybinding(userUnsetKeybindings, keybinding, { existOk: false })
return
}
throw new Error(`Unknown keybinding: ${JSON.stringify(keybinding)}`)
}
/**
* Update the keybinding on given command if it is different from the current keybinding.
*
* @returns true if the keybinding is updated, false otherwise.
*/
function updateKeybindingOnCommand(keybinding: KeybindingImpl): boolean {
const currentKeybinding = getKeybindingByCommandId(keybinding.commandId)
if (currentKeybinding?.equals(keybinding)) {
return false
const existingKeybinding1 = getKeybindingByCommandId(keybinding.commandId)
const existingKeybinding2 = getKeybinding(
keybinding.effectiveCombo,
keybinding.context
)
if (existingKeybinding1) {
existingKeybinding1.overwriteCombo(keybinding.effectiveCombo)
return true
}
if (currentKeybinding) {
unsetKeybinding(currentKeybinding)
if (existingKeybinding2) {
existingKeybinding2.overwriteCombo(keybinding.effectiveCombo)
return true
}
addUserKeybinding(keybinding)
addKeybinding(keybinding)
return true
}
function loadUserKeybindings() {
async function loadUserKeybindings() {
await migrateKeybindings()
const settingStore = useSettingStore()
// Unset bindings first as new bindings might conflict with default bindings.
const unsetBindings = settingStore.get('Comfy.Keybinding.UnsetBindings')
for (const keybinding of unsetBindings) {
unsetKeybinding(new KeybindingImpl(keybinding))
}
const newBindings = settingStore.get('Comfy.Keybinding.NewBindings')
for (const keybinding of newBindings) {
addUserKeybinding(new KeybindingImpl(keybinding))
// Load modified bindings from settings
const modifiedBindings =
settingStore.get('Comfy.Keybinding.ModifiedBindings') ?? []
for (const binding of modifiedBindings) {
const existing = getKeybindingByCommandId(binding.commandId)
if (existing != null && binding) {
const keyCombo = binding.currentCombo
? new KeyComboImpl(binding.currentCombo)
: null
existing.overwriteCombo(keyCombo)
}
}
}
async function migrateKeybindings() {
const settingStore = useSettingStore()
const changedKeybindings =
settingStore.get('Comfy.Keybinding.NewBindings') ?? []
const unsetKeybindings =
settingStore.get('Comfy.Keybinding.UnsetBindings') ?? []
console.log('Migrating keybindings', changedKeybindings, unsetKeybindings)
for (const keybinding of changedKeybindings) {
const existing = getKeybindingByCommandId(keybinding.commandId)
if (existing != null) {
existing.overwriteCombo(new KeyComboImpl(keybinding.combo))
} else {
const { combo: currentCombo, ...rest } = keybinding
const newKeybinding = {
...rest,
currentCombo,
combo: null
}
addUserKeybinding(new KeybindingImpl(newKeybinding))
}
}
for (const keybinding of unsetKeybindings) {
unsetKeybinding(keybinding.commandId)
}
//await settingStore.set('Comfy.Keybinding.NewBindings', [])
//await settingStore.set('Comfy.Keybinding.UnsetBindings', [])
/*
clearing the old keybinding arrays can be done in a future version because the new keybinding
system is loaded after this migration function is called. This means that the old keybindings
will be overwritten if the user sets a new keybinding for the same command.
*/
}
function loadCoreKeybindings() {
// Simply load core keybindings as defaults
for (const keybinding of CORE_KEYBINDINGS) {
addDefaultKeybinding(new KeybindingImpl(keybinding))
}
}
function loadExtensionKeybindingContexts(extension: ComfyExtension) {
if (extension.keybindingContexts) {
for (const context of extension.keybindingContexts) {
if (!keybindingContexts.value.includes(context)) {
keybindingContexts.value.push(context)
}
}
}
}
function loadExtensionKeybindings(extension: ComfyExtension) {
if (extension.keybindings) {
for (const keybinding of extension.keybindings) {
try {
addDefaultKeybinding(new KeybindingImpl(keybinding))
addUserKeybinding(new KeybindingImpl(keybinding))
} catch (error) {
console.warn(
`Failed to load keybinding for extension ${extension.name}`,
@@ -281,37 +320,48 @@ export const useKeybindingStore = defineStore('keybinding', () => {
async function persistUserKeybindings() {
const settingStore = useSettingStore()
// TODO(https://github.com/Comfy-Org/ComfyUI_frontend/issues/1079):
// Allow setting multiple values at once in settingStore
// Only save modified keybindings
const modifiedBindings = keybindings.value
.filter((kb) => kb.isModified())
.map((kb) => ({
//only the relevant properties are saved to the settings
commandId: kb.commandId,
currentCombo: kb.currentCombo
}))
await settingStore.set(
'Comfy.Keybinding.NewBindings',
Object.values(userKeybindings.value)
)
await settingStore.set(
'Comfy.Keybinding.UnsetBindings',
Object.values(userUnsetKeybindings.value)
'Comfy.Keybinding.ModifiedBindings',
modifiedBindings
)
}
function resetKeybindings() {
userKeybindings.value = {}
userUnsetKeybindings.value = {}
/**
* Resets keybindings for a specific context or all keybindings if no context is provided
* @param context Optional context to reset keybindings for
*/
function resetKeybindings(context?: string): void {
if (!keybindings.value?.length) {
return
}
keybindings.value.forEach((keybinding) => {
if (!context || keybinding.getContext() === context) {
keybinding.resetCombo()
}
})
}
function isCommandKeybindingModified(commandId: string): boolean {
const currentKeybinding: KeybindingImpl | undefined =
getKeybindingByCommandId(commandId)
const defaultKeybinding: KeybindingImpl | undefined =
defaultKeybindingsByCommandId.value[commandId]?.[0]
return !(
(currentKeybinding === undefined && defaultKeybinding === undefined) ||
currentKeybinding?.equals(defaultKeybinding)
)
const keybinding = getKeybindingByCommandId(commandId)
if (!keybinding)
throw new Error(`Keybinding for command ${commandId} not found.`)
return keybinding.isModified()
}
return {
keybindings,
keybindingContexts,
getKeybinding,
getKeybindingsByCommandId,
getKeybindingByCommandId,
@@ -322,6 +372,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
loadUserKeybindings,
loadCoreKeybindings,
loadExtensionKeybindings,
loadExtensionKeybindingContexts,
persistUserKeybindings,
resetKeybindings,
isCommandKeybindingModified

View File

@@ -13,13 +13,25 @@ export const zKeyCombo = z.object({
export const zKeybinding = z.object({
commandId: z.string(),
combo: zKeyCombo,
currentCombo: zKeyCombo.optional(),
// Optional target element CSS selector 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.
targetSelector: z.string().optional()
targetSelector: z.string().optional(),
//context is used to distinguish between global keybindings and keybinds that only work in designated contexts.
// keybindngs without a set context are presumed to be global keybindngs.
// Extensions can create their own contexts to reuse keybindngs already set in the global context like crtl+z for undo.
context: z.string().optional()
})
// KeyBindingContext schema
export const zKeyBindingContext = z.object({
id: z.string(),
name: z.string()
})
// Infer types from schemas
export type KeyCombo = z.infer<typeof zKeyCombo>
export type Keybinding = z.infer<typeof zKeybinding>
export type KeyBindingContext = z.infer<typeof zKeyBindingContext>

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import { setActivePinia, createPinia } from 'pinia'
import { useKeybindingStore, KeybindingImpl } from '@/stores/keybindingStore'
@@ -16,7 +17,7 @@ describe('useKeybindingStore', () => {
store.addDefaultKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
expect(store.getKeybinding(keybinding.combo)).toEqual(keybinding)
expect(store.getKeybinding(keybinding!.combo)).toEqual(keybinding)
})
it('should add and retrieve user keybindings', () => {
@@ -57,7 +58,7 @@ describe('useKeybindingStore', () => {
combo: { key: 'C', ctrl: true }
})
store.addDefaultKeybinding(defaultKeybinding)
store.unsetKeybinding(defaultKeybinding)
store.unsetKeybinding(defaultKeybinding.commandId)
const userKeybinding = new KeybindingImpl({
commandId: 'test.command2',
@@ -79,8 +80,10 @@ describe('useKeybindingStore', () => {
store.addUserKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
store.unsetKeybinding(keybinding)
expect(store.keybindings).toHaveLength(0)
store.unsetKeybinding(keybinding.commandId)
expect(
store.getKeybindingByCommandId(keybinding.commandId).currentCombo
).toBeNull()
})
it('should unset default keybindings', () => {
@@ -93,8 +96,10 @@ describe('useKeybindingStore', () => {
store.addDefaultKeybinding(keybinding)
expect(store.keybindings).toHaveLength(1)
store.unsetKeybinding(keybinding)
expect(store.keybindings).toHaveLength(0)
store.unsetKeybinding(keybinding.commandId)
expect(
store.getKeybindingByCommandId(keybinding.commandId).currentCombo
).toBeNull()
})
it('should throw an error when adding duplicate default keybindings', () => {
@@ -133,7 +138,7 @@ describe('useKeybindingStore', () => {
combo: { key: 'H', alt: true, shift: true }
})
expect(() => store.unsetKeybinding(keybinding)).not.toThrow()
expect(() => store.unsetKeybinding(keybinding.commandId)).not.toThrow()
})
it('should remove unset keybinding when adding back a default keybinding', () => {
@@ -148,7 +153,7 @@ describe('useKeybindingStore', () => {
expect(store.keybindings).toHaveLength(1)
// Unset the default keybinding
store.unsetKeybinding(defaultKeybinding)
store.unsetKeybinding(defaultKeybinding.commandId)
expect(store.keybindings).toHaveLength(0)
// Add the same keybinding as a user keybinding
@@ -215,7 +220,7 @@ describe('useKeybindingStore', () => {
)
for (const keybinding of userUnsetKeybindings) {
store.unsetKeybinding(keybinding)
store.unsetKeybinding(keybinding.commandId)
}
expect(store.keybindings).toHaveLength(1)