mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 09:27:41 +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:
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>
|
||||
Reference in New Issue
Block a user