mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Support keybinding customization (#1081)
* Basic keybinding panel nit Make row selectable Reduce padding Better key seq render Show actions on demand Turn off autocomplete nit Persist keybindings Autofocus Fix set unsetted keybinding bug Refactor Add reset button Add back default keybinding logic Report key conflict error Adjust style fix bug Highlight modified keybindings * Set current editing command's id as dialog header
This commit is contained in:
@@ -54,6 +54,9 @@
|
|||||||
<TabPanel key="about" value="About">
|
<TabPanel key="about" value="About">
|
||||||
<AboutPanel />
|
<AboutPanel />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel key="keybinding" value="Keybinding">
|
||||||
|
<KeybindingPanel />
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,6 +77,7 @@ import SearchBox from '@/components/common/SearchBox.vue'
|
|||||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||||
import { flattenTree } from '@/utils/treeUtil'
|
import { flattenTree } from '@/utils/treeUtil'
|
||||||
import AboutPanel from './setting/AboutPanel.vue'
|
import AboutPanel from './setting/AboutPanel.vue'
|
||||||
|
import KeybindingPanel from './setting/KeybindingPanel.vue'
|
||||||
|
|
||||||
interface ISettingGroup {
|
interface ISettingGroup {
|
||||||
label: string
|
label: string
|
||||||
@@ -86,10 +90,17 @@ const aboutPanelNode: SettingTreeNode = {
|
|||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keybindingPanelNode: SettingTreeNode = {
|
||||||
|
key: 'keybinding',
|
||||||
|
label: 'Keybinding',
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
|
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
|
||||||
const categories = computed<SettingTreeNode[]>(() => [
|
const categories = computed<SettingTreeNode[]>(() => [
|
||||||
...(settingRoot.value.children || []),
|
...(settingRoot.value.children || []),
|
||||||
|
keybindingPanelNode,
|
||||||
aboutPanelNode
|
aboutPanelNode
|
||||||
])
|
])
|
||||||
const activeCategory = ref<SettingTreeNode | null>(null)
|
const activeCategory = ref<SettingTreeNode | null>(null)
|
||||||
|
|||||||
223
src/components/dialog/content/setting/KeybindingPanel.vue
Normal file
223
src/components/dialog/content/setting/KeybindingPanel.vue
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<div class="keybinding-panel">
|
||||||
|
<DataTable
|
||||||
|
:value="commandsData"
|
||||||
|
v-model:selection="selectedCommandData"
|
||||||
|
selectionMode="single"
|
||||||
|
stripedRows
|
||||||
|
>
|
||||||
|
<Column field="actions" header="">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div class="actions invisible">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
class="p-button-text"
|
||||||
|
@click="editKeybinding(slotProps.data)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-trash"
|
||||||
|
class="p-button-text p-button-danger"
|
||||||
|
@click="removeKeybinding(slotProps.data)"
|
||||||
|
:disabled="!slotProps.data.keybinding"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="id" header="Command ID" sortable></Column>
|
||||||
|
<Column field="keybinding" header="Keybinding">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<KeyComboDisplay
|
||||||
|
v-if="slotProps.data.keybinding"
|
||||||
|
:keyCombo="slotProps.data.keybinding.combo"
|
||||||
|
:isModified="
|
||||||
|
keybindingStore.isCommandKeybindingModified(slotProps.data.id)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
class="min-w-96"
|
||||||
|
v-model:visible="editDialogVisible"
|
||||||
|
modal
|
||||||
|
:header="currentEditingCommand?.id"
|
||||||
|
@hide="cancelEdit"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<InputText
|
||||||
|
class="mb-2 text-center"
|
||||||
|
ref="keybindingInput"
|
||||||
|
:modelValue="newBindingKeyCombo?.toString() ?? ''"
|
||||||
|
placeholder="Press keys for new binding"
|
||||||
|
@keydown.stop.prevent="captureKeybinding"
|
||||||
|
autocomplete="off"
|
||||||
|
fluid
|
||||||
|
:invalid="!!existingKeybindingOnCombo"
|
||||||
|
/>
|
||||||
|
<Message v-if="existingKeybindingOnCombo" severity="error">
|
||||||
|
Keybinding already exists on
|
||||||
|
<Tag
|
||||||
|
severity="secondary"
|
||||||
|
:value="existingKeybindingOnCombo.commandId"
|
||||||
|
/>
|
||||||
|
</Message>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button
|
||||||
|
label="Save"
|
||||||
|
icon="pi pi-check"
|
||||||
|
@click="saveKeybinding"
|
||||||
|
:disabled="!!existingKeybindingOnCombo"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
<Button
|
||||||
|
class="mt-4"
|
||||||
|
:label="$t('reset')"
|
||||||
|
v-tooltip="$t('resetKeybindingsTooltip')"
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
fluid
|
||||||
|
text
|
||||||
|
@click="resetKeybindings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watchEffect } from 'vue'
|
||||||
|
import {
|
||||||
|
useKeybindingStore,
|
||||||
|
KeyComboImpl,
|
||||||
|
KeybindingImpl
|
||||||
|
} from '@/stores/keybindingStore'
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import DataTable from 'primevue/datatable'
|
||||||
|
import Column from 'primevue/column'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Dialog from 'primevue/dialog'
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Message from 'primevue/message'
|
||||||
|
import Tag from 'primevue/tag'
|
||||||
|
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
|
||||||
|
import { useToast } from 'primevue/usetoast'
|
||||||
|
|
||||||
|
const keybindingStore = useKeybindingStore()
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
|
||||||
|
interface ICommandData {
|
||||||
|
id: string
|
||||||
|
keybinding: KeybindingImpl | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandsData = computed<ICommandData[]>(() => {
|
||||||
|
return Object.values(commandStore.commands).map((command) => ({
|
||||||
|
id: command.id,
|
||||||
|
keybinding: keybindingStore.getKeybindingByCommandId(command.id)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCommandData = ref<ICommandData | null>(null)
|
||||||
|
const editDialogVisible = ref(false)
|
||||||
|
const newBindingKeyCombo = ref<KeyComboImpl | null>(null)
|
||||||
|
const currentEditingCommand = ref<ICommandData | null>(null)
|
||||||
|
const keybindingInput = ref(null)
|
||||||
|
|
||||||
|
const existingKeybindingOnCombo = computed<KeybindingImpl | null>(() => {
|
||||||
|
if (!currentEditingCommand.value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the new keybinding is the same as the current editing command, then don't show the error
|
||||||
|
if (
|
||||||
|
currentEditingCommand.value.keybinding?.combo?.equals(
|
||||||
|
newBindingKeyCombo.value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newBindingKeyCombo.value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return keybindingStore.getKeybinding(newBindingKeyCombo.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
function editKeybinding(commandData: ICommandData) {
|
||||||
|
currentEditingCommand.value = commandData
|
||||||
|
newBindingKeyCombo.value = commandData.keybinding
|
||||||
|
? commandData.keybinding.combo
|
||||||
|
: null
|
||||||
|
editDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (editDialogVisible.value) {
|
||||||
|
// nextTick doesn't work here, so we use a timeout instead
|
||||||
|
setTimeout(() => {
|
||||||
|
keybindingInput.value?.$el?.focus()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function removeKeybinding(commandData: ICommandData) {
|
||||||
|
if (commandData.keybinding) {
|
||||||
|
keybindingStore.unsetKeybinding(commandData.keybinding)
|
||||||
|
keybindingStore.persistUserKeybindings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureKeybinding(event: KeyboardEvent) {
|
||||||
|
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||||
|
newBindingKeyCombo.value = keyCombo
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editDialogVisible.value = false
|
||||||
|
currentEditingCommand.value = null
|
||||||
|
newBindingKeyCombo.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveKeybinding() {
|
||||||
|
if (currentEditingCommand.value && newBindingKeyCombo.value) {
|
||||||
|
const updated = keybindingStore.updateKeybindingOnCommand(
|
||||||
|
new KeybindingImpl({
|
||||||
|
commandId: currentEditingCommand.value.id,
|
||||||
|
combo: newBindingKeyCombo.value
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (updated) {
|
||||||
|
keybindingStore.persistUserKeybindings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
async function resetKeybindings() {
|
||||||
|
keybindingStore.resetKeybindings()
|
||||||
|
await keybindingStore.persistUserKeybindings()
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: 'Info',
|
||||||
|
detail: 'Keybindings reset',
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.p-datatable-tbody) > tr > td {
|
||||||
|
padding: 1px;
|
||||||
|
min-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-datatable-row-selected) .actions,
|
||||||
|
:deep(.p-datatable-selectable-row:hover) .actions {
|
||||||
|
@apply visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Tag from 'primevue/tag'
|
||||||
|
import { KeyComboImpl } from '@/stores/keybindingStore'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
keyCombo: KeyComboImpl
|
||||||
|
isModified: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
isModified: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const keySequences = computed(() => props.keyCombo.getKeySequences())
|
||||||
|
</script>
|
||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
useModelToNodeStore
|
useModelToNodeStore
|
||||||
} from '@/stores/modelToNodeStore'
|
} from '@/stores/modelToNodeStore'
|
||||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||||
|
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||||
|
|
||||||
const emit = defineEmits(['ready'])
|
const emit = defineEmits(['ready'])
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
@@ -200,6 +201,9 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Load keybindings. This must be done after comfyApp loads settings.
|
||||||
|
useKeybindingStore().loadUserKeybindings()
|
||||||
|
|
||||||
// Migrate legacy bookmarks
|
// Migrate legacy bookmarks
|
||||||
useNodeBookmarkStore().migrateLegacyBookmarks()
|
useNodeBookmarkStore().migrateLegacyBookmarks()
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,6 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', keybindListener, true)
|
window.addEventListener('keydown', keybindListener)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const messages = {
|
|||||||
add: 'Add',
|
add: 'Add',
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
|
resetKeybindingsTooltip: 'Reset keybindings to default',
|
||||||
customizeFolder: 'Customize Folder',
|
customizeFolder: 'Customize Folder',
|
||||||
icon: 'Icon',
|
icon: 'Icon',
|
||||||
color: 'Color',
|
color: 'Color',
|
||||||
@@ -112,6 +113,7 @@ const messages = {
|
|||||||
add: '添加',
|
add: '添加',
|
||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
|
resetKeybindingsTooltip: '重置键位',
|
||||||
customizeFolder: '定制文件夹',
|
customizeFolder: '定制文件夹',
|
||||||
icon: '图标',
|
icon: '图标',
|
||||||
color: '颜色',
|
color: '颜色',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { globalTracker } from '@/scripts/changeTracker'
|
import { globalTracker } from '@/scripts/changeTracker'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useToastStore } from '@/stores/toastStore'
|
import { useToastStore } from '@/stores/toastStore'
|
||||||
@@ -32,12 +32,14 @@ const getTracker = () =>
|
|||||||
export const useCommandStore = defineStore('command', () => {
|
export const useCommandStore = defineStore('command', () => {
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
const commands = ref<Record<string, ComfyCommand>>({})
|
const commandsById = ref<Record<string, ComfyCommand>>({})
|
||||||
|
const commands = computed(() => Object.values(commandsById.value))
|
||||||
|
|
||||||
const registerCommand = (command: ComfyCommand) => {
|
const registerCommand = (command: ComfyCommand) => {
|
||||||
if (commands.value[command.id]) {
|
if (commandsById.value[command.id]) {
|
||||||
console.warn(`Command ${command.id} already registered`)
|
console.warn(`Command ${command.id} already registered`)
|
||||||
}
|
}
|
||||||
commands.value[command.id] = command
|
commandsById.value[command.id] = command
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandDefinitions: ComfyCommand[] = [
|
const commandDefinitions: ComfyCommand[] = [
|
||||||
@@ -311,15 +313,15 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
|
|
||||||
commandDefinitions.forEach(registerCommand)
|
commandDefinitions.forEach(registerCommand)
|
||||||
const getCommandFunction = (command: string) => {
|
const getCommandFunction = (command: string) => {
|
||||||
return commands.value[command]?.function ?? (() => {})
|
return commandsById.value[command]?.function ?? (() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCommand = (command: string) => {
|
const getCommand = (command: string) => {
|
||||||
return commands.value[command]
|
return commandsById.value[command]
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRegistered = (command: string) => {
|
const isRegistered = (command: string) => {
|
||||||
return !!commands.value[command]
|
return !!commandsById.value[command]
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadExtensionCommands = (extension: ComfyExtension) => {
|
const loadExtensionCommands = (extension: ComfyExtension) => {
|
||||||
@@ -331,6 +333,7 @@ export const useCommandStore = defineStore('command', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
commands,
|
||||||
getCommand,
|
getCommand,
|
||||||
getCommandFunction,
|
getCommandFunction,
|
||||||
registerCommand,
|
registerCommand,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export class KeyComboImpl implements KeyCombo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return `${this.key} + ${this.ctrl ? 'Ctrl' : ''}${this.alt ? 'Alt' : ''}${this.shift ? 'Shift' : ''}`
|
return this.getKeySequences().join(' + ')
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasModifier(): boolean {
|
get hasModifier(): boolean {
|
||||||
@@ -84,6 +84,21 @@ export class KeyComboImpl implements KeyCombo {
|
|||||||
get isModifier(): boolean {
|
get isModifier(): boolean {
|
||||||
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
|
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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', () => {
|
||||||
@@ -123,6 +138,37 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
return keybindingByKeyCombo.value[combo.serialize()]
|
return keybindingByKeyCombo.value[combo.serialize()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return getKeybindingsByCommandId(commandId)[0]
|
||||||
|
}
|
||||||
|
|
||||||
function addKeybinding(
|
function addKeybinding(
|
||||||
target: Ref<Record<string, KeybindingImpl>>,
|
target: Ref<Record<string, KeybindingImpl>>,
|
||||||
keybinding: KeybindingImpl,
|
keybinding: KeybindingImpl,
|
||||||
@@ -145,9 +191,23 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
function addUserKeybinding(keybinding: KeybindingImpl) {
|
function addUserKeybinding(keybinding: KeybindingImpl) {
|
||||||
const defaultKeybinding =
|
const defaultKeybinding =
|
||||||
defaultKeybindings.value[keybinding.combo.serialize()]
|
defaultKeybindings.value[keybinding.combo.serialize()]
|
||||||
if (defaultKeybinding) {
|
const userUnsetKeybinding =
|
||||||
|
userUnsetKeybindings.value[keybinding.combo.serialize()]
|
||||||
|
|
||||||
|
// User is adding back a keybinding that was an unsetted default keybinding.
|
||||||
|
if (
|
||||||
|
keybinding.equals(defaultKeybinding) &&
|
||||||
|
keybinding.equals(userUnsetKeybinding)
|
||||||
|
) {
|
||||||
|
delete userUnsetKeybindings.value[keybinding.combo.serialize()]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset keybinding on default keybinding if it exists and is not the same as userUnsetKeybinding
|
||||||
|
if (defaultKeybinding && !defaultKeybinding.equals(userUnsetKeybinding)) {
|
||||||
unsetKeybinding(defaultKeybinding)
|
unsetKeybinding(defaultKeybinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
addKeybinding(userKeybindings, keybinding, { existOk: true })
|
addKeybinding(userKeybindings, keybinding, { existOk: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +230,23 @@ export const useKeybindingStore = defineStore('keybinding', () => {
|
|||||||
throw new Error(`NOT_REACHED`)
|
throw new Error(`NOT_REACHED`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
if (currentKeybinding) {
|
||||||
|
unsetKeybinding(currentKeybinding)
|
||||||
|
}
|
||||||
|
addUserKeybinding(keybinding)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function loadUserKeybindings() {
|
function loadUserKeybindings() {
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
// Unset bindings first as new bindings might conflict with default bindings.
|
// Unset bindings first as new bindings might conflict with default bindings.
|
||||||
@@ -204,14 +281,51 @@ 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
|
||||||
|
await settingStore.set(
|
||||||
|
'Comfy.Keybinding.NewBindings',
|
||||||
|
Object.values(userKeybindings.value)
|
||||||
|
)
|
||||||
|
await settingStore.set(
|
||||||
|
'Comfy.Keybinding.UnsetBindings',
|
||||||
|
Object.values(userUnsetKeybindings.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetKeybindings() {
|
||||||
|
userKeybindings.value = {}
|
||||||
|
userUnsetKeybindings.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
keybindings,
|
keybindings,
|
||||||
getKeybinding,
|
getKeybinding,
|
||||||
|
getKeybindingsByCommandId,
|
||||||
|
getKeybindingByCommandId,
|
||||||
addDefaultKeybinding,
|
addDefaultKeybinding,
|
||||||
addUserKeybinding,
|
addUserKeybinding,
|
||||||
unsetKeybinding,
|
unsetKeybinding,
|
||||||
|
updateKeybindingOnCommand,
|
||||||
loadUserKeybindings,
|
loadUserKeybindings,
|
||||||
loadCoreKeybindings,
|
loadCoreKeybindings,
|
||||||
loadExtensionKeybindings
|
loadExtensionKeybindings,
|
||||||
|
persistUserKeybindings,
|
||||||
|
resetKeybindings,
|
||||||
|
isCommandKeybindingModified
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ export const useSettingStore = defineStore('setting', {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
set<K extends keyof Settings>(key: K, value: Settings[K]) {
|
async set<K extends keyof Settings>(key: K, value: Settings[K]) {
|
||||||
this.settingValues[key] = value
|
this.settingValues[key] = value
|
||||||
app.ui.settings.setSettingValue(key, value)
|
await app.ui.settings.setSettingValueAsync(key, value)
|
||||||
},
|
},
|
||||||
|
|
||||||
get<K extends keyof Settings>(key: K): Settings[K] {
|
get<K extends keyof Settings>(key: K): Settings[K] {
|
||||||
|
|||||||
@@ -53,6 +53,25 @@ describe('useKeybindingStore', () => {
|
|||||||
expect(store.getKeybinding(userKeybinding.combo)).toEqual(userKeybinding)
|
expect(store.getKeybinding(userKeybinding.combo)).toEqual(userKeybinding)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should allow binding to unsetted default keybindings', () => {
|
||||||
|
const store = useKeybindingStore()
|
||||||
|
const defaultKeybinding = new KeybindingImpl({
|
||||||
|
commandId: 'test.command1',
|
||||||
|
combo: { key: 'C', ctrl: true }
|
||||||
|
})
|
||||||
|
store.addDefaultKeybinding(defaultKeybinding)
|
||||||
|
store.unsetKeybinding(defaultKeybinding)
|
||||||
|
|
||||||
|
const userKeybinding = new KeybindingImpl({
|
||||||
|
commandId: 'test.command2',
|
||||||
|
combo: { key: 'C', ctrl: true }
|
||||||
|
})
|
||||||
|
store.addUserKeybinding(userKeybinding)
|
||||||
|
|
||||||
|
expect(store.keybindings).toHaveLength(1)
|
||||||
|
expect(store.getKeybinding(userKeybinding.combo)).toEqual(userKeybinding)
|
||||||
|
})
|
||||||
|
|
||||||
it('should unset user keybindings', () => {
|
it('should unset user keybindings', () => {
|
||||||
const store = useKeybindingStore()
|
const store = useKeybindingStore()
|
||||||
const keybinding = new KeybindingImpl({
|
const keybinding = new KeybindingImpl({
|
||||||
@@ -119,4 +138,29 @@ describe('useKeybindingStore', () => {
|
|||||||
|
|
||||||
expect(() => store.unsetKeybinding(keybinding)).toThrow()
|
expect(() => store.unsetKeybinding(keybinding)).toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should remove unset keybinding when adding back a default keybinding', () => {
|
||||||
|
const store = useKeybindingStore()
|
||||||
|
const defaultKeybinding = new KeybindingImpl({
|
||||||
|
commandId: 'test.command',
|
||||||
|
combo: { key: 'I', ctrl: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add default keybinding
|
||||||
|
store.addDefaultKeybinding(defaultKeybinding)
|
||||||
|
expect(store.keybindings).toHaveLength(1)
|
||||||
|
|
||||||
|
// Unset the default keybinding
|
||||||
|
store.unsetKeybinding(defaultKeybinding)
|
||||||
|
expect(store.keybindings).toHaveLength(0)
|
||||||
|
|
||||||
|
// Add the same keybinding as a user keybinding
|
||||||
|
store.addUserKeybinding(defaultKeybinding)
|
||||||
|
|
||||||
|
// Check that the keybinding is back and not in the unset list
|
||||||
|
expect(store.keybindings).toHaveLength(1)
|
||||||
|
expect(store.getKeybinding(defaultKeybinding.combo)).toEqual(
|
||||||
|
defaultKeybinding
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user